@protobase/ui 0.46.3 → 0.46.5

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 (214) hide show
  1. package/dist/account/account-page.js +50 -0
  2. package/dist/account/account-pages.js +24 -9
  3. package/dist/account/account-tree.js +18 -0
  4. package/dist/account/blocks/account-loading.js +19 -0
  5. package/dist/account/blocks/connected-accounts-block.js +37 -0
  6. package/dist/account/blocks/connected-apps-block.js +21 -0
  7. package/dist/account/blocks/devices-block.js +23 -0
  8. package/dist/account/blocks/passkeys-block.js +25 -0
  9. package/dist/account/blocks/password-block.js +27 -0
  10. package/dist/account/blocks/profile-block.js +28 -0
  11. package/dist/account/blocks/two-factor-block.js +29 -0
  12. package/dist/account/blocks/use-account-data.js +24 -0
  13. package/dist/account/change-email-dialog.js +98 -0
  14. package/dist/account/connected-apps-section.js +4 -4
  15. package/dist/account/device-name.js +26 -0
  16. package/dist/account/devices-section.js +119 -0
  17. package/dist/account/messages.js +94 -2
  18. package/dist/account/passkeys-section.js +4 -4
  19. package/dist/account/password-change-dialog.js +119 -0
  20. package/dist/account/password-dialog.js +2 -2
  21. package/dist/account/password-section.js +51 -0
  22. package/dist/account/profile-section.js +107 -0
  23. package/dist/account/sign-in-policy-form.js +1 -1
  24. package/dist/account/sign-in-policy-page.js +1 -1
  25. package/dist/account/sign-in-providers-section.js +2 -2
  26. package/dist/account/staff-sign-in-log.js +1 -1
  27. package/dist/account/two-factor-section.js +1 -1
  28. package/dist/account/two-factor-setup.js +2 -2
  29. package/dist/activity-chart/activity-chart.js +2 -2
  30. package/dist/app/app.js +39 -23
  31. package/dist/app/charts/chart-view.js +2 -2
  32. package/dist/app/comments/comment-composer.js +1 -1
  33. package/dist/app/comments/comments-section.js +3 -3
  34. package/dist/app/create/create-page.js +4 -4
  35. package/dist/app/current-address.d.ts +1 -0
  36. package/dist/app/custom-section.js +4 -4
  37. package/dist/app/delete/use-delete-flow.js +1 -1
  38. package/dist/app/displays/board-column.js +1 -1
  39. package/dist/app/displays/board-display.js +2 -2
  40. package/dist/app/displays/calendar-display.js +2 -2
  41. package/dist/app/displays/custom-display.js +3 -3
  42. package/dist/app/displays/display-controls.js +1 -1
  43. package/dist/app/displays/map-display.js +2 -2
  44. package/dist/app/displays/period-controls.js +1 -1
  45. package/dist/app/displays/record-card.js +2 -2
  46. package/dist/app/displays/timeline-display.js +2 -2
  47. package/dist/app/displays/use-calendar-move.js +1 -1
  48. package/dist/app/document-title.js +2 -2
  49. package/dist/app/embedded-list/embedded-display.js +1 -1
  50. package/dist/app/embedded-list/embedded-list.js +1 -1
  51. package/dist/app/export/export-dialog.js +2 -2
  52. package/dist/app/export/use-export-flow.js +1 -1
  53. package/dist/app/field-editor.js +2 -2
  54. package/dist/app/file-field.js +3 -3
  55. package/dist/app/gallery/gallery-section.js +2 -2
  56. package/dist/app/gallery/photo-details.js +1 -1
  57. package/dist/app/gallery/use-gallery-photos.js +1 -1
  58. package/dist/app/hand-order/hand-sorted-list.js +3 -3
  59. package/dist/app/history/history-section.js +2 -2
  60. package/dist/app/import/import-button.js +1 -1
  61. package/dist/app/import/import-check-step.js +1 -1
  62. package/dist/app/incoming-card.js +2 -2
  63. package/dist/app/list-group-chart.js +1 -1
  64. package/dist/app/list-page.js +6 -6
  65. package/dist/app/messages.js +0 -2
  66. package/dist/app/meta-gate.js +1 -1
  67. package/dist/app/nav-from-meta.js +1 -1
  68. package/dist/app/new-version/new-version-banner.js +1 -1
  69. package/dist/app/new-version/new-version-button.js +1 -1
  70. package/dist/app/new-version/new-version-toast.js +1 -1
  71. package/dist/app/pages/action-inputs/action-inputs-dialog.js +2 -2
  72. package/dist/app/pages/action-inputs/input-answers.js +1 -1
  73. package/dist/app/pages/blocks/card-row.js +2 -2
  74. package/dist/app/pages/blocks/field.js +1 -1
  75. package/dist/app/pages/blocks/form-draft.js +19 -0
  76. package/dist/app/pages/blocks/modal-form.js +10 -24
  77. package/dist/app/pages/blocks/page-block.js +1 -1
  78. package/dist/app/pages/blocks/record-card.js +2 -2
  79. package/dist/app/pages/blocks/settings.js +179 -0
  80. package/dist/app/pages/blocks/stat.js +2 -2
  81. package/dist/app/pages/blocks/tabs.js +59 -0
  82. package/dist/app/pages/composed-page.js +16 -3
  83. package/dist/app/pages/custom-boundary.js +1 -1
  84. package/dist/app/pages/field-value.js +1 -1
  85. package/dist/app/pages/messages.js +6 -0
  86. package/dist/app/pages/page-tabs.js +27 -0
  87. package/dist/app/pages/record-actions.js +2 -2
  88. package/dist/app/pages/render-node.js +22 -3
  89. package/dist/app/pages/use-run-action.js +1 -1
  90. package/dist/app/project-logo.js +2 -2
  91. package/dist/app/record-chart/record-chart-section.js +4 -4
  92. package/dist/app/record-page.js +12 -12
  93. package/dist/app/record-row/record-row-body.js +1 -1
  94. package/dist/app/record-row/saved-record-row.js +1 -1
  95. package/dist/app/related-cards/related-card.js +1 -1
  96. package/dist/app/related-dots/related-dots-columns.js +1 -1
  97. package/dist/app/related-section.js +2 -2
  98. package/dist/app/relation-picker.js +1 -1
  99. package/dist/app/router.js +2 -2
  100. package/dist/app/saved/saved-list.js +1 -1
  101. package/dist/app/search/use-global-search.js +1 -1
  102. package/dist/app/searches/save-search-button.js +1 -1
  103. package/dist/app/searches/saved-search-rows.js +1 -1
  104. package/dist/app/searches/saved-search-section.js +2 -2
  105. package/dist/app/searches/saved-searches.js +3 -3
  106. package/dist/app/searches/search-controls.js +2 -2
  107. package/dist/app/searches/search-name-dialog.js +1 -1
  108. package/dist/app/searches/update-search-dialog.js +2 -2
  109. package/dist/app/searches/use-save-search.js +1 -1
  110. package/dist/app/sensitive-field.js +2 -2
  111. package/dist/app/source-notice.js +1 -1
  112. package/dist/app/staff-banner.js +2 -2
  113. package/dist/app/use-app-address.js +2 -2
  114. package/dist/app/use-incoming-counts.js +1 -1
  115. package/dist/app-shell/app-shell.js +2 -1
  116. package/dist/app-shell/breadcrumb.js +1 -1
  117. package/dist/app-shell/drawer.js +1 -1
  118. package/dist/app-shell/global-search.js +1 -1
  119. package/dist/app-shell/profile-menu.js +27 -12
  120. package/dist/app-shell/sidebar.js +2 -1
  121. package/dist/assistant-dock/widget-view.js +1 -1
  122. package/dist/auth/auth-gate.js +2 -2
  123. package/dist/auth/auth-messages.js +2 -2
  124. package/dist/auth/auth-provider.js +4 -0
  125. package/dist/auth/code-form.js +1 -1
  126. package/dist/auth/email-code-form.js +1 -1
  127. package/dist/auth/email-code-page.js +1 -1
  128. package/dist/auth/forgot-password-form.js +4 -4
  129. package/dist/auth/provider-sign-in-page.js +2 -2
  130. package/dist/auth/reset-password-form.js +1 -1
  131. package/dist/auth/setup-required-form.js +1 -1
  132. package/dist/auth/setup-required-page.js +2 -2
  133. package/dist/auth/sign-in-form.js +2 -2
  134. package/dist/auth/sign-in-page.js +1 -1
  135. package/dist/auth/sign-in-prompt.js +2 -2
  136. package/dist/auth/sign-up-code-form.js +1 -1
  137. package/dist/auth/sign-up-details-form.js +2 -2
  138. package/dist/auth/sign-up-email-form.js +1 -1
  139. package/dist/auth/sign-up-page.js +1 -1
  140. package/dist/auth/staff-sign-in-form.js +2 -2
  141. package/dist/auth/staff-sign-in-page.js +1 -1
  142. package/dist/auth/two-factor-form.js +3 -3
  143. package/dist/auth/two-factor-page.js +1 -1
  144. package/dist/chat/composer.js +1 -1
  145. package/dist/connect/connect-page.js +4 -4
  146. package/dist/data/meta-data.d.ts +3 -1
  147. package/dist/data/use-meta.js +1 -0
  148. package/dist/data-grid/cell-text.js +1 -1
  149. package/dist/data-grid/cells.js +3 -3
  150. package/dist/data-grid/grid-header.js +1 -1
  151. package/dist/data-grid/grid-row.js +2 -2
  152. package/dist/data-grid/row-menu.js +1 -1
  153. package/dist/filter-panel/facet-group.js +1 -1
  154. package/dist/forms/public-form-page.js +5 -5
  155. package/dist/index.js +20 -20
  156. package/dist/inline-child-table/line-row.js +1 -1
  157. package/dist/inline-child-table/totals-footer.js +1 -1
  158. package/dist/invitations/invite-form.js +1 -1
  159. package/dist/lists/paged-search-list.js +1 -1
  160. package/dist/live/columns-from-model.js +1 -1
  161. package/dist/live/use-filter-config.js +1 -1
  162. package/dist/live/use-reference-search.js +1 -1
  163. package/dist/notifications/bell/notification-bell.js +1 -1
  164. package/dist/notifications/bell/notification-item.js +2 -2
  165. package/dist/organizations/all-organizations.js +1 -1
  166. package/dist/organizations/invitation-page.js +6 -6
  167. package/dist/organizations/invitations-page.js +3 -3
  168. package/dist/organizations/members-page.js +5 -5
  169. package/dist/organizations/organization-page.js +3 -3
  170. package/dist/organizations/organization-row.js +1 -1
  171. package/dist/organizations/organizations-page.js +3 -3
  172. package/dist/organizations/use-organizations.js +1 -1
  173. package/dist/package.js +1 -1
  174. package/dist/plugins/before-sign-in.js +1 -1
  175. package/dist/plugins/plugin-gate.js +1 -1
  176. package/dist/primitives/sheet.js +1 -1
  177. package/dist/record-view/field.js +2 -2
  178. package/dist/record-view/record-header.js +1 -1
  179. package/dist/record-view/record-tabs.js +1 -1
  180. package/dist/roles/use-active-role.js +1 -1
  181. package/dist/shell/assets/app-DYCHMbTU.js +9 -0
  182. package/dist/shell/assets/index-CT4BrHx-.js +2 -0
  183. package/dist/shell/assets/index-zVvlyz9B.css +2 -0
  184. package/dist/shell/assets/{maplibre-BlwO7zTh.js → maplibre-CjNYwEqW.js} +3 -3
  185. package/dist/shell/assets/{preload-helper-C24C2MCK.js → preload-helper-SYSsmQ2O.js} +2 -2
  186. package/dist/shell/assets/{record-map-QajKJid1.js → record-map-DjqQGqZ2.js} +2 -2
  187. package/dist/shell/assets/{records-map-DybHhZlM.js → records-map-CZB4A_qN.js} +2 -2
  188. package/dist/shell/assets/shared-protobase-ui-DVOkKNxu.js +2 -0
  189. package/dist/shell/assets/vendor-BzlIWwX2.js +63 -0
  190. package/dist/shell/index.html +6 -6
  191. package/dist/shell/protobase.shell.json +1 -1
  192. package/dist/shell/protobase.sw.js +2 -2
  193. package/dist/shell-source-maps/assets/app-DYCHMbTU.js.map +1 -0
  194. package/dist/shell-source-maps/assets/{index-CY22munM.js.map → index-CT4BrHx-.js.map} +1 -1
  195. package/dist/shell-source-maps/assets/{maplibre-BlwO7zTh.js.map → maplibre-CjNYwEqW.js.map} +1 -1
  196. package/dist/shell-source-maps/assets/{preload-helper-C24C2MCK.js.map → preload-helper-SYSsmQ2O.js.map} +1 -1
  197. package/dist/shell-source-maps/assets/{record-map-QajKJid1.js.map → record-map-DjqQGqZ2.js.map} +1 -1
  198. package/dist/shell-source-maps/assets/{records-map-DybHhZlM.js.map → records-map-CZB4A_qN.js.map} +1 -1
  199. package/dist/shell-source-maps/assets/{shared-protobase-ui-TOG-RVkz.js.map → shared-protobase-ui-DVOkKNxu.js.map} +1 -1
  200. package/dist/shell-source-maps/assets/vendor-BzlIWwX2.js.map +1 -0
  201. package/dist/side-panel/side-panel.js +1 -1
  202. package/dist/toasts/toast.js +1 -1
  203. package/dist/users/create-user-form.js +1 -1
  204. package/dist/users/use-user-roles.js +1 -1
  205. package/dist/users/user-row.js +1 -1
  206. package/package.json +4 -4
  207. package/dist/account/account-security-page.js +0 -97
  208. package/dist/shell/assets/app-BDvKMNNB.js +0 -9
  209. package/dist/shell/assets/index-CY22munM.js +0 -2
  210. package/dist/shell/assets/index-DHaeB8cs.css +0 -2
  211. package/dist/shell/assets/shared-protobase-ui-TOG-RVkz.js +0 -2
  212. package/dist/shell/assets/vendor-C4-Hop1T.js +0 -63
  213. package/dist/shell-source-maps/assets/app-BDvKMNNB.js.map +0 -1
  214. package/dist/shell-source-maps/assets/vendor-C4-Hop1T.js.map +0 -1
@@ -0,0 +1,50 @@
1
+ import { text } from "./messages.js";
2
+ import { accountTabPath } from "./account-pages.js";
3
+ import { useAuth } from "../auth/auth-provider.js";
4
+ import { Notice, useAdminMeta } from "../app/meta-gate.js";
5
+ import { PageTabsProvider } from "../app/pages/page-tabs.js";
6
+ import { linkableProviders } from "./blocks/connected-accounts-block.js";
7
+ import { RenderNode } from "../app/pages/render-node.js";
8
+ import { shownAccountTree } from "./account-tree.js";
9
+ import { useMemo } from "react";
10
+ import { jsx } from "react/jsx-runtime";
11
+ //#region src/account/account-page.tsx
12
+ /**
13
+ * The signed-in person's account page as `/meta` gave it to them, the app's own or the standard one, without the
14
+ * account blocks that have nothing to show here and the tabs they leave empty; `undefined` when `/meta` has none.
15
+ */
16
+ const useAccountPage = () => {
17
+ const { account } = useAdminMeta();
18
+ const auth = useAuth();
19
+ const { mcp } = auth;
20
+ const linkable = linkableProviders(auth).length > 0;
21
+ return useMemo(() => {
22
+ if (!account) return void 0;
23
+ const hidden = /* @__PURE__ */ new Set([...mcp ? [] : ["ConnectedApps"], ...linkable ? [] : ["ConnectedAccounts"]]);
24
+ return {
25
+ ...account,
26
+ tree: shownAccountTree(account.tree, hidden)
27
+ };
28
+ }, [
29
+ account,
30
+ mcp,
31
+ linkable
32
+ ]);
33
+ };
34
+ /** `/-/account`, and `/-/account/<tab>` for one of its tabs: the signed-in person's account page. */
35
+ const AccountPage = ({ tab }) => {
36
+ const page = useAccountPage();
37
+ if (!page) return /* @__PURE__ */ jsx(Notice, {
38
+ title: text("noAccount"),
39
+ children: text("noAccountDescription")
40
+ });
41
+ return /* @__PURE__ */ jsx(PageTabsProvider, {
42
+ route: {
43
+ ...tab && { tab },
44
+ pathOf: accountTabPath
45
+ },
46
+ children: /* @__PURE__ */ jsx(RenderNode, { node: page.tree })
47
+ });
48
+ };
49
+ //#endregion
50
+ export { AccountPage, useAccountPage };
@@ -1,15 +1,30 @@
1
1
  import { text } from "./messages.js";
2
- import { KeyRound, ShieldCheck } from "lucide-react";
2
+ import { ShieldCheck, UserRound } from "lucide-react";
3
3
  //#region src/account/account-pages.ts
4
- const accountSecurityPath = "/-/account";
4
+ const accountPath = "/-/account";
5
5
  const signInPolicyPath = "/-/sign-in-policy";
6
- /** The app's own pages for the signed-in person, at the end of the profile menu: their sign-in, and for admins the policy. */
6
+ /** The tab of the account page `path` shows, `{}` for its first, or `undefined` for a path outside it. */
7
+ const readAccountPath = (path) => {
8
+ if (path === "/-/account") return {};
9
+ if (!path.startsWith(`/-/account/`)) return void 0;
10
+ const tab = path.slice(11);
11
+ if (tab === "" || tab.includes("/")) return void 0;
12
+ try {
13
+ return { tab: decodeURIComponent(tab) };
14
+ } catch (error) {
15
+ if (error instanceof URIError) return void 0;
16
+ throw error;
17
+ }
18
+ };
19
+ /** The path of the account page's tab `tab`, or of the page itself (its first tab). */
20
+ const accountTabPath = (tab) => tab ? `${accountPath}/${encodeURIComponent(tab)}` : accountPath;
21
+ /** The app's own pages for the signed-in person, the profile menu's last group: their account, and for admins the policy. */
7
22
  const accountMenuItems = (basePath, path, admin) => [{
8
- id: "account-security",
9
- label: text("signInSecurity"),
10
- icon: KeyRound,
11
- href: `${basePath}${accountSecurityPath}`,
12
- active: path === accountSecurityPath
23
+ id: "account",
24
+ label: text("account"),
25
+ icon: UserRound,
26
+ href: `${basePath}${accountPath}`,
27
+ active: readAccountPath(path) !== void 0
13
28
  }, ...admin ? [{
14
29
  id: "sign-in-policy",
15
30
  label: text("signInPolicy"),
@@ -18,4 +33,4 @@ const accountMenuItems = (basePath, path, admin) => [{
18
33
  active: path === signInPolicyPath
19
34
  }] : []];
20
35
  //#endregion
21
- export { accountMenuItems, accountSecurityPath, signInPolicyPath };
36
+ export { accountMenuItems, accountPath, accountTabPath, readAccountPath, signInPolicyPath };
@@ -0,0 +1,18 @@
1
+ import { mapTree } from "@protobase/layout";
2
+ //#region src/account/account-tree.ts
3
+ /**
4
+ * The account page's tree without the account blocks named in `hidden`, those with nothing to show for this app, and
5
+ * without the tabs that leaves empty, so a tab such as "Connected apps" is not offered on an app without MCP.
6
+ */
7
+ const shownAccountTree = (tree, hidden) => {
8
+ const shown = mapTree(tree, (node) => {
9
+ if (node.custom) return node;
10
+ if (hidden.has(node.type)) return null;
11
+ if ((node.type === "Tab" || node.type === "Tabs") && node.children.length === 0) return null;
12
+ return node;
13
+ });
14
+ if (!shown) throw new Error("The account page keeps its Page, which no hidden block replaces");
15
+ return shown;
16
+ };
17
+ //#endregion
18
+ export { shownAccountTree };
@@ -0,0 +1,19 @@
1
+ import { text } from "../messages.js";
2
+ import { Spinner } from "../../primitives/spinner.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/account/blocks/account-loading.tsx
5
+ /** An account block whose data is not in yet: why it could not be read, or that it is loading. */
6
+ const AccountLoading = ({ error }) => error ? /* @__PURE__ */ jsx("p", {
7
+ role: "alert",
8
+ className: "text-danger-text",
9
+ children: error
10
+ }) : /* @__PURE__ */ jsxs("div", {
11
+ className: "flex items-center gap-2 text-muted-foreground",
12
+ children: [
13
+ /* @__PURE__ */ jsx(Spinner, {}),
14
+ " ",
15
+ text("loading")
16
+ ]
17
+ });
18
+ //#endregion
19
+ export { AccountLoading };
@@ -0,0 +1,37 @@
1
+ import { text } from "../messages.js";
2
+ import { readProviderError, withoutSignInLink } from "../../auth/provider-link.js";
3
+ import { useAuth } from "../../auth/auth-provider.js";
4
+ import { providerMessage } from "../../auth/auth-messages.js";
5
+ import { SignInProvidersSection } from "../sign-in-providers-section.js";
6
+ import { AccountLoading } from "./account-loading.js";
7
+ import { useAccountData } from "./use-account-data.js";
8
+ import { useCallback, useState } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ //#region src/account/blocks/connected-accounts-block.tsx
11
+ const takeLinkError = () => {
12
+ const code = readProviderError(window.location.href);
13
+ if (code) window.history.replaceState(window.history.state, "", withoutSignInLink(window.location.href));
14
+ return code;
15
+ };
16
+ const Linked = ({ providers }) => {
17
+ const { session, linkProvider } = useAuth();
18
+ const { account } = session;
19
+ const [linkError] = useState(takeLinkError);
20
+ const { data: linked, error } = useAccountData(useCallback(() => account.linkedProviders(), [account]), text("couldNotReadMethods"));
21
+ if (!linked) return /* @__PURE__ */ jsx(AccountLoading, { error });
22
+ return /* @__PURE__ */ jsx(SignInProvidersSection, {
23
+ providers,
24
+ linked,
25
+ onConnect: linkProvider,
26
+ error: linkError && providerMessage(linkError, providers.length === 1 ? providers[0].name : text("theProvider"))
27
+ });
28
+ };
29
+ /** The sign-in providers an account links to from here: the OAuth ones, as a SAML identity provider links it by signing in. */
30
+ const linkableProviders = ({ signInProviders, socialProviders }) => signInProviders.filter((provider) => socialProviders.includes(provider.id));
31
+ /** The `ConnectedAccounts` block: the OAuth providers the signed-in person can link, hidden when the app has none. */
32
+ const ConnectedAccountsBlock = () => {
33
+ const linkable = linkableProviders(useAuth());
34
+ return linkable.length > 0 ? /* @__PURE__ */ jsx(Linked, { providers: linkable }) : null;
35
+ };
36
+ //#endregion
37
+ export { ConnectedAccountsBlock, linkableProviders };
@@ -0,0 +1,21 @@
1
+ import { text } from "../messages.js";
2
+ import { useAuth } from "../../auth/auth-provider.js";
3
+ import { AccountLoading } from "./account-loading.js";
4
+ import { useAccountData } from "./use-account-data.js";
5
+ import { ConnectedAppsSection } from "../connected-apps-section.js";
6
+ import { useCallback } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/account/blocks/connected-apps-block.tsx
9
+ const Apps = () => {
10
+ const { account } = useAuth().session;
11
+ const { data: apps, error, reload } = useAccountData(useCallback(() => account.connectedApps(), [account]), text("couldNotReadMethods"));
12
+ if (!apps) return /* @__PURE__ */ jsx(AccountLoading, { error });
13
+ return /* @__PURE__ */ jsx(ConnectedAppsSection, {
14
+ apps,
15
+ onDisconnect: (clientId) => account.disconnectApp(clientId).then(reload)
16
+ });
17
+ };
18
+ /** The `ConnectedApps` block: the apps the signed-in person allowed to act as them, hidden when the app has no MCP. */
19
+ const ConnectedAppsBlock = () => useAuth().mcp ? /* @__PURE__ */ jsx(Apps, {}) : null;
20
+ //#endregion
21
+ export { ConnectedAppsBlock };
@@ -0,0 +1,23 @@
1
+ import { text } from "../messages.js";
2
+ import { useAuth } from "../../auth/auth-provider.js";
3
+ import { AccountLoading } from "./account-loading.js";
4
+ import { useAccountData } from "./use-account-data.js";
5
+ import { DevicesSection } from "../devices-section.js";
6
+ import { useCallback } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/account/blocks/devices-block.tsx
9
+ /** The `Devices` block: the browsers and apps signed in as the person, this one marked, the others to sign out. */
10
+ const DevicesBlock = () => {
11
+ const { session, staffSignIn } = useAuth();
12
+ const { account } = session;
13
+ const { data: sessions, error, reload } = useAccountData(useCallback(() => account.sessions(), [account]), text("couldNotReadDevices"));
14
+ if (!sessions) return /* @__PURE__ */ jsx(AccountLoading, { error });
15
+ return /* @__PURE__ */ jsx(DevicesSection, {
16
+ sessions,
17
+ operator: staffSignIn,
18
+ onSignOut: (token) => account.revokeSession(token).then(reload),
19
+ onSignOutOthers: () => account.revokeOtherSessions().then(reload)
20
+ });
21
+ };
22
+ //#endregion
23
+ export { DevicesBlock };
@@ -0,0 +1,25 @@
1
+ import { text } from "../messages.js";
2
+ import { useAuth } from "../../auth/auth-provider.js";
3
+ import { AccountLoading } from "./account-loading.js";
4
+ import { useAccountData } from "./use-account-data.js";
5
+ import { PasskeysSection } from "../passkeys-section.js";
6
+ import { useCallback } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/account/blocks/passkeys-block.tsx
9
+ /** The `Passkeys` block: the signed-in person's passkeys, within what the sign-in policy allows. */
10
+ const PasskeysBlock = () => {
11
+ const { account } = useAuth().session;
12
+ const load = useCallback(() => Promise.all([account.signInMethods(), account.passkeys()]), [account]);
13
+ const { data, error, reload } = useAccountData(load, text("couldNotReadMethods"));
14
+ if (!data) return /* @__PURE__ */ jsx(AccountLoading, { error });
15
+ const [methods, passkeys] = data;
16
+ return /* @__PURE__ */ jsx(PasskeysSection, {
17
+ rule: methods.policy.passkey,
18
+ passkeys,
19
+ onAdd: () => account.addPasskey().then(reload),
20
+ onRename: (id, name) => account.renamePasskey(id, name).then(reload),
21
+ onRemove: (id) => account.removePasskey(id).then(reload)
22
+ });
23
+ };
24
+ //#endregion
25
+ export { PasskeysBlock };
@@ -0,0 +1,27 @@
1
+ import { text } from "../messages.js";
2
+ import { useAuth } from "../../auth/auth-provider.js";
3
+ import { AccountLoading } from "./account-loading.js";
4
+ import { useAccountData } from "./use-account-data.js";
5
+ import { PasswordSection } from "../password-section.js";
6
+ import { useCallback } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/account/blocks/password-block.tsx
9
+ /**
10
+ * The `Password` block: the signed-in person changes their password, or sets a first one without signing anyone out,
11
+ * within what the sign-in policy allows; staff signed in as the person cannot.
12
+ */
13
+ const PasswordBlock = () => {
14
+ const { session, state } = useAuth();
15
+ const { account } = session;
16
+ const { data: methods, error, reload } = useAccountData(useCallback(() => account.signInMethods(), [account]), text("couldNotReadMethods"));
17
+ if (!methods) return /* @__PURE__ */ jsx(AccountLoading, { error });
18
+ return /* @__PURE__ */ jsx(PasswordSection, {
19
+ rule: methods.policy.password,
20
+ hasPassword: methods.account.password,
21
+ staff: state.kind === "signed-in" && Boolean(state.staff),
22
+ onChange: (current, next, signOutOthers) => account.changePassword(current, next, signOutOthers).then(reload),
23
+ onSet: (next) => account.setPassword(next).then(reload)
24
+ });
25
+ };
26
+ //#endregion
27
+ export { PasswordBlock };
@@ -0,0 +1,28 @@
1
+ import { text } from "../messages.js";
2
+ import { useAuth } from "../../auth/auth-provider.js";
3
+ import { AccountLoading } from "./account-loading.js";
4
+ import { useAccountData } from "./use-account-data.js";
5
+ import { ProfileSection } from "../profile-section.js";
6
+ import { useCallback } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/account/blocks/profile-block.tsx
9
+ /** The `Profile` block: the signed-in person's name and address, which they can change; the address only with mail. */
10
+ const ProfileBlock = () => {
11
+ const { session, changeEmail } = useAuth();
12
+ const { data: user, loaded, error, reload } = useAccountData(useCallback(() => session.session(), [session]), text("couldNotReadProfile"));
13
+ if (!loaded) return /* @__PURE__ */ jsx(AccountLoading, { error });
14
+ if (!user) return null;
15
+ const emailChange = changeEmail ? {
16
+ request: session.account.requestEmailChange,
17
+ confirm: session.account.changeEmail
18
+ } : void 0;
19
+ return /* @__PURE__ */ jsx(ProfileSection, {
20
+ name: user.name,
21
+ email: user.email,
22
+ onRename: (name) => session.account.updateName(name).then(reload),
23
+ emailChange,
24
+ onEmailChanged: reload
25
+ });
26
+ };
27
+ //#endregion
28
+ export { ProfileBlock };
@@ -0,0 +1,29 @@
1
+ import { text } from "../messages.js";
2
+ import { useAuth } from "../../auth/auth-provider.js";
3
+ import { AccountLoading } from "./account-loading.js";
4
+ import { useAccountData } from "./use-account-data.js";
5
+ import { TwoFactorSection } from "../two-factor-section.js";
6
+ import { useCallback } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/account/blocks/two-factor-block.tsx
9
+ /** The `TwoFactor` block: the signed-in person's two-factor authentication, within what the sign-in policy allows. */
10
+ const TwoFactorBlock = () => {
11
+ const { account } = useAuth().session;
12
+ const { data: methods, error, reload } = useAccountData(useCallback(() => account.signInMethods(), [account]), text("couldNotReadMethods"));
13
+ if (!methods) return /* @__PURE__ */ jsx(AccountLoading, { error });
14
+ return /* @__PURE__ */ jsx(TwoFactorSection, {
15
+ rule: methods.policy.twoFactor,
16
+ enabled: methods.account.twoFactor,
17
+ authenticatorApp: methods.account.authenticatorApp,
18
+ mail: methods.mail,
19
+ needsPassword: methods.account.password,
20
+ onStartApp: (password) => account.startAuthenticatorApp(password),
21
+ onConfirmApp: (code) => account.confirmAuthenticatorApp(code),
22
+ onEmailedCodes: (password) => account.turnOnEmailedCodes(password),
23
+ onTurnOff: (password) => account.turnOffTwoFactor(password),
24
+ onNewBackupCodes: (password) => account.newBackupCodes(password),
25
+ onChanged: reload
26
+ });
27
+ };
28
+ //#endregion
29
+ export { TwoFactorBlock };
@@ -0,0 +1,24 @@
1
+ import { authMessage } from "../../auth/auth-messages.js";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { AuthError } from "@protobase/client";
4
+ //#region src/account/blocks/use-account-data.ts
5
+ /**
6
+ * What an account block reads, `undefined` until it is in, with `reload` to read it again after a change so the block
7
+ * shows what the server now has. A failure shows the server's reason, or `fallback`.
8
+ */
9
+ const useAccountData = (load, fallback) => {
10
+ const [data, setData] = useState();
11
+ const [error, setError] = useState();
12
+ const reload = useCallback(() => {
13
+ load().then((value) => setData({ value }), (failure) => setError(failure instanceof AuthError ? authMessage(failure) : fallback));
14
+ }, [load, fallback]);
15
+ useEffect(reload, [reload]);
16
+ return {
17
+ data: data?.value,
18
+ loaded: data !== void 0,
19
+ error,
20
+ reload
21
+ };
22
+ };
23
+ //#endregion
24
+ export { useAccountData };
@@ -0,0 +1,98 @@
1
+ import { text } from "./messages.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { Dialog } from "../primitives/dialog.js";
4
+ import { Input } from "../primitives/input.js";
5
+ import { SectionError } from "./account-section.js";
6
+ import { authMessage } from "../auth/auth-messages.js";
7
+ import { OtpInput } from "../primitives/otp-input.js";
8
+ import { useState } from "react";
9
+ import { AuthError } from "@protobase/client";
10
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/account/change-email-dialog.tsx
12
+ /** Asks for the new address, mails it a code, and moves the account to it once the code is right. */
13
+ const ChangeEmailDialog = ({ email, change, onDone, onClose }) => {
14
+ const [draft, setDraft] = useState("");
15
+ const [sentTo, setSentTo] = useState();
16
+ const [code, setCode] = useState("");
17
+ const [busy, setBusy] = useState(false);
18
+ const [error, setError] = useState();
19
+ const run = async (step) => {
20
+ setBusy(true);
21
+ setError(void 0);
22
+ try {
23
+ await step();
24
+ } catch (failure) {
25
+ if (!(failure instanceof AuthError)) throw failure;
26
+ setError(authMessage(failure));
27
+ }
28
+ setBusy(false);
29
+ };
30
+ const newEmail = draft.trim();
31
+ const send = () => run(async () => {
32
+ await change.request(newEmail);
33
+ setCode("");
34
+ setSentTo(newEmail);
35
+ });
36
+ const confirm = (complete) => run(async () => {
37
+ await change.confirm(sentTo, complete);
38
+ onDone();
39
+ });
40
+ if (!sentTo) return /* @__PURE__ */ jsxs(Dialog, {
41
+ title: text("changeEmail"),
42
+ description: text("changeEmailDescription"),
43
+ onClose,
44
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
45
+ onClick: onClose,
46
+ children: text("cancel")
47
+ }), /* @__PURE__ */ jsx(Button, {
48
+ variant: "primary",
49
+ loading: busy,
50
+ disabled: !newEmail || newEmail.toLowerCase() === email.toLowerCase(),
51
+ onClick: () => void send(),
52
+ children: text("sendCode")
53
+ })] }),
54
+ children: [/* @__PURE__ */ jsxs("label", {
55
+ className: "flex flex-col gap-1.5 font-medium",
56
+ children: [text("newEmail"), /* @__PURE__ */ jsx(Input, {
57
+ type: "email",
58
+ autoComplete: "email",
59
+ autoFocus: true,
60
+ value: draft,
61
+ onChange: (event) => setDraft(event.target.value)
62
+ })]
63
+ }), /* @__PURE__ */ jsx(SectionError, { message: error })]
64
+ });
65
+ return /* @__PURE__ */ jsxs(Dialog, {
66
+ title: text("changeEmail"),
67
+ description: text("codeSentTo", sentTo),
68
+ onClose,
69
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
70
+ variant: "ghost",
71
+ onClick: () => {
72
+ setSentTo(void 0);
73
+ setError(void 0);
74
+ },
75
+ children: text("otherAddress")
76
+ }), /* @__PURE__ */ jsx(Button, {
77
+ variant: "primary",
78
+ loading: busy,
79
+ disabled: code.length < 6,
80
+ onClick: () => void confirm(code),
81
+ children: text("changeEmail")
82
+ })] }),
83
+ children: [/* @__PURE__ */ jsxs("label", {
84
+ className: "flex flex-col gap-1.5 font-medium",
85
+ children: [text("code"), /* @__PURE__ */ jsx(OtpInput, {
86
+ name: "code",
87
+ required: true,
88
+ autoFocus: true,
89
+ value: code,
90
+ invalid: Boolean(error),
91
+ onChange: setCode,
92
+ onComplete: (complete) => !busy && void confirm(complete)
93
+ })]
94
+ }), /* @__PURE__ */ jsx(SectionError, { message: error })]
95
+ });
96
+ };
97
+ //#endregion
98
+ export { ChangeEmailDialog };
@@ -1,10 +1,10 @@
1
- import { authMessage } from "../auth/auth-messages.js";
2
- import { clientName } from "../connect/client-name.js";
3
- import { clientPublisher } from "../connect/client-publisher.js";
4
- import { formatDate } from "../format/date.js";
5
1
  import { Button } from "../primitives/button.js";
6
2
  import { Dialog } from "../primitives/dialog.js";
3
+ import { formatDate } from "../format/date.js";
7
4
  import { AccountSection, SectionError } from "./account-section.js";
5
+ import { authMessage } from "../auth/auth-messages.js";
6
+ import { clientName } from "../connect/client-name.js";
7
+ import { clientPublisher } from "../connect/client-publisher.js";
8
8
  import { useState } from "react";
9
9
  import { Bot } from "lucide-react";
10
10
  import { AuthError } from "@protobase/client";
@@ -0,0 +1,26 @@
1
+ //#region src/account/device-name.ts
2
+ const browsers = [
3
+ [/Edg(e|A|iOS)?\//, "Edge"],
4
+ [/OPR\/|Opera/, "Opera"],
5
+ [/SamsungBrowser\//, "Samsung Internet"],
6
+ [/Firefox\/|FxiOS\//, "Firefox"],
7
+ [/Chrome\/|CriOS\//, "Chrome"],
8
+ [/Safari\//, "Safari"]
9
+ ];
10
+ const systems = [
11
+ [/iPhone/, "iPhone"],
12
+ [/iPad/, "iPad"],
13
+ [/Android/, "Android"],
14
+ [/CrOS/, "ChromeOS"],
15
+ [/Windows/, "Windows"],
16
+ [/Mac OS X|Macintosh/, "macOS"],
17
+ [/Linux/, "Linux"]
18
+ ];
19
+ const find = (list, userAgent) => list.find(([pattern]) => pattern.test(userAgent))?.[1];
20
+ /** The browser and system a user agent names, such as Chrome and macOS, each `undefined` when it names none we know. */
21
+ const deviceOf = (userAgent) => userAgent ? {
22
+ browser: find(browsers, userAgent),
23
+ system: find(systems, userAgent)
24
+ } : {};
25
+ //#endregion
26
+ export { deviceOf };
@@ -0,0 +1,119 @@
1
+ import { text } from "./messages.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { Dialog } from "../primitives/dialog.js";
4
+ import { formatDateTime } from "../format/date.js";
5
+ import { Badge } from "../primitives/badge.js";
6
+ import { AccountSection, SectionError } from "./account-section.js";
7
+ import { authMessage } from "../auth/auth-messages.js";
8
+ import { deviceOf } from "./device-name.js";
9
+ import { useState } from "react";
10
+ import { Laptop, ShieldUser, Smartphone, Tablet } from "lucide-react";
11
+ import { AuthError } from "@protobase/client";
12
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
13
+ //#region src/account/devices-section.tsx
14
+ const nameOf = (session, operator) => {
15
+ if (session.staff) return operator ? text("staffOf", operator) : text("staff");
16
+ const { browser, system } = deviceOf(session.userAgent);
17
+ if (browser && system) return text("browserOn", browser, system);
18
+ if (system) return text("aBrowserOn", system);
19
+ return browser ?? text("unknownDevice");
20
+ };
21
+ const iconOf = (session) => {
22
+ if (session.staff) return ShieldUser;
23
+ const { system } = deviceOf(session.userAgent);
24
+ if (system === "iPhone" || system === "Android") return Smartphone;
25
+ if (system === "iPad") return Tablet;
26
+ return Laptop;
27
+ };
28
+ const ordered = (sessions) => [...sessions].sort((a, b) => Number(b.current) - Number(a.current) || b.createdAt.localeCompare(a.createdAt));
29
+ /** The devices signed in to the person's account, this one marked, each other one of which they can sign out. */
30
+ const DevicesSection = ({ sessions, operator, onSignOut, onSignOutOthers }) => {
31
+ const [busy, setBusy] = useState(false);
32
+ const [error, setError] = useState();
33
+ const [confirming, setConfirming] = useState();
34
+ const others = sessions.filter((session) => !session.current);
35
+ const signOut = async (what) => {
36
+ setBusy(true);
37
+ setError(void 0);
38
+ try {
39
+ await (what.kind === "one" ? onSignOut(what.session.token) : onSignOutOthers());
40
+ setConfirming(void 0);
41
+ } catch (failure) {
42
+ if (!(failure instanceof AuthError)) throw failure;
43
+ setError(authMessage(failure));
44
+ }
45
+ setBusy(false);
46
+ };
47
+ const open = (what) => {
48
+ setError(void 0);
49
+ setConfirming(what);
50
+ };
51
+ return /* @__PURE__ */ jsxs(AccountSection, {
52
+ title: text("devices"),
53
+ description: text("devicesDescription"),
54
+ children: [
55
+ sessions.length === 0 ? /* @__PURE__ */ jsx("p", {
56
+ className: "text-muted-foreground",
57
+ children: text("noDevices")
58
+ }) : /* @__PURE__ */ jsx("ul", {
59
+ className: "divide-y rounded-md border",
60
+ children: ordered(sessions).map((session) => {
61
+ const Icon = iconOf(session);
62
+ return /* @__PURE__ */ jsxs("li", {
63
+ className: "flex flex-wrap items-center gap-3 px-3 py-2.5",
64
+ children: [
65
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground" }),
66
+ /* @__PURE__ */ jsxs("span", {
67
+ className: "min-w-0 flex-1",
68
+ children: [/* @__PURE__ */ jsxs("span", {
69
+ className: "flex flex-wrap items-center gap-2",
70
+ children: [/* @__PURE__ */ jsx("bdi", {
71
+ className: "truncate font-medium",
72
+ children: nameOf(session, operator)
73
+ }), session.current && /* @__PURE__ */ jsx(Badge, {
74
+ tone: "green",
75
+ children: text("thisDevice")
76
+ })]
77
+ }), /* @__PURE__ */ jsxs("span", {
78
+ className: "block text-xs text-muted-foreground",
79
+ children: [text("signedInFromUntil", formatDateTime(Date.parse(session.createdAt)), formatDateTime(Date.parse(session.expiresAt))), !session.staff && session.ipAddress && ` · ${session.ipAddress}`]
80
+ })]
81
+ }),
82
+ !session.current && /* @__PURE__ */ jsx(Button, {
83
+ size: "sm",
84
+ variant: "ghost",
85
+ onClick: () => open({
86
+ kind: "one",
87
+ session
88
+ }),
89
+ children: text("signOut")
90
+ })
91
+ ]
92
+ }, session.token);
93
+ })
94
+ }),
95
+ others.length > 0 && /* @__PURE__ */ jsx(Button, {
96
+ className: "mt-3",
97
+ onClick: () => open({ kind: "others" }),
98
+ children: text("signOutOthers")
99
+ }),
100
+ confirming && /* @__PURE__ */ jsx(Dialog, {
101
+ title: confirming.kind === "one" ? text("signOutDevice", nameOf(confirming.session, operator)) : text("signOutOthersTitle"),
102
+ description: confirming.kind === "one" ? text("signOutDeviceDescription") : text("signOutOthersDescription"),
103
+ onClose: () => setConfirming(void 0),
104
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
105
+ onClick: () => setConfirming(void 0),
106
+ children: text("cancel")
107
+ }), /* @__PURE__ */ jsx(Button, {
108
+ variant: "danger",
109
+ loading: busy,
110
+ onClick: () => void signOut(confirming),
111
+ children: text("signOut")
112
+ })] }),
113
+ children: /* @__PURE__ */ jsx(SectionError, { message: error })
114
+ })
115
+ ]
116
+ });
117
+ };
118
+ //#endregion
119
+ export { DevicesSection };