@protobase/ui 0.15.4 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/account/two-factor-setup.js +6 -5
  2. package/dist/app/app.js +2 -1
  3. package/dist/app/create/create-page.js +1 -0
  4. package/dist/app/create/create-record-form.js +2 -1
  5. package/dist/app/displays/custom-display.js +1 -1
  6. package/dist/app/displays/map-display.js +50 -34
  7. package/dist/app/displays/map-groups.js +12 -3
  8. package/dist/app/displays/measured-heights.js +23 -0
  9. package/dist/app/displays/record-card.js +2 -2
  10. package/dist/app/displays/virtual-cards.js +12 -14
  11. package/dist/app/field-editor.js +9 -1
  12. package/dist/app/index.html +2 -2
  13. package/dist/app/list-page.js +2 -2
  14. package/dist/app/map/approximate-position.js +76 -0
  15. package/dist/app/map/area-features.js +80 -0
  16. package/dist/app/map/maplibre.js +11 -0
  17. package/dist/app/map/position-areas.js +78 -0
  18. package/dist/app/map/record-map.js +2 -7
  19. package/dist/app/map/record-position.js +5 -4
  20. package/dist/app/map/records-map.js +24 -7
  21. package/dist/app/map/theme-color.js +23 -0
  22. package/dist/app/meta-gate.js +1 -1
  23. package/dist/app/nav-recent.js +1 -1
  24. package/dist/app/pages/blocks/card-frame.js +2 -2
  25. package/dist/app/pages/blocks/grid.js +10 -2
  26. package/dist/app/pages/blocks/link.js +1 -1
  27. package/dist/app/pages/blocks/page-block.js +1 -1
  28. package/dist/app/pages/blocks/stat.js +9 -5
  29. package/dist/app/pages/field-value.js +1 -1
  30. package/dist/app/record-page.js +8 -4
  31. package/dist/app/router.js +7 -8
  32. package/dist/app/saved/saved-records.js +1 -1
  33. package/dist/app/search/search-model.js +1 -1
  34. package/dist/app/searches/use-save-search.js +4 -3
  35. package/dist/app/source-notice.js +2 -2
  36. package/dist/app/use-record-editor.js +5 -3
  37. package/dist/app-shell/app-shell.d.ts +1 -0
  38. package/dist/app-shell/app-shell.js +6 -4
  39. package/dist/app-shell/page-scroll.js +21 -4
  40. package/dist/app-shell/sidebar.d.ts +1 -0
  41. package/dist/app-shell/sidebar.js +4 -4
  42. package/dist/app-shell/top-bar.js +4 -4
  43. package/dist/auth/auth-gate.js +18 -9
  44. package/dist/auth/auth-provider.js +1 -0
  45. package/dist/auth/code-form.js +77 -0
  46. package/dist/auth/email-code-form.js +18 -77
  47. package/dist/auth/forgot-password-form.js +10 -10
  48. package/dist/auth/forgot-password-page.js +2 -2
  49. package/dist/auth/resend-code-button.js +29 -0
  50. package/dist/auth/sign-in-form.js +7 -8
  51. package/dist/auth/sign-in-page.js +3 -3
  52. package/dist/auth/sign-in-prompt.js +13 -9
  53. package/dist/auth/sign-up-code-form.js +22 -0
  54. package/dist/auth/sign-up-details-form.js +81 -111
  55. package/dist/auth/sign-up-page.js +17 -8
  56. package/dist/auth/two-factor-form.js +3 -3
  57. package/dist/auth/use-sign-in-address.js +21 -8
  58. package/dist/index.d.ts +2 -1
  59. package/dist/index.js +2 -1
  60. package/dist/lib/flip-guard.js +43 -0
  61. package/dist/lib/page-scroll-lock.js +20 -0
  62. package/dist/lib/use-focus-trap.js +4 -4
  63. package/dist/organizations/invitation-page.js +7 -6
  64. package/dist/package.js +1 -1
  65. package/dist/primitives/otp-input.d.ts +21 -0
  66. package/dist/primitives/otp-input.js +117 -0
  67. package/dist/primitives/otp-value.js +23 -0
  68. package/dist/primitives/route-link.d.ts +1 -0
  69. package/dist/primitives/route-link.js +20 -0
  70. package/dist/primitives/textarea.js +36 -0
  71. package/dist/record-view/field.js +56 -53
  72. package/dist/shell/assets/app-DJAZP1yB.js +66 -0
  73. package/dist/shell/assets/index-C5pYkUfv.css +2 -0
  74. package/dist/shell/assets/{index-CrtAqGCx.js → index-LBhMBiK4.js} +1 -1
  75. package/dist/shell/assets/maplibre-Dy0ZTnFZ.js +814 -0
  76. package/dist/shell/assets/record-map-BiWSbqqT.js +1 -0
  77. package/dist/shell/assets/records-map-DabYc3ad.js +1 -0
  78. package/dist/shell/assets/shared-protobase-ui-BURelnPG.js +1 -0
  79. package/dist/shell/index.html +6 -6
  80. package/dist/shell/protobase.shell.json +1 -1
  81. package/dist/styles.css +15 -0
  82. package/dist/toasts/toast.js +22 -6
  83. package/package.json +4 -4
  84. package/dist/auth/code-input.js +0 -17
  85. package/dist/auth/sign-in-address.js +0 -47
  86. package/dist/shell/assets/app-CpFoFNtG.js +0 -66
  87. package/dist/shell/assets/index-ibaHR5wQ.css +0 -2
  88. package/dist/shell/assets/maplibre-gl-B_TZ9n3D.js +0 -1
  89. package/dist/shell/assets/record-map-B2mkMEcE.js +0 -814
  90. package/dist/shell/assets/records-map-CAj21Al1.js +0 -1
  91. package/dist/shell/assets/shared-protobase-ui-D942ARkw.js +0 -1
  92. /package/dist/shell/assets/{record-map-DBE96hFo.css → maplibre-DBE96hFo.css} +0 -0
@@ -1,129 +1,99 @@
1
1
  import { Input } from "../primitives/input.js";
2
2
  import { Button } from "../primitives/button.js";
3
- import { CodeInput } from "./code-input.js";
4
3
  import { AuthLayout } from "./auth-layout.js";
5
4
  import { useId, useState } from "react";
6
5
  import { jsx, jsxs } from "react/jsx-runtime";
7
6
  //#region src/auth/sign-up-details-form.tsx
8
- /** The second step of making an account: the emailed code, the person's name and how they will sign in. */
9
- const SignUpDetailsForm = ({ email, onSubmit, onResend, onBack, methods, busy, error, notice }) => {
10
- const [code, setCode] = useState("");
7
+ /** The last step of making an account, once the emailed code was right: the person's name and how they will sign in. */
8
+ const SignUpDetailsForm = ({ email, onSubmit, onBack, methods, busy, error }) => {
11
9
  const [name, setName] = useState("");
12
10
  const [password, setPassword] = useState("");
13
11
  const passwordHint = useId();
14
12
  const withPassword = methods.includes("password");
15
13
  const passwordOptional = methods.includes("emailCode");
16
14
  return /* @__PURE__ */ jsxs(AuthLayout, {
17
- title: "Check your email",
18
- description: "Enter the code we sent to create your account.",
19
- children: [
20
- /* @__PURE__ */ jsxs("p", {
21
- className: "mb-4 text-[13px] text-muted-foreground",
22
- children: [
23
- "A 6-digit code is on its way to ",
24
- /* @__PURE__ */ jsx("span", {
25
- className: "font-medium text-foreground",
26
- children: email
27
- }),
28
- ". It works for 5 minutes, once. If the address has an account already, the email says so instead."
29
- ]
30
- }),
31
- notice && /* @__PURE__ */ jsx("p", {
32
- role: "status",
33
- className: "mb-4 rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
34
- children: notice
35
- }),
36
- /* @__PURE__ */ jsxs("form", {
37
- className: "flex flex-col gap-4",
38
- onSubmit: (event) => {
39
- event.preventDefault();
40
- onSubmit({
41
- code: code.trim(),
42
- name: name.trim(),
43
- ...withPassword && password && { password }
44
- });
45
- },
46
- children: [
47
- /* @__PURE__ */ jsxs("label", {
15
+ title: "Your details",
16
+ description: "One last step to create your account.",
17
+ children: [/* @__PURE__ */ jsxs("p", {
18
+ className: "mb-4 text-[13px] text-muted-foreground",
19
+ children: [
20
+ "The code was right: the account is for ",
21
+ /* @__PURE__ */ jsx("span", {
22
+ className: "font-medium text-foreground",
23
+ children: email
24
+ }),
25
+ "."
26
+ ]
27
+ }), /* @__PURE__ */ jsxs("form", {
28
+ className: "flex flex-col gap-4",
29
+ onSubmit: (event) => {
30
+ event.preventDefault();
31
+ onSubmit({
32
+ name: name.trim(),
33
+ ...withPassword && password && { password }
34
+ });
35
+ },
36
+ children: [
37
+ /* @__PURE__ */ jsxs("label", {
38
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
39
+ children: ["Your name", /* @__PURE__ */ jsx(Input, {
40
+ name: "name",
41
+ autoComplete: "name",
42
+ required: true,
43
+ autoFocus: true,
44
+ maxLength: 200,
45
+ value: name,
46
+ onChange: (event) => setName(event.target.value),
47
+ className: "min-h-6"
48
+ })]
49
+ }),
50
+ withPassword && /* @__PURE__ */ jsxs("div", {
51
+ className: "flex flex-col gap-1.5",
52
+ children: [/* @__PURE__ */ jsxs("label", {
48
53
  className: "flex flex-col gap-1.5 text-[13px] font-medium",
49
- children: ["Code", /* @__PURE__ */ jsx(CodeInput, {
50
- name: "code",
51
- required: true,
52
- autoFocus: true,
53
- value: code,
54
- invalid: Boolean(error),
55
- onChange: (event) => setCode(event.target.value)
56
- })]
57
- }),
58
- /* @__PURE__ */ jsxs("label", {
59
- className: "flex flex-col gap-1.5 text-[13px] font-medium",
60
- children: ["Your name", /* @__PURE__ */ jsx(Input, {
61
- name: "name",
62
- autoComplete: "name",
63
- required: true,
64
- maxLength: 200,
65
- value: name,
66
- onChange: (event) => setName(event.target.value),
54
+ children: [passwordOptional ? "Password (optional)" : "Password", /* @__PURE__ */ jsx(Input, {
55
+ type: "password",
56
+ name: "password",
57
+ autoComplete: "new-password",
58
+ "aria-describedby": passwordHint,
59
+ required: !passwordOptional,
60
+ minLength: 12,
61
+ maxLength: 128,
62
+ value: password,
63
+ onChange: (event) => setPassword(event.target.value),
67
64
  className: "min-h-6"
68
65
  })]
69
- }),
70
- withPassword && /* @__PURE__ */ jsxs("div", {
71
- className: "flex flex-col gap-1.5",
72
- children: [/* @__PURE__ */ jsxs("label", {
73
- className: "flex flex-col gap-1.5 text-[13px] font-medium",
74
- children: [passwordOptional ? "Password (optional)" : "Password", /* @__PURE__ */ jsx(Input, {
75
- type: "password",
76
- name: "password",
77
- autoComplete: "new-password",
78
- "aria-describedby": passwordHint,
79
- required: !passwordOptional,
80
- minLength: 12,
81
- maxLength: 128,
82
- value: password,
83
- onChange: (event) => setPassword(event.target.value),
84
- className: "min-h-6"
85
- })]
86
- }), /* @__PURE__ */ jsxs("p", {
87
- id: passwordHint,
88
- className: "text-xs text-muted-foreground",
89
- children: [
90
- "At least ",
91
- 12,
92
- " characters.",
93
- passwordOptional && " Without one, you sign in with a code we email you."
94
- ]
95
- })]
96
- }),
97
- error && /* @__PURE__ */ jsx("p", {
98
- role: "alert",
99
- className: "text-xs font-medium text-danger-text",
100
- children: error
101
- }),
102
- /* @__PURE__ */ jsx(Button, {
103
- variant: "primary",
104
- type: "submit",
105
- loading: busy,
106
- className: "min-h-10",
107
- children: "Create account"
108
- }),
109
- /* @__PURE__ */ jsxs("div", {
110
- className: "flex justify-between gap-2",
111
- children: [/* @__PURE__ */ jsx(Button, {
112
- variant: "ghost",
113
- onClick: onBack,
114
- className: "min-h-10",
115
- children: "Use another address"
116
- }), /* @__PURE__ */ jsx(Button, {
117
- variant: "ghost",
118
- onClick: onResend,
119
- disabled: busy,
120
- className: "min-h-10",
121
- children: "Send a new code"
122
- })]
123
- })
124
- ]
125
- })
126
- ]
66
+ }), /* @__PURE__ */ jsxs("p", {
67
+ id: passwordHint,
68
+ className: "text-xs text-muted-foreground",
69
+ children: [
70
+ "At least ",
71
+ 12,
72
+ " characters.",
73
+ passwordOptional && " Without one, you sign in with a code we email you."
74
+ ]
75
+ })]
76
+ }),
77
+ error && /* @__PURE__ */ jsx("p", {
78
+ role: "alert",
79
+ className: "text-xs font-medium text-danger-text",
80
+ children: error
81
+ }),
82
+ /* @__PURE__ */ jsx(Button, {
83
+ variant: "primary",
84
+ type: "submit",
85
+ loading: busy,
86
+ className: "min-h-10",
87
+ children: "Create account"
88
+ }),
89
+ /* @__PURE__ */ jsx(Button, {
90
+ variant: "ghost",
91
+ onClick: onBack,
92
+ className: "min-h-10",
93
+ children: "Use another address"
94
+ })
95
+ ]
96
+ })]
127
97
  });
128
98
  };
129
99
  //#endregion
@@ -1,5 +1,6 @@
1
1
  import { authMessage } from "./auth-messages.js";
2
2
  import { useAuth } from "./auth-provider.js";
3
+ import { SignUpCodeForm } from "./sign-up-code-form.js";
3
4
  import { SignUpDetailsForm } from "./sign-up-details-form.js";
4
5
  import { SignUpEmailForm } from "./sign-up-email-form.js";
5
6
  import { useState } from "react";
@@ -7,11 +8,12 @@ import { AuthError } from "@protobase/client";
7
8
  import { jsx } from "react/jsx-runtime";
8
9
  //#region src/auth/sign-up-page.tsx
9
10
  /**
10
- * Makes an account on a server with open sign-up: the address, then the code emailed to it with the person's name and
11
- * password. The new account is signed in, and sets up what the sign-in policy requires as after any sign-in.
11
+ * Makes an account on a server with open sign-up: the address, then the code emailed to it on its own, then the
12
+ * person's name and password. The new account is signed in, and sets up what the sign-in policy requires as after any
13
+ * sign-in.
12
14
  */
13
- const SignUpPage = ({ workspace, email, onCodeSent, onBack, onSignIn, onBrowse }) => {
14
- const { signInMethods, sendSignUpCode, signUp } = useAuth();
15
+ const SignUpPage = ({ workspace, email, ticket, onCodeSent, onVerified, onBack, onSignIn, onBrowse }) => {
16
+ const { signInMethods, sendSignUpCode, verifySignUpCode, signUp } = useAuth();
15
17
  const [busy, setBusy] = useState(false);
16
18
  const [error, setError] = useState();
17
19
  const [notice, setNotice] = useState();
@@ -38,19 +40,26 @@ const SignUpPage = ({ workspace, email, onCodeSent, onBack, onSignIn, onBrowse }
38
40
  onCodeSent(address);
39
41
  })
40
42
  });
41
- return /* @__PURE__ */ jsx(SignUpDetailsForm, {
43
+ if (ticket === void 0) return /* @__PURE__ */ jsx(SignUpCodeForm, {
42
44
  email,
43
- methods: signInMethods,
44
45
  busy,
45
46
  error,
46
47
  notice,
47
48
  onBack,
49
+ onSubmit: (code) => void attempt(async () => onVerified(await verifySignUpCode(email, code))),
48
50
  onResend: () => void attempt(async () => {
49
51
  await sendSignUpCode(email);
50
52
  setNotice("A new code is on its way. Only the newest one works.");
51
- }),
53
+ })
54
+ });
55
+ return /* @__PURE__ */ jsx(SignUpDetailsForm, {
56
+ email,
57
+ methods: signInMethods,
58
+ busy,
59
+ error,
60
+ onBack,
52
61
  onSubmit: (details) => void attempt(() => signUp({
53
- email,
62
+ ticket,
54
63
  ...details
55
64
  }))
56
65
  });
@@ -1,6 +1,6 @@
1
1
  import { Input } from "../primitives/input.js";
2
2
  import { Button } from "../primitives/button.js";
3
- import { CodeInput } from "./code-input.js";
3
+ import { OtpInput } from "../primitives/otp-input.js";
4
4
  import { AuthLayout } from "./auth-layout.js";
5
5
  import { Checkbox } from "../primitives/checkbox.js";
6
6
  import { useState } from "react";
@@ -72,13 +72,13 @@ const TwoFactorForm = ({ methods, onVerify, onSendCode, onBack, busy, error, cod
72
72
  invalid: Boolean(error),
73
73
  onChange: (event) => setCode(event.target.value),
74
74
  className: "min-h-6 font-mono"
75
- }) : /* @__PURE__ */ jsx(CodeInput, {
75
+ }) : /* @__PURE__ */ jsx(OtpInput, {
76
76
  name: "code",
77
77
  required: true,
78
78
  autoFocus: true,
79
79
  value: code,
80
80
  invalid: Boolean(error),
81
- onChange: (event) => setCode(event.target.value)
81
+ onChange: setCode
82
82
  })]
83
83
  }),
84
84
  /* @__PURE__ */ jsxs("label", {
@@ -1,25 +1,38 @@
1
1
  import { withoutSignInLink } from "./provider-link.js";
2
2
  import { useRouter } from "../app/router.js";
3
- import { readSignInAddress, signInPath } from "./sign-in-address.js";
4
3
  import { useCallback, useMemo } from "react";
4
+ import { readSignInAddress, signInPath } from "@protobase/schema";
5
5
  //#region src/auth/use-sign-in-address.ts
6
6
  /**
7
7
  * The sign-in route as logical values, and the ways to move along it: `address` is the step it shows, or `undefined` on
8
- * any other page; `next` is the page to come back to; `open` shows a step, and `leave` goes back to `next` in place of
9
- * the route in the tab's history. Components read and write the sign-in route through this hook, never its query.
8
+ * any other page; `next` is the page to come back to; `open` shows a step, `link` is a step for a `RouteLink`, which a
9
+ * click opens, and `leave` goes back to `next` in place of the route in the tab's history. Components read and write
10
+ * the sign-in route through this hook, never its query.
10
11
  */
11
12
  const useSignInAddress = () => {
12
- const { path, params, navigate } = useRouter();
13
+ const { path, params, basePath, navigate } = useRouter();
13
14
  const address = useMemo(() => readSignInAddress(path, params), [path, params]);
14
15
  const here = withoutSignInLink(new URL(params.size > 0 ? `${path}?${params}` : path, "http://local").href);
15
16
  const next = address ? address.next : here;
17
+ const open = useCallback((step = "sign-in", options) => navigate(signInPath({
18
+ step,
19
+ next
20
+ }), options), [navigate, next]);
16
21
  return {
17
22
  address,
18
23
  next,
19
- open: useCallback((step = "sign-in", options) => navigate(signInPath({
20
- step,
21
- next
22
- }), options), [navigate, next]),
24
+ open,
25
+ link: useCallback((step = "sign-in", options) => ({
26
+ href: `${basePath}${signInPath({
27
+ step,
28
+ next
29
+ })}`,
30
+ onFollow: () => open(step, options)
31
+ }), [
32
+ basePath,
33
+ next,
34
+ open
35
+ ]),
23
36
  leave: useCallback(() => navigate(next ?? "/", { replace: true }), [navigate, next])
24
37
  };
25
38
  };
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ import { RecordLink } from "./app/displays/record-link.js";
22
22
  import { useApiToken } from "./auth/use-api-token.js";
23
23
  import { SignInPrompt, SignInPromptProps } from "./auth/sign-in-prompt.js";
24
24
  import { ResponsiveDialog, ResponsiveDialogProps } from "./primitives/responsive-dialog.js";
25
+ import { OtpInput, OtpInputProps } from "./primitives/otp-input.js";
25
26
  import { AssistantDock, AssistantDockProps } from "./assistant-dock/assistant-dock.js";
26
27
  import { PartView } from "./assistant-dock/part-view.js";
27
28
  import { useAssistant } from "./assistant-dock/use-assistant.js";
@@ -41,4 +42,4 @@ import { Step, StepList, StepState } from "./step-list/step-list.js";
41
42
  import { DiffView, DiffViewProps } from "./diff-view/diff-view.js";
42
43
  import { DiffLine, diffStats, parseDiff } from "./diff-view/diff.js";
43
44
  import { CompactTable, CompactTableProps } from "./compact-table/compact-table.js";
44
- export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, ResponsiveDialog, type ResponsiveDialogProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
45
+ export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, OtpInput, type OtpInputProps, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, ResponsiveDialog, type ResponsiveDialogProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type Step, StepList, type StepState, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { OtpInput } from "./primitives/otp-input.js";
1
2
  import { ApiProvider, useClient, useResourceClient } from "./data/api-provider.js";
2
3
  import { useApiToken } from "./auth/use-api-token.js";
3
4
  import { useRecord } from "./data/use-record.js";
@@ -32,4 +33,4 @@ import { useList } from "./data/use-list.js";
32
33
  import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
33
34
  import { App } from "./app/app.js";
34
35
  import { RecordLink } from "./app/displays/record-link.js";
35
- export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, PartView, RecordLink, ResponsiveDialog, SidePanel, SignInPrompt, StepList, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
36
+ export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, OtpInput, PartView, RecordLink, ResponsiveDialog, SidePanel, SignInPrompt, StepList, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
@@ -0,0 +1,43 @@
1
+ //#region src/lib/flip-guard.ts
2
+ const warnInDevelopment = (held, values) => {
3
+ if (!import.meta.env?.DEV) return;
4
+ console.warn(`A layout kept changing between ${values.join(", ")} and is held at ${held}, so it does not flash. Something it draws changes what it measures.`);
5
+ };
6
+ /**
7
+ * Settles a measure that a layout follows, such as a list's width, when the layout feeds back into it and it keeps
8
+ * changing between the same few values, as a width does when the layout it gives brings a scrollbar that narrows it
9
+ * again. Each call takes the measured `value` at time `at` and returns the value to lay out with: the measured one,
10
+ * until it comes back to a value it had within `windowMs` `comebacks` times. It then holds the smallest of the values
11
+ * it last changed between, which fits whichever of them the measure settles at, until the measure takes another value,
12
+ * as on a resize of the window. A layout that keeps to the held value stops feeding the loop, so it cannot flash.
13
+ */
14
+ const createFlipGuard = ({ windowMs = 1e3, comebacks = 2, onHold = warnInDevelopment } = {}) => {
15
+ let current;
16
+ let changes = [];
17
+ let held;
18
+ return (value, at) => {
19
+ if (held?.among.has(value)) return held.value;
20
+ if (held) {
21
+ held = void 0;
22
+ changes = [];
23
+ }
24
+ if (value === current) return value;
25
+ current = value;
26
+ changes = changes.filter((change) => at - change.at <= windowMs);
27
+ changes.push({
28
+ value,
29
+ at,
30
+ comeback: changes.some((change) => change.value === value)
31
+ });
32
+ if (changes.filter((change) => change.comeback).length < comebacks) return value;
33
+ const among = new Set(changes.slice(-2 * comebacks).map((change) => change.value));
34
+ held = {
35
+ value: Math.min(...among),
36
+ among
37
+ };
38
+ onHold(held.value, [...among]);
39
+ return held.value;
40
+ };
41
+ };
42
+ //#endregion
43
+ export { createFlipGuard };
@@ -0,0 +1,20 @@
1
+ //#region src/lib/page-scroll-lock.ts
2
+ const attribute = "data-pb-scroll-locked";
3
+ /**
4
+ * Stops the page scrolling behind a modal until the returned function is called. Where the page shows a scrollbar that
5
+ * takes room, the page keeps that room meanwhile (`styles.css` reserves it for this attribute), so hiding the scrollbar
6
+ * does not widen the page and move everything behind the modal sideways.
7
+ */
8
+ const lockPageScroll = () => {
9
+ const { body, documentElement: root } = document;
10
+ const overflow = body.style.overflow;
11
+ const locked = root.hasAttribute(attribute);
12
+ if (window.innerWidth > root.clientWidth) root.setAttribute(attribute, "");
13
+ body.style.overflow = "hidden";
14
+ return () => {
15
+ body.style.overflow = overflow;
16
+ if (!locked) root.removeAttribute(attribute);
17
+ };
18
+ };
19
+ //#endregion
20
+ export { lockPageScroll };
@@ -1,8 +1,9 @@
1
+ import { lockPageScroll } from "./page-scroll-lock.js";
1
2
  import { useEffect, useRef } from "react";
2
3
  //#region src/lib/use-focus-trap.ts
3
4
  const focusable = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
4
5
  /**
5
- * While `active`: moves focus into `container`, keeps Tab inside it, locks body scrolling,
6
+ * While `active`: moves focus into `container`, keeps Tab inside it, locks page scrolling (see `lockPageScroll`),
6
7
  * calls `onEscape` on Escape, and returns focus to the previously focused element afterwards.
7
8
  */
8
9
  const useFocusTrap = (container, active, onEscape) => {
@@ -12,8 +13,7 @@ const useFocusTrap = (container, active, onEscape) => {
12
13
  if (!active || !container.current) return;
13
14
  const root = container.current;
14
15
  const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
15
- const overflow = document.body.style.overflow;
16
- document.body.style.overflow = "hidden";
16
+ const unlock = lockPageScroll();
17
17
  const items = () => [...root.querySelectorAll(focusable)];
18
18
  (items()[0] ?? root).focus();
19
19
  const onKey = (event) => {
@@ -37,7 +37,7 @@ const useFocusTrap = (container, active, onEscape) => {
37
37
  document.addEventListener("keydown", onKey);
38
38
  return () => {
39
39
  document.removeEventListener("keydown", onKey);
40
- document.body.style.overflow = overflow;
40
+ unlock();
41
41
  previous?.focus();
42
42
  };
43
43
  }, [active, container]);
@@ -1,8 +1,10 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { RouteLink } from "../primitives/route-link.js";
1
3
  import { Input } from "../primitives/input.js";
2
4
  import { Spinner } from "../primitives/spinner.js";
3
5
  import { authMessage } from "../auth/auth-messages.js";
4
6
  import { useAuth } from "../auth/auth-provider.js";
5
- import { Button } from "../primitives/button.js";
7
+ import { Button, buttonClass } from "../primitives/button.js";
6
8
  import { AuthLayout } from "../auth/auth-layout.js";
7
9
  import { organizationRoleLabels, roleLabel } from "./organization-paths.js";
8
10
  import { useAction } from "./use-action.js";
@@ -74,7 +76,7 @@ const SignUp = ({ token, preview, onDone }) => {
74
76
  * Where an emailed invitation link leads: what it is for, then accepting it signed in as the invited address, signing
75
77
  * in first, or making an account for an address without one.
76
78
  */
77
- const InvitationPage = ({ token, onDone, onSignIn }) => {
79
+ const InvitationPage = ({ token, onDone, signIn }) => {
78
80
  const { session, state, signOut, organizations } = useAuth();
79
81
  const [preview, setPreview] = useState();
80
82
  const [invalid, setInvalid] = useState();
@@ -138,10 +140,9 @@ const InvitationPage = ({ token, onDone, onSignIn }) => {
138
140
  }),
139
141
  children: ["Join ", preview.organization.name]
140
142
  })]
141
- }) : preview.hasAccount ? /* @__PURE__ */ jsxs(Button, {
142
- variant: "primary",
143
- className: "w-full",
144
- onClick: onSignIn,
143
+ }) : preview.hasAccount ? /* @__PURE__ */ jsxs(RouteLink, {
144
+ to: signIn,
145
+ className: cn(buttonClass({ variant: "primary" }), "w-full"),
145
146
  children: [
146
147
  "Sign in as ",
147
148
  preview.email,
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.15.4";
2
+ var version = "0.16.0";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -0,0 +1,21 @@
1
+ import { InputHTMLAttributes, Ref } from "react";
2
+ //#region src/primitives/otp-input.d.ts
3
+ export type OtpInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'defaultValue' | 'onChange' | 'type' | 'inputMode' | 'autoComplete' | 'maxLength' | 'pattern' | 'size'> & {
4
+ /** The digits entered so far. */
5
+ value: string;
6
+ onChange: (code: string) => void;
7
+ /** Called with the code once its last digit is in, typed, pasted or filled in by the system, to submit it at once. */
8
+ onComplete?: (code: string) => void;
9
+ /** How many digits the code has; 6 by default. */
10
+ length?: number;
11
+ invalid?: boolean;
12
+ ref?: Ref<HTMLInputElement>;
13
+ };
14
+ /**
15
+ * A one-time code field drawn as a box per digit: one real input, so the system's code autofill (`one-time-code`, from
16
+ * a mail or a text message), pasting a whole code into any box, the phone's digit keyboard and a form's `required`
17
+ * work as on any field. Arrow keys, Home and End move between boxes, and a digit typed on a filled box replaces it.
18
+ * Label it as any input, with a `<label>` around it or `aria-label`.
19
+ */
20
+ export declare const OtpInput: ({ value, onChange, onComplete, length, invalid, disabled, className, ref, onFocus, onBlur, onSelect, onKeyDown, onClick, onPaste, ...rest }: OtpInputProps) => import("react").JSX.Element;
21
+ //#endregion
@@ -0,0 +1,117 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { editedCode, pastedCode } from "./otp-value.js";
3
+ import { useRef, useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/primitives/otp-input.tsx
6
+ /**
7
+ * A one-time code field drawn as a box per digit: one real input, so the system's code autofill (`one-time-code`, from
8
+ * a mail or a text message), pasting a whole code into any box, the phone's digit keyboard and a form's `required`
9
+ * work as on any field. Arrow keys, Home and End move between boxes, and a digit typed on a filled box replaces it.
10
+ * Label it as any input, with a `<label>` around it or `aria-label`.
11
+ */
12
+ const OtpInput = ({ value, onChange, onComplete, length = 6, invalid, disabled, className, ref, onFocus, onBlur, onSelect, onKeyDown, onClick, onPaste, ...rest }) => {
13
+ const input = useRef(null);
14
+ const [focused, setFocused] = useState(false);
15
+ const [caret, setCaret] = useState(value.length);
16
+ const code = value.slice(0, length);
17
+ const active = Math.min(caret, length - 1);
18
+ const attach = (node) => {
19
+ input.current = node;
20
+ if (typeof ref === "function") ref(node);
21
+ else if (ref) ref.current = node;
22
+ };
23
+ const commit = (next) => {
24
+ if (next === code) return;
25
+ onChange(next);
26
+ if (next.length === length) onComplete?.(next);
27
+ };
28
+ const place = (at) => {
29
+ const field = input.current;
30
+ if (!field) return;
31
+ const to = Math.max(0, Math.min(at, code.length));
32
+ field.setSelectionRange(to, to < code.length ? to + 1 : to);
33
+ setCaret(to);
34
+ };
35
+ const moveWithKeys = (event) => {
36
+ onKeyDown?.(event);
37
+ if (event.defaultPrevented || event.shiftKey || event.altKey || event.metaKey || event.ctrlKey) return;
38
+ const at = event.currentTarget.selectionStart ?? code.length;
39
+ const target = {
40
+ ArrowLeft: at - 1,
41
+ ArrowRight: at + 1,
42
+ Home: 0,
43
+ End: code.length
44
+ }[event.key];
45
+ if (target === void 0) return;
46
+ event.preventDefault();
47
+ place(target);
48
+ };
49
+ const placeAtPointer = (event) => {
50
+ onClick?.(event);
51
+ const box = event.currentTarget.getBoundingClientRect();
52
+ if (event.clientX <= box.left || event.clientX >= box.right) return;
53
+ place(Math.floor((event.clientX - box.left) / box.width * length));
54
+ };
55
+ return /* @__PURE__ */ jsxs("div", {
56
+ className: cn("relative", disabled && "opacity-50", className),
57
+ children: [/* @__PURE__ */ jsx("div", {
58
+ "aria-hidden": true,
59
+ className: "flex justify-center gap-2",
60
+ children: Array.from({ length }, (_, index) => {
61
+ const current = focused && index === active;
62
+ return /* @__PURE__ */ jsx("div", {
63
+ "data-slot": index,
64
+ "data-digit": code[index],
65
+ className: cn("flex h-12 min-w-0 max-w-12 flex-1 items-center justify-center rounded-md border bg-background font-mono text-xl font-semibold text-foreground shadow-sm transition-colors before:content-[attr(data-digit)]", invalid ? "border-danger" : current ? "border-primary" : "border-border-strong", current && (invalid ? "ring-2 ring-danger/20" : "ring-2 ring-primary/20")),
66
+ children: code[index] === void 0 && (current ? /* @__PURE__ */ jsx("span", { className: "h-6 w-px animate-pulse bg-foreground" }) : /* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-border-strong" }))
67
+ }, index);
68
+ })
69
+ }), /* @__PURE__ */ jsx("input", {
70
+ ref: attach,
71
+ type: "text",
72
+ inputMode: "numeric",
73
+ autoComplete: "one-time-code",
74
+ pattern: `[0-9]{${length}}`,
75
+ spellCheck: false,
76
+ autoCorrect: "off",
77
+ autoCapitalize: "off",
78
+ "aria-invalid": invalid || void 0,
79
+ disabled,
80
+ value: code,
81
+ className: "absolute inset-0 size-full cursor-text bg-transparent text-transparent caret-transparent outline-none selection:bg-transparent disabled:cursor-not-allowed",
82
+ ...rest,
83
+ onChange: (event) => {
84
+ const { data, dataTransfer } = event.nativeEvent;
85
+ commit(editedCode(event.target.value, data ?? dataTransfer?.getData("text/plain"), length));
86
+ },
87
+ onPaste: (event) => {
88
+ onPaste?.(event);
89
+ if (event.defaultPrevented) return;
90
+ event.preventDefault();
91
+ const field = event.currentTarget;
92
+ const next = pastedCode(code, event.clipboardData.getData("text"), field.selectionStart ?? code.length, length);
93
+ commit(next);
94
+ setCaret(next.length);
95
+ },
96
+ onSelect: (event) => {
97
+ onSelect?.(event);
98
+ const field = event.currentTarget;
99
+ const start = field.selectionStart ?? code.length;
100
+ if (start === field.selectionEnd && start < field.value.length) field.setSelectionRange(start, start + 1);
101
+ setCaret(start);
102
+ },
103
+ onKeyDown: moveWithKeys,
104
+ onClick: placeAtPointer,
105
+ onFocus: (event) => {
106
+ setFocused(true);
107
+ onFocus?.(event);
108
+ },
109
+ onBlur: (event) => {
110
+ setFocused(false);
111
+ onBlur?.(event);
112
+ }
113
+ })]
114
+ });
115
+ };
116
+ //#endregion
117
+ export { OtpInput };