@shipfox/client-auth 0.2.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 (225) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +7 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +111 -0
  8. package/LICENSE +21 -0
  9. package/dist/components/auth-guard.d.ts +5 -0
  10. package/dist/components/auth-guard.d.ts.map +1 -0
  11. package/dist/components/auth-guard.js +61 -0
  12. package/dist/components/auth-guard.js.map +1 -0
  13. package/dist/components/auth-provider.d.ts +11 -0
  14. package/dist/components/auth-provider.d.ts.map +1 -0
  15. package/dist/components/auth-provider.js +21 -0
  16. package/dist/components/auth-provider.js.map +1 -0
  17. package/dist/components/auth-shell.d.ts +11 -0
  18. package/dist/components/auth-shell.d.ts.map +1 -0
  19. package/dist/components/auth-shell.js +63 -0
  20. package/dist/components/auth-shell.js.map +1 -0
  21. package/dist/components/redirect-target.d.ts +2 -0
  22. package/dist/components/redirect-target.d.ts.map +1 -0
  23. package/dist/components/redirect-target.js +22 -0
  24. package/dist/components/redirect-target.js.map +1 -0
  25. package/dist/components/workspace-crumb.d.ts +3 -0
  26. package/dist/components/workspace-crumb.d.ts.map +1 -0
  27. package/dist/components/workspace-crumb.js +3 -0
  28. package/dist/components/workspace-crumb.js.map +1 -0
  29. package/dist/components/workspace-switcher.d.ts +6 -0
  30. package/dist/components/workspace-switcher.d.ts.map +1 -0
  31. package/dist/components/workspace-switcher.js +91 -0
  32. package/dist/components/workspace-switcher.js.map +1 -0
  33. package/dist/components/workspace-switcher.stories.d.ts +26 -0
  34. package/dist/components/workspace-switcher.stories.d.ts.map +1 -0
  35. package/dist/components/workspace-switcher.stories.js +168 -0
  36. package/dist/components/workspace-switcher.stories.js.map +1 -0
  37. package/dist/feature.d.ts +33 -0
  38. package/dist/feature.d.ts.map +1 -0
  39. package/dist/feature.js +43 -0
  40. package/dist/feature.js.map +1 -0
  41. package/dist/hooks/api/login-auth.d.ts +29 -0
  42. package/dist/hooks/api/login-auth.d.ts.map +1 -0
  43. package/dist/hooks/api/login-auth.js +42 -0
  44. package/dist/hooks/api/login-auth.js.map +1 -0
  45. package/dist/hooks/api/logout-auth.d.ts +2 -0
  46. package/dist/hooks/api/logout-auth.d.ts.map +1 -0
  47. package/dist/hooks/api/logout-auth.js +32 -0
  48. package/dist/hooks/api/logout-auth.js.map +1 -0
  49. package/dist/hooks/api/password-reset-auth.d.ts +33 -0
  50. package/dist/hooks/api/password-reset-auth.d.ts.map +1 -0
  51. package/dist/hooks/api/password-reset-auth.js +48 -0
  52. package/dist/hooks/api/password-reset-auth.js.map +1 -0
  53. package/dist/hooks/api/refresh-auth.d.ts +2 -0
  54. package/dist/hooks/api/refresh-auth.d.ts.map +1 -0
  55. package/dist/hooks/api/refresh-auth.js +3 -0
  56. package/dist/hooks/api/refresh-auth.js.map +1 -0
  57. package/dist/hooks/api/signup-auth.d.ts +27 -0
  58. package/dist/hooks/api/signup-auth.d.ts.map +1 -0
  59. package/dist/hooks/api/signup-auth.js +15 -0
  60. package/dist/hooks/api/signup-auth.js.map +1 -0
  61. package/dist/hooks/api/verify-email-auth.d.ts +20 -0
  62. package/dist/hooks/api/verify-email-auth.d.ts.map +1 -0
  63. package/dist/hooks/api/verify-email-auth.js +48 -0
  64. package/dist/hooks/api/verify-email-auth.js.map +1 -0
  65. package/dist/hooks/api/workspace-auth.d.ts +22 -0
  66. package/dist/hooks/api/workspace-auth.d.ts.map +1 -0
  67. package/dist/hooks/api/workspace-auth.js +25 -0
  68. package/dist/hooks/api/workspace-auth.js.map +1 -0
  69. package/dist/hooks/index.d.ts +11 -0
  70. package/dist/hooks/index.d.ts.map +1 -0
  71. package/dist/hooks/index.js +11 -0
  72. package/dist/hooks/index.js.map +1 -0
  73. package/dist/hooks/use-active-workspace.d.ts +2 -0
  74. package/dist/hooks/use-active-workspace.d.ts.map +1 -0
  75. package/dist/hooks/use-active-workspace.js +3 -0
  76. package/dist/hooks/use-active-workspace.js.map +1 -0
  77. package/dist/hooks/use-auth-state.d.ts +2 -0
  78. package/dist/hooks/use-auth-state.d.ts.map +1 -0
  79. package/dist/hooks/use-auth-state.js +3 -0
  80. package/dist/hooks/use-auth-state.js.map +1 -0
  81. package/dist/index.d.ts +15 -0
  82. package/dist/index.d.ts.map +1 -0
  83. package/dist/index.js +16 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/pages/email-verification-resend-model.d.ts +7 -0
  86. package/dist/pages/email-verification-resend-model.d.ts.map +1 -0
  87. package/dist/pages/email-verification-resend-model.js +15 -0
  88. package/dist/pages/email-verification-resend-model.js.map +1 -0
  89. package/dist/pages/form-errors.d.ts +19 -0
  90. package/dist/pages/form-errors.d.ts.map +1 -0
  91. package/dist/pages/form-errors.js +51 -0
  92. package/dist/pages/form-errors.js.map +1 -0
  93. package/dist/pages/form-utils.d.ts +2 -0
  94. package/dist/pages/form-utils.d.ts.map +1 -0
  95. package/dist/pages/form-utils.js +28 -0
  96. package/dist/pages/form-utils.js.map +1 -0
  97. package/dist/pages/invitation-context.d.ts +31 -0
  98. package/dist/pages/invitation-context.d.ts.map +1 -0
  99. package/dist/pages/invitation-context.js +50 -0
  100. package/dist/pages/invitation-context.js.map +1 -0
  101. package/dist/pages/login-page.d.ts +2 -0
  102. package/dist/pages/login-page.d.ts.map +1 -0
  103. package/dist/pages/login-page.js +214 -0
  104. package/dist/pages/login-page.js.map +1 -0
  105. package/dist/pages/logout-page.d.ts +2 -0
  106. package/dist/pages/logout-page.d.ts.map +1 -0
  107. package/dist/pages/logout-page.js +45 -0
  108. package/dist/pages/logout-page.js.map +1 -0
  109. package/dist/pages/password-reset-page.d.ts +2 -0
  110. package/dist/pages/password-reset-page.d.ts.map +1 -0
  111. package/dist/pages/password-reset-page.js +265 -0
  112. package/dist/pages/password-reset-page.js.map +1 -0
  113. package/dist/pages/signup-page.d.ts +2 -0
  114. package/dist/pages/signup-page.d.ts.map +1 -0
  115. package/dist/pages/signup-page.js +384 -0
  116. package/dist/pages/signup-page.js.map +1 -0
  117. package/dist/pages/verify-email-page.d.ts +2 -0
  118. package/dist/pages/verify-email-page.d.ts.map +1 -0
  119. package/dist/pages/verify-email-page.js +57 -0
  120. package/dist/pages/verify-email-page.js.map +1 -0
  121. package/dist/pages/workspace-onboarding-page.d.ts +2 -0
  122. package/dist/pages/workspace-onboarding-page.d.ts.map +1 -0
  123. package/dist/pages/workspace-onboarding-page.js +294 -0
  124. package/dist/pages/workspace-onboarding-page.js.map +1 -0
  125. package/dist/routes/index.d.ts +10 -0
  126. package/dist/routes/index.d.ts.map +1 -0
  127. package/dist/routes/index.js +29 -0
  128. package/dist/routes/index.js.map +1 -0
  129. package/dist/routes/login.d.ts +5 -0
  130. package/dist/routes/login.d.ts.map +1 -0
  131. package/dist/routes/login.js +11 -0
  132. package/dist/routes/login.js.map +1 -0
  133. package/dist/routes/logout.d.ts +6 -0
  134. package/dist/routes/logout.d.ts.map +1 -0
  135. package/dist/routes/logout.js +7 -0
  136. package/dist/routes/logout.js.map +1 -0
  137. package/dist/routes/reset.d.ts +5 -0
  138. package/dist/routes/reset.d.ts.map +1 -0
  139. package/dist/routes/reset.js +11 -0
  140. package/dist/routes/reset.js.map +1 -0
  141. package/dist/routes/signup.d.ts +5 -0
  142. package/dist/routes/signup.d.ts.map +1 -0
  143. package/dist/routes/signup.js +11 -0
  144. package/dist/routes/signup.js.map +1 -0
  145. package/dist/routes/verify-email.d.ts +5 -0
  146. package/dist/routes/verify-email.d.ts.map +1 -0
  147. package/dist/routes/verify-email.js +11 -0
  148. package/dist/routes/verify-email.js.map +1 -0
  149. package/dist/routes/workspace-onboarding.d.ts +13 -0
  150. package/dist/routes/workspace-onboarding.d.ts.map +1 -0
  151. package/dist/routes/workspace-onboarding.js +18 -0
  152. package/dist/routes/workspace-onboarding.js.map +1 -0
  153. package/dist/state/auth.d.ts +10 -0
  154. package/dist/state/auth.d.ts.map +1 -0
  155. package/dist/state/auth.js +9 -0
  156. package/dist/state/auth.js.map +1 -0
  157. package/dist/state/last-workspace.d.ts +2 -0
  158. package/dist/state/last-workspace.d.ts.map +1 -0
  159. package/dist/state/last-workspace.js +3 -0
  160. package/dist/state/last-workspace.js.map +1 -0
  161. package/dist/tsconfig.test.tsbuildinfo +1 -0
  162. package/package.json +116 -0
  163. package/src/components/auth-guard.tsx +58 -0
  164. package/src/components/auth-provider.test.tsx +164 -0
  165. package/src/components/auth-provider.tsx +24 -0
  166. package/src/components/auth-shell.tsx +53 -0
  167. package/src/components/redirect-target.test.ts +60 -0
  168. package/src/components/redirect-target.ts +21 -0
  169. package/src/components/workspace-crumb.tsx +2 -0
  170. package/src/components/workspace-switcher.stories.tsx +169 -0
  171. package/src/components/workspace-switcher.tsx +76 -0
  172. package/src/feature.ts +18 -0
  173. package/src/hooks/api/login-auth.test.ts +45 -0
  174. package/src/hooks/api/login-auth.ts +41 -0
  175. package/src/hooks/api/logout-auth.ts +26 -0
  176. package/src/hooks/api/password-reset-auth.test.ts +68 -0
  177. package/src/hooks/api/password-reset-auth.ts +51 -0
  178. package/src/hooks/api/refresh-auth.test.ts +37 -0
  179. package/src/hooks/api/refresh-auth.ts +5 -0
  180. package/src/hooks/api/signup-auth.ts +11 -0
  181. package/src/hooks/api/verify-email-auth.ts +55 -0
  182. package/src/hooks/api/workspace-auth.test.ts +40 -0
  183. package/src/hooks/api/workspace-auth.ts +25 -0
  184. package/src/hooks/index.ts +13 -0
  185. package/src/hooks/use-active-workspace.ts +1 -0
  186. package/src/hooks/use-auth-state.ts +1 -0
  187. package/src/index.ts +18 -0
  188. package/src/pages/email-verification-resend-model.test.ts +52 -0
  189. package/src/pages/email-verification-resend-model.ts +21 -0
  190. package/src/pages/form-errors.test.ts +114 -0
  191. package/src/pages/form-errors.ts +45 -0
  192. package/src/pages/form-utils.test.ts +37 -0
  193. package/src/pages/form-utils.ts +29 -0
  194. package/src/pages/invitation-context.ts +57 -0
  195. package/src/pages/login-page.test.tsx +128 -0
  196. package/src/pages/login-page.tsx +184 -0
  197. package/src/pages/logout-page.tsx +33 -0
  198. package/src/pages/password-reset-page.test.tsx +109 -0
  199. package/src/pages/password-reset-page.tsx +222 -0
  200. package/src/pages/signup-page.test.tsx +289 -0
  201. package/src/pages/signup-page.tsx +354 -0
  202. package/src/pages/verify-email-page.test.tsx +82 -0
  203. package/src/pages/verify-email-page.tsx +48 -0
  204. package/src/pages/workspace-onboarding-page.test.tsx +161 -0
  205. package/src/pages/workspace-onboarding-page.tsx +213 -0
  206. package/src/routes/index.tsx +18 -0
  207. package/src/routes/login.tsx +11 -0
  208. package/src/routes/logout.tsx +4 -0
  209. package/src/routes/reset.tsx +11 -0
  210. package/src/routes/signup.tsx +11 -0
  211. package/src/routes/verify-email.tsx +11 -0
  212. package/src/routes/workspace-onboarding.tsx +14 -0
  213. package/src/state/auth.ts +18 -0
  214. package/src/state/last-workspace.test.ts +63 -0
  215. package/src/state/last-workspace.ts +5 -0
  216. package/test/factories/user.ts +17 -0
  217. package/test/pages.tsx +83 -0
  218. package/test/setup.ts +3 -0
  219. package/test/utils.ts +11 -0
  220. package/tsconfig.build.json +10 -0
  221. package/tsconfig.build.tsbuildinfo +1 -0
  222. package/tsconfig.json +3 -0
  223. package/tsconfig.test.json +8 -0
  224. package/vercel.json +8 -0
  225. package/vitest.config.ts +71 -0
@@ -0,0 +1,384 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { signupBodySchema } from '@shipfox/api-auth-dto';
3
+ import { displayNameFieldError } from '@shipfox/client-ui';
4
+ import { Button, ButtonLink } from '@shipfox/react-ui/button';
5
+ import { Callout } from '@shipfox/react-ui/callout';
6
+ import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
7
+ import { Icon } from '@shipfox/react-ui/icon';
8
+ import { toast } from '@shipfox/react-ui/toast';
9
+ import { Text } from '@shipfox/react-ui/typography';
10
+ import { useForm } from '@tanstack/react-form';
11
+ import { Link, useNavigate, useSearch } from '@tanstack/react-router';
12
+ import { useAtom } from 'jotai';
13
+ import { useEffect, useRef, useState } from 'react';
14
+ import { AuthShell } from '#/components/auth-shell.js';
15
+ import { useRefreshAuth } from '#hooks/api/refresh-auth.js';
16
+ import { useSignupAuth } from '#hooks/api/signup-auth.js';
17
+ import { useResendEmailVerificationAuth } from '#hooks/api/verify-email-auth.js';
18
+ import { authFormDraftAtom, initialAuthFormDraft } from '#state/auth.js';
19
+ import { getLocalResendAvailableAt, getResendRemainingSeconds, parseNextResendAvailableAt } from './email-verification-resend-model.js';
20
+ import { signupErrorToFormError } from './form-errors.js';
21
+ import { authErrorMessage } from './form-utils.js';
22
+ import { extractInvitationToken, pendingInvitation, useInvitationContext } from './invitation-context.js';
23
+ export function SignupPage() {
24
+ const signup = useSignupAuth();
25
+ const resendEmailVerification = useResendEmailVerificationAuth();
26
+ const refreshAuth = useRefreshAuth();
27
+ const navigate = useNavigate();
28
+ const search = useSearch({
29
+ strict: false
30
+ });
31
+ const invitationToken = extractInvitationToken(search.redirect);
32
+ const invitationPreview = useInvitationContext(invitationToken);
33
+ const invitationPending = pendingInvitation(invitationPreview.data);
34
+ const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);
35
+ const [submittedEmail, setSubmittedEmail] = useState();
36
+ const [now, setNow] = useState(()=>Date.now());
37
+ const [nextResendAvailableAt, setNextResendAvailableAt] = useState();
38
+ const [formError, setFormError] = useState();
39
+ const [resendError, setResendError] = useState();
40
+ const draftRef = useRef(authFormDraft);
41
+ draftRef.current = authFormDraft;
42
+ // Set just before clearing the draft on success so the unmount cleanup
43
+ // below does not repersist the just-submitted credentials.
44
+ const skipDraftPersistRef = useRef(false);
45
+ const resendRemainingSeconds = getResendRemainingSeconds({
46
+ nextResendAvailableAt,
47
+ now
48
+ });
49
+ const isResendCoolingDown = resendRemainingSeconds > 0;
50
+ const form = useForm({
51
+ defaultValues: {
52
+ email: authFormDraft.email,
53
+ password: authFormDraft.password,
54
+ name: ''
55
+ },
56
+ onSubmit: async ({ value })=>{
57
+ setFormError(undefined);
58
+ try {
59
+ const body = signupBodySchema.parse({
60
+ email: value.email,
61
+ password: value.password,
62
+ name: value.name,
63
+ ...invitationToken ? {
64
+ invitation_token: invitationToken
65
+ } : {}
66
+ });
67
+ const result = await signup.mutateAsync(body);
68
+ skipDraftPersistRef.current = true;
69
+ setAuthFormDraft(initialAuthFormDraft);
70
+ if (invitationToken && result.membership && invitationPending) {
71
+ try {
72
+ await refreshAuth();
73
+ } catch {
74
+ // Refresh failures don't block the success message — the next API
75
+ // call's 401 handling will re-route the user.
76
+ }
77
+ toast.success(`You joined ${invitationPending.workspace_name}.`);
78
+ await navigate({
79
+ to: '/workspaces/$wid',
80
+ params: {
81
+ wid: result.membership.workspace_id
82
+ }
83
+ });
84
+ return;
85
+ }
86
+ if (invitationToken && result.accept_error) {
87
+ toast.error(result.accept_error.message);
88
+ await navigate({
89
+ to: '/invitations/accept',
90
+ search: {
91
+ token: invitationToken
92
+ }
93
+ });
94
+ return;
95
+ }
96
+ setSubmittedEmail(result.user.email);
97
+ setResendError(undefined);
98
+ restartLocalCooldown();
99
+ } catch (error) {
100
+ const mapped = signupErrorToFormError(error);
101
+ if (mapped.kind === 'field') {
102
+ form.setFieldMeta(mapped.field, (prev)=>({
103
+ ...prev,
104
+ errorMap: {
105
+ ...prev.errorMap,
106
+ onServer: mapped.message
107
+ }
108
+ }));
109
+ } else {
110
+ setFormError(mapped.message);
111
+ }
112
+ }
113
+ }
114
+ });
115
+ useEffect(()=>{
116
+ if (!submittedEmail || !nextResendAvailableAt) {
117
+ return;
118
+ }
119
+ const current = Date.now();
120
+ setNow(current);
121
+ if (nextResendAvailableAt <= current) {
122
+ return;
123
+ }
124
+ const handle = window.setInterval(()=>{
125
+ const tickNow = Date.now();
126
+ setNow(tickNow);
127
+ if (nextResendAvailableAt <= tickNow) {
128
+ window.clearInterval(handle);
129
+ }
130
+ }, 1000);
131
+ return ()=>window.clearInterval(handle);
132
+ }, [
133
+ nextResendAvailableAt,
134
+ submittedEmail
135
+ ]);
136
+ function restartLocalCooldown() {
137
+ const current = Date.now();
138
+ setNow(current);
139
+ setNextResendAvailableAt(getLocalResendAvailableAt(current));
140
+ }
141
+ // When arriving from an invitation link, prefill the email and lock it.
142
+ useEffect(()=>{
143
+ if (invitationPending && form.state.values.email !== invitationPending.email) {
144
+ form.setFieldValue('email', invitationPending.email);
145
+ setAuthFormDraft((current)=>({
146
+ ...current,
147
+ email: invitationPending.email
148
+ }));
149
+ }
150
+ }, [
151
+ invitationPending,
152
+ form,
153
+ setAuthFormDraft
154
+ ]);
155
+ // Sync form values back to the Jotai draft on unmount (only email + password
156
+ // — name is intentionally not persisted across navigation). Skipped after a
157
+ // successful signup because we just intentionally cleared the draft.
158
+ useEffect(()=>{
159
+ return ()=>{
160
+ if (skipDraftPersistRef.current) return;
161
+ const { email, password } = form.state.values;
162
+ if (email !== draftRef.current.email || password !== draftRef.current.password) {
163
+ setAuthFormDraft({
164
+ email,
165
+ password
166
+ });
167
+ }
168
+ };
169
+ }, [
170
+ form,
171
+ setAuthFormDraft
172
+ ]);
173
+ async function onResendVerificationEmail() {
174
+ if (!submittedEmail || isResendCoolingDown || resendEmailVerification.isPending) return;
175
+ setResendError(undefined);
176
+ try {
177
+ const result = await resendEmailVerification.mutateAsync({
178
+ email: submittedEmail
179
+ });
180
+ const nextAvailableAt = parseNextResendAvailableAt(result.next_resend_available_at);
181
+ setNow(Date.now());
182
+ if (nextAvailableAt !== undefined) {
183
+ setNextResendAvailableAt(nextAvailableAt);
184
+ } else {
185
+ restartLocalCooldown();
186
+ }
187
+ toast.success('If another verification email can be sent, it will arrive shortly.');
188
+ } catch (error) {
189
+ setResendError(authErrorMessage(error));
190
+ }
191
+ }
192
+ if (submittedEmail) {
193
+ return /*#__PURE__*/ _jsxs(AuthShell, {
194
+ title: "Check your email",
195
+ description: `We sent a verification link to ${submittedEmail}.`,
196
+ children: [
197
+ resendError ? /*#__PURE__*/ _jsx(Callout, {
198
+ role: "alert",
199
+ type: "error",
200
+ children: resendError
201
+ }) : null,
202
+ /*#__PURE__*/ _jsx(Callout, {
203
+ role: "alert",
204
+ type: "success",
205
+ children: "Click the verification link to activate your account, then come back to log in."
206
+ }),
207
+ /*#__PURE__*/ _jsxs("div", {
208
+ className: "flex flex-col gap-8",
209
+ children: [
210
+ /*#__PURE__*/ _jsx(Button, {
211
+ "aria-disabled": isResendCoolingDown ? true : undefined,
212
+ className: "w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70",
213
+ variant: "secondary",
214
+ type: "button",
215
+ isLoading: resendEmailVerification.isPending,
216
+ onClick: onResendVerificationEmail,
217
+ children: resendEmailVerification.isPending ? 'Sending email...' : isResendCoolingDown ? `Resend in ${resendRemainingSeconds}s` : 'Resend verification email'
218
+ }),
219
+ /*#__PURE__*/ _jsx(Button, {
220
+ className: "w-full",
221
+ variant: "transparent",
222
+ type: "button",
223
+ onClick: ()=>{
224
+ setSubmittedEmail(undefined);
225
+ setResendError(undefined);
226
+ },
227
+ children: "Use another email"
228
+ })
229
+ ]
230
+ }),
231
+ /*#__PURE__*/ _jsxs(Text, {
232
+ size: "sm",
233
+ className: "text-center text-foreground-neutral-subtle",
234
+ children: [
235
+ "Already verified?",
236
+ ' ',
237
+ /*#__PURE__*/ _jsx(ButtonLink, {
238
+ asChild: true,
239
+ variant: "interactive",
240
+ underline: true,
241
+ children: /*#__PURE__*/ _jsx(Link, {
242
+ to: "/auth/login",
243
+ children: "Log in"
244
+ })
245
+ })
246
+ ]
247
+ })
248
+ ]
249
+ });
250
+ }
251
+ const headerTitle = invitationPending ? `Join ${invitationPending.workspace_name}` : 'Create your Shipfox account';
252
+ const headerDescription = invitationPending ? `Create an account to accept your invitation.` : 'Start with your email and a password.';
253
+ const isInvitationEmailLocked = Boolean(invitationPending);
254
+ const invitationRedirect = invitationToken ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}` : undefined;
255
+ function persistDraft() {
256
+ const { email, password } = form.state.values;
257
+ setAuthFormDraft({
258
+ email,
259
+ password
260
+ });
261
+ }
262
+ return /*#__PURE__*/ _jsxs(AuthShell, {
263
+ title: headerTitle,
264
+ description: headerDescription,
265
+ children: [
266
+ /*#__PURE__*/ _jsxs("form", {
267
+ className: "flex flex-col gap-18",
268
+ noValidate: true,
269
+ onSubmit: (event)=>{
270
+ event.preventDefault();
271
+ event.stopPropagation();
272
+ void form.handleSubmit();
273
+ },
274
+ children: [
275
+ formError ? /*#__PURE__*/ _jsx(Callout, {
276
+ role: "alert",
277
+ type: "error",
278
+ children: formError
279
+ }) : null,
280
+ /*#__PURE__*/ _jsx(form.Field, {
281
+ name: "name",
282
+ validators: {
283
+ onBlur: ({ value })=>displayNameFieldError(value, 'Name', signupBodySchema.shape.name),
284
+ onSubmit: ({ value })=>displayNameFieldError(value, 'Name', signupBodySchema.shape.name)
285
+ },
286
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
287
+ label: "Name",
288
+ id: "name",
289
+ error: fieldError(field),
290
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
291
+ autoComplete: "name",
292
+ name: "name",
293
+ type: "text",
294
+ value: field.state.value,
295
+ onChange: (event)=>field.handleChange(event.target.value),
296
+ onBlur: field.handleBlur
297
+ })
298
+ })
299
+ }),
300
+ /*#__PURE__*/ _jsx(form.Field, {
301
+ name: "email",
302
+ validators: {
303
+ onBlur: signupBodySchema.shape.email,
304
+ onSubmit: signupBodySchema.shape.email
305
+ },
306
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
307
+ label: "Email",
308
+ id: "email",
309
+ error: fieldError(field),
310
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
311
+ autoComplete: "email",
312
+ name: "email",
313
+ type: "email",
314
+ value: field.state.value,
315
+ onChange: (event)=>field.handleChange(event.target.value),
316
+ onBlur: ()=>{
317
+ field.handleBlur();
318
+ persistDraft();
319
+ },
320
+ readOnly: isInvitationEmailLocked,
321
+ iconRight: isInvitationEmailLocked ? /*#__PURE__*/ _jsx(Icon, {
322
+ "aria-hidden": "true",
323
+ className: "size-16 text-foreground-neutral-disabled",
324
+ name: "lockLine"
325
+ }) : undefined
326
+ })
327
+ })
328
+ }),
329
+ /*#__PURE__*/ _jsx(form.Field, {
330
+ name: "password",
331
+ validators: {
332
+ onBlur: signupBodySchema.shape.password,
333
+ onSubmit: signupBodySchema.shape.password
334
+ },
335
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
336
+ label: "Password",
337
+ id: "password",
338
+ error: fieldError(field),
339
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
340
+ autoComplete: "new-password",
341
+ name: "password",
342
+ type: "password",
343
+ value: field.state.value,
344
+ onChange: (event)=>field.handleChange(event.target.value),
345
+ onBlur: ()=>{
346
+ field.handleBlur();
347
+ persistDraft();
348
+ }
349
+ })
350
+ })
351
+ }),
352
+ /*#__PURE__*/ _jsx(Button, {
353
+ className: "w-full",
354
+ isLoading: signup.isPending,
355
+ type: "submit",
356
+ children: signup.isPending ? 'Creating account...' : 'Create account'
357
+ })
358
+ ]
359
+ }),
360
+ /*#__PURE__*/ _jsxs(Text, {
361
+ size: "sm",
362
+ className: "text-center text-foreground-neutral-subtle",
363
+ children: [
364
+ "Already have an account?",
365
+ ' ',
366
+ /*#__PURE__*/ _jsx(ButtonLink, {
367
+ asChild: true,
368
+ variant: "interactive",
369
+ underline: true,
370
+ children: /*#__PURE__*/ _jsx(Link, {
371
+ to: "/auth/login",
372
+ search: invitationRedirect ? {
373
+ redirect: invitationRedirect
374
+ } : undefined,
375
+ children: "Log in"
376
+ })
377
+ })
378
+ ]
379
+ })
380
+ ]
381
+ });
382
+ }
383
+
384
+ //# sourceMappingURL=signup-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pages/signup-page.tsx"],"sourcesContent":["import {signupBodySchema} from '@shipfox/api-auth-dto';\nimport {displayNameFieldError} from '@shipfox/client-ui';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {useForm} from '@tanstack/react-form';\nimport {Link, useNavigate, useSearch} from '@tanstack/react-router';\nimport {useAtom} from 'jotai';\nimport {useEffect, useRef, useState} from 'react';\nimport {AuthShell} from '#/components/auth-shell.js';\nimport {useRefreshAuth} from '#hooks/api/refresh-auth.js';\nimport {useSignupAuth} from '#hooks/api/signup-auth.js';\nimport {useResendEmailVerificationAuth} from '#hooks/api/verify-email-auth.js';\nimport {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';\nimport {\n getLocalResendAvailableAt,\n getResendRemainingSeconds,\n parseNextResendAvailableAt,\n} from './email-verification-resend-model.js';\nimport {signupErrorToFormError} from './form-errors.js';\nimport {authErrorMessage} from './form-utils.js';\nimport {\n extractInvitationToken,\n pendingInvitation,\n useInvitationContext,\n} from './invitation-context.js';\n\nexport function SignupPage() {\n const signup = useSignupAuth();\n const resendEmailVerification = useResendEmailVerificationAuth();\n const refreshAuth = useRefreshAuth();\n const navigate = useNavigate();\n const search = useSearch({strict: false}) as {redirect?: unknown};\n const invitationToken = extractInvitationToken(search.redirect);\n const invitationPreview = useInvitationContext(invitationToken);\n const invitationPending = pendingInvitation(invitationPreview.data);\n const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);\n const [submittedEmail, setSubmittedEmail] = useState<string | undefined>();\n const [now, setNow] = useState(() => Date.now());\n const [nextResendAvailableAt, setNextResendAvailableAt] = useState<number | undefined>();\n const [formError, setFormError] = useState<string | undefined>();\n const [resendError, setResendError] = useState<string | undefined>();\n const draftRef = useRef(authFormDraft);\n draftRef.current = authFormDraft;\n // Set just before clearing the draft on success so the unmount cleanup\n // below does not repersist the just-submitted credentials.\n const skipDraftPersistRef = useRef(false);\n const resendRemainingSeconds = getResendRemainingSeconds({nextResendAvailableAt, now});\n const isResendCoolingDown = resendRemainingSeconds > 0;\n\n const form = useForm({\n defaultValues: {email: authFormDraft.email, password: authFormDraft.password, name: ''},\n onSubmit: async ({value}) => {\n setFormError(undefined);\n try {\n const body = signupBodySchema.parse({\n email: value.email,\n password: value.password,\n name: value.name,\n ...(invitationToken ? {invitation_token: invitationToken} : {}),\n });\n const result = await signup.mutateAsync(body);\n skipDraftPersistRef.current = true;\n setAuthFormDraft(initialAuthFormDraft);\n\n if (invitationToken && result.membership && invitationPending) {\n try {\n await refreshAuth();\n } catch {\n // Refresh failures don't block the success message — the next API\n // call's 401 handling will re-route the user.\n }\n toast.success(`You joined ${invitationPending.workspace_name}.`);\n await navigate({\n to: '/workspaces/$wid',\n params: {wid: result.membership.workspace_id},\n });\n return;\n }\n\n if (invitationToken && result.accept_error) {\n toast.error(result.accept_error.message);\n await navigate({\n to: '/invitations/accept',\n search: {token: invitationToken},\n });\n return;\n }\n\n setSubmittedEmail(result.user.email);\n setResendError(undefined);\n restartLocalCooldown();\n } catch (error) {\n const mapped = signupErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (prev) => ({\n ...prev,\n errorMap: {...prev.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n },\n });\n\n useEffect(() => {\n if (!submittedEmail || !nextResendAvailableAt) {\n return;\n }\n\n const current = Date.now();\n setNow(current);\n if (nextResendAvailableAt <= current) {\n return;\n }\n\n const handle = window.setInterval(() => {\n const tickNow = Date.now();\n setNow(tickNow);\n if (nextResendAvailableAt <= tickNow) {\n window.clearInterval(handle);\n }\n }, 1000);\n return () => window.clearInterval(handle);\n }, [nextResendAvailableAt, submittedEmail]);\n\n function restartLocalCooldown() {\n const current = Date.now();\n setNow(current);\n setNextResendAvailableAt(getLocalResendAvailableAt(current));\n }\n\n // When arriving from an invitation link, prefill the email and lock it.\n useEffect(() => {\n if (invitationPending && form.state.values.email !== invitationPending.email) {\n form.setFieldValue('email', invitationPending.email);\n setAuthFormDraft((current) => ({...current, email: invitationPending.email}));\n }\n }, [invitationPending, form, setAuthFormDraft]);\n\n // Sync form values back to the Jotai draft on unmount (only email + password\n // — name is intentionally not persisted across navigation). Skipped after a\n // successful signup because we just intentionally cleared the draft.\n useEffect(() => {\n return () => {\n if (skipDraftPersistRef.current) return;\n const {email, password} = form.state.values;\n if (email !== draftRef.current.email || password !== draftRef.current.password) {\n setAuthFormDraft({email, password});\n }\n };\n }, [form, setAuthFormDraft]);\n\n async function onResendVerificationEmail() {\n if (!submittedEmail || isResendCoolingDown || resendEmailVerification.isPending) return;\n\n setResendError(undefined);\n try {\n const result = await resendEmailVerification.mutateAsync({email: submittedEmail});\n const nextAvailableAt = parseNextResendAvailableAt(result.next_resend_available_at);\n setNow(Date.now());\n if (nextAvailableAt !== undefined) {\n setNextResendAvailableAt(nextAvailableAt);\n } else {\n restartLocalCooldown();\n }\n toast.success('If another verification email can be sent, it will arrive shortly.');\n } catch (error) {\n setResendError(authErrorMessage(error));\n }\n }\n\n if (submittedEmail) {\n return (\n <AuthShell\n title=\"Check your email\"\n description={`We sent a verification link to ${submittedEmail}.`}\n >\n {resendError ? (\n <Callout role=\"alert\" type=\"error\">\n {resendError}\n </Callout>\n ) : null}\n <Callout role=\"alert\" type=\"success\">\n Click the verification link to activate your account, then come back to log in.\n </Callout>\n <div className=\"flex flex-col gap-8\">\n <Button\n aria-disabled={isResendCoolingDown ? true : undefined}\n className=\"w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70\"\n variant=\"secondary\"\n type=\"button\"\n isLoading={resendEmailVerification.isPending}\n onClick={onResendVerificationEmail}\n >\n {resendEmailVerification.isPending\n ? 'Sending email...'\n : isResendCoolingDown\n ? `Resend in ${resendRemainingSeconds}s`\n : 'Resend verification email'}\n </Button>\n <Button\n className=\"w-full\"\n variant=\"transparent\"\n type=\"button\"\n onClick={() => {\n setSubmittedEmail(undefined);\n setResendError(undefined);\n }}\n >\n Use another email\n </Button>\n </div>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-subtle\">\n Already verified?{' '}\n <ButtonLink asChild variant=\"interactive\" underline>\n <Link to=\"/auth/login\">Log in</Link>\n </ButtonLink>\n </Text>\n </AuthShell>\n );\n }\n\n const headerTitle = invitationPending\n ? `Join ${invitationPending.workspace_name}`\n : 'Create your Shipfox account';\n const headerDescription = invitationPending\n ? `Create an account to accept your invitation.`\n : 'Start with your email and a password.';\n const isInvitationEmailLocked = Boolean(invitationPending);\n const invitationRedirect = invitationToken\n ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}`\n : undefined;\n\n function persistDraft() {\n const {email, password} = form.state.values;\n setAuthFormDraft({email, password});\n }\n\n return (\n <AuthShell title={headerTitle} description={headerDescription}>\n <form\n className=\"flex flex-col gap-18\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n >\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n {formError}\n </Callout>\n ) : null}\n <form.Field\n name=\"name\"\n validators={{\n onBlur: ({value}) => displayNameFieldError(value, 'Name', signupBodySchema.shape.name),\n onSubmit: ({value}) =>\n displayNameFieldError(value, 'Name', signupBodySchema.shape.name),\n }}\n >\n {(field) => (\n <FormField label=\"Name\" id=\"name\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"name\"\n name=\"name\"\n type=\"text\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={field.handleBlur}\n />\n </FormField>\n )}\n </form.Field>\n <form.Field\n name=\"email\"\n validators={{\n onBlur: signupBodySchema.shape.email,\n onSubmit: signupBodySchema.shape.email,\n }}\n >\n {(field) => (\n <FormField label=\"Email\" id=\"email\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"email\"\n name=\"email\"\n type=\"email\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n readOnly={isInvitationEmailLocked}\n iconRight={\n isInvitationEmailLocked ? (\n <Icon\n aria-hidden=\"true\"\n className=\"size-16 text-foreground-neutral-disabled\"\n name=\"lockLine\"\n />\n ) : undefined\n }\n />\n </FormField>\n )}\n </form.Field>\n <form.Field\n name=\"password\"\n validators={{\n onBlur: signupBodySchema.shape.password,\n onSubmit: signupBodySchema.shape.password,\n }}\n >\n {(field) => (\n <FormField label=\"Password\" id=\"password\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"new-password\"\n name=\"password\"\n type=\"password\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n />\n </FormField>\n )}\n </form.Field>\n <Button className=\"w-full\" isLoading={signup.isPending} type=\"submit\">\n {signup.isPending ? 'Creating account...' : 'Create account'}\n </Button>\n </form>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-subtle\">\n Already have an account?{' '}\n <ButtonLink asChild variant=\"interactive\" underline>\n <Link\n to=\"/auth/login\"\n search={invitationRedirect ? {redirect: invitationRedirect} : undefined}\n >\n Log in\n </Link>\n </ButtonLink>\n </Text>\n </AuthShell>\n );\n}\n"],"names":["signupBodySchema","displayNameFieldError","Button","ButtonLink","Callout","FormField","FormFieldInput","fieldError","Icon","toast","Text","useForm","Link","useNavigate","useSearch","useAtom","useEffect","useRef","useState","AuthShell","useRefreshAuth","useSignupAuth","useResendEmailVerificationAuth","authFormDraftAtom","initialAuthFormDraft","getLocalResendAvailableAt","getResendRemainingSeconds","parseNextResendAvailableAt","signupErrorToFormError","authErrorMessage","extractInvitationToken","pendingInvitation","useInvitationContext","SignupPage","signup","resendEmailVerification","refreshAuth","navigate","search","strict","invitationToken","redirect","invitationPreview","invitationPending","data","authFormDraft","setAuthFormDraft","submittedEmail","setSubmittedEmail","now","setNow","Date","nextResendAvailableAt","setNextResendAvailableAt","formError","setFormError","resendError","setResendError","draftRef","current","skipDraftPersistRef","resendRemainingSeconds","isResendCoolingDown","form","defaultValues","email","password","name","onSubmit","value","undefined","body","parse","invitation_token","result","mutateAsync","membership","success","workspace_name","to","params","wid","workspace_id","accept_error","error","message","token","user","restartLocalCooldown","mapped","kind","setFieldMeta","field","prev","errorMap","onServer","handle","window","setInterval","tickNow","clearInterval","state","values","setFieldValue","onResendVerificationEmail","isPending","nextAvailableAt","next_resend_available_at","title","description","role","type","div","className","aria-disabled","variant","isLoading","onClick","size","asChild","underline","headerTitle","headerDescription","isInvitationEmailLocked","Boolean","invitationRedirect","encodeURIComponent","persistDraft","noValidate","event","preventDefault","stopPropagation","handleSubmit","Field","validators","onBlur","shape","label","id","autoComplete","onChange","handleChange","target","handleBlur","readOnly","iconRight","aria-hidden"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function VerifyEmailPage(): null;
2
+ //# sourceMappingURL=verify-email-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-email-page.d.ts","sourceRoot":"","sources":["../../src/pages/verify-email-page.tsx"],"names":[],"mappings":"AAUA,wBAAgB,eAAe,SAqC9B"}
@@ -0,0 +1,57 @@
1
+ import { toast } from '@shipfox/react-ui/toast';
2
+ import { useNavigate, useSearch } from '@tanstack/react-router';
3
+ import { useEffect } from 'react';
4
+ import { useRefreshAuth } from '#hooks/api/refresh-auth.js';
5
+ import { useVerifyEmailAuth } from '#hooks/api/verify-email-auth.js';
6
+ import { authErrorMessage } from './form-utils.js';
7
+ const verificationRequests = new Map();
8
+ const toastedVerificationTokens = new Set();
9
+ export function VerifyEmailPage() {
10
+ const verifyEmail = useVerifyEmailAuth();
11
+ const refreshAuth = useRefreshAuth();
12
+ const navigate = useNavigate();
13
+ const search = useSearch({
14
+ strict: false
15
+ });
16
+ const token = typeof search.token === 'string' ? search.token : undefined;
17
+ useEffect(()=>{
18
+ if (!token) {
19
+ toast.error('This verification link is missing a token.');
20
+ navigate({
21
+ to: '/',
22
+ replace: true
23
+ });
24
+ return;
25
+ }
26
+ let request = verificationRequests.get(token);
27
+ if (!request) {
28
+ request = verifyEmail.mutateAsync({
29
+ token
30
+ }).then(async ()=>{
31
+ await refreshAuth();
32
+ });
33
+ verificationRequests.set(token, request);
34
+ }
35
+ request.then(()=>{
36
+ if (toastedVerificationTokens.has(token)) return;
37
+ toastedVerificationTokens.add(token);
38
+ toast.success('Your email is verified. You are now logged in.');
39
+ navigate({
40
+ to: '/',
41
+ replace: true
42
+ });
43
+ }).catch((error)=>{
44
+ if (toastedVerificationTokens.has(token)) return;
45
+ toastedVerificationTokens.add(token);
46
+ toast.error(authErrorMessage(error));
47
+ });
48
+ }, [
49
+ navigate,
50
+ refreshAuth,
51
+ token,
52
+ verifyEmail.mutateAsync
53
+ ]);
54
+ return null;
55
+ }
56
+
57
+ //# sourceMappingURL=verify-email-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pages/verify-email-page.tsx"],"sourcesContent":["import {toast} from '@shipfox/react-ui/toast';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect} from 'react';\nimport {useRefreshAuth} from '#hooks/api/refresh-auth.js';\nimport {useVerifyEmailAuth} from '#hooks/api/verify-email-auth.js';\nimport {authErrorMessage} from './form-utils.js';\n\nconst verificationRequests = new Map<string, Promise<void>>();\nconst toastedVerificationTokens = new Set<string>();\n\nexport function VerifyEmailPage() {\n const verifyEmail = useVerifyEmailAuth();\n const refreshAuth = useRefreshAuth();\n const navigate = useNavigate();\n const search = useSearch({strict: false});\n const token = typeof search.token === 'string' ? search.token : undefined;\n\n useEffect(() => {\n if (!token) {\n toast.error('This verification link is missing a token.');\n navigate({to: '/', replace: true});\n return;\n }\n\n let request = verificationRequests.get(token);\n if (!request) {\n request = verifyEmail.mutateAsync({token}).then(async () => {\n await refreshAuth();\n });\n verificationRequests.set(token, request);\n }\n\n request\n .then(() => {\n if (toastedVerificationTokens.has(token)) return;\n toastedVerificationTokens.add(token);\n toast.success('Your email is verified. You are now logged in.');\n navigate({to: '/', replace: true});\n })\n .catch((error) => {\n if (toastedVerificationTokens.has(token)) return;\n toastedVerificationTokens.add(token);\n toast.error(authErrorMessage(error));\n });\n }, [navigate, refreshAuth, token, verifyEmail.mutateAsync]);\n\n return null;\n}\n"],"names":["toast","useNavigate","useSearch","useEffect","useRefreshAuth","useVerifyEmailAuth","authErrorMessage","verificationRequests","Map","toastedVerificationTokens","Set","VerifyEmailPage","verifyEmail","refreshAuth","navigate","search","strict","token","undefined","error","to","replace","request","get","mutateAsync","then","set","has","add","success","catch"],"mappings":"AAAA,SAAQA,KAAK,QAAO,0BAA0B;AAC9C,SAAQC,WAAW,EAAEC,SAAS,QAAO,yBAAyB;AAC9D,SAAQC,SAAS,QAAO,QAAQ;AAChC,SAAQC,cAAc,QAAO,6BAA6B;AAC1D,SAAQC,kBAAkB,QAAO,kCAAkC;AACnE,SAAQC,gBAAgB,QAAO,kBAAkB;AAEjD,MAAMC,uBAAuB,IAAIC;AACjC,MAAMC,4BAA4B,IAAIC;AAEtC,OAAO,SAASC;IACd,MAAMC,cAAcP;IACpB,MAAMQ,cAAcT;IACpB,MAAMU,WAAWb;IACjB,MAAMc,SAASb,UAAU;QAACc,QAAQ;IAAK;IACvC,MAAMC,QAAQ,OAAOF,OAAOE,KAAK,KAAK,WAAWF,OAAOE,KAAK,GAAGC;IAEhEf,UAAU;QACR,IAAI,CAACc,OAAO;YACVjB,MAAMmB,KAAK,CAAC;YACZL,SAAS;gBAACM,IAAI;gBAAKC,SAAS;YAAI;YAChC;QACF;QAEA,IAAIC,UAAUf,qBAAqBgB,GAAG,CAACN;QACvC,IAAI,CAACK,SAAS;YACZA,UAAUV,YAAYY,WAAW,CAAC;gBAACP;YAAK,GAAGQ,IAAI,CAAC;gBAC9C,MAAMZ;YACR;YACAN,qBAAqBmB,GAAG,CAACT,OAAOK;QAClC;QAEAA,QACGG,IAAI,CAAC;YACJ,IAAIhB,0BAA0BkB,GAAG,CAACV,QAAQ;YAC1CR,0BAA0BmB,GAAG,CAACX;YAC9BjB,MAAM6B,OAAO,CAAC;YACdf,SAAS;gBAACM,IAAI;gBAAKC,SAAS;YAAI;QAClC,GACCS,KAAK,CAAC,CAACX;YACN,IAAIV,0BAA0BkB,GAAG,CAACV,QAAQ;YAC1CR,0BAA0BmB,GAAG,CAACX;YAC9BjB,MAAMmB,KAAK,CAACb,iBAAiBa;QAC/B;IACJ,GAAG;QAACL;QAAUD;QAAaI;QAAOL,YAAYY,WAAW;KAAC;IAE1D,OAAO;AACT"}
@@ -0,0 +1,2 @@
1
+ export declare function WorkspaceOnboardingPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=workspace-onboarding-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-onboarding-page.d.ts","sourceRoot":"","sources":["../../src/pages/workspace-onboarding-page.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,uBAAuB,gCA2JtC"}