@optare/react 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1477 @@
1
+ // src/context.tsx
2
+ import {
3
+ createContext,
4
+ createElement,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+ import {
12
+ resolveOptareConfig,
13
+ OptareConfigError,
14
+ DEFAULT_AUTH_METHODS
15
+ } from "@optare/client";
16
+
17
+ // src/auth-client.ts
18
+ import { createAuthClient } from "better-auth/react";
19
+ import {
20
+ organizationClient,
21
+ twoFactorClient,
22
+ emailOTPClient,
23
+ magicLinkClient,
24
+ multiSessionClient,
25
+ jwtClient
26
+ } from "better-auth/client/plugins";
27
+ function optarePlugins() {
28
+ return [
29
+ organizationClient(),
30
+ twoFactorClient(),
31
+ emailOTPClient(),
32
+ magicLinkClient(),
33
+ multiSessionClient(),
34
+ jwtClient()
35
+ ];
36
+ }
37
+ function createReactAuthClient(options) {
38
+ return createAuthClient({
39
+ baseURL: options.bootstrap.baseURL,
40
+ fetchOptions: {
41
+ ...options.fetch ? { customFetchImpl: options.fetch } : {},
42
+ headers: {
43
+ "x-optare-publishable-key": options.publishableKey,
44
+ ...options.headers
45
+ }
46
+ },
47
+ plugins: [...optarePlugins()]
48
+ });
49
+ }
50
+
51
+ // src/theme.ts
52
+ var DEFAULT_PRIMARY = "#4f46e5";
53
+ var DEFAULT_RADIUS = "10px";
54
+ var CSS_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
55
+ var CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|rem|em|%)$/;
56
+ function safeColor(value, fallback) {
57
+ return value && CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
58
+ }
59
+ function safeLength(value, fallback) {
60
+ const v = (value ?? "").trim();
61
+ if (CSS_LENGTH.test(v)) return v;
62
+ if (/^\d+$/.test(v)) return `${v}px`;
63
+ return fallback;
64
+ }
65
+ function normalizeHex(hex) {
66
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim());
67
+ if (!m) return null;
68
+ let h = m[1];
69
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
70
+ return `#${h.slice(0, 6)}`;
71
+ }
72
+ function relativeLuminance(hex) {
73
+ const n = normalizeHex(hex);
74
+ if (!n) return 0;
75
+ const int = parseInt(n.slice(1), 16);
76
+ const chan = [int >> 16 & 255, int >> 8 & 255, int & 255].map((v) => {
77
+ const s2 = v / 255;
78
+ return s2 <= 0.03928 ? s2 / 12.92 : ((s2 + 0.055) / 1.055) ** 2.4;
79
+ });
80
+ return 0.2126 * chan[0] + 0.7152 * chan[1] + 0.0722 * chan[2];
81
+ }
82
+ function contrastText(hex) {
83
+ const bg = relativeLuminance(hex);
84
+ const whiteTextRatio = (1 + 0.05) / (bg + 0.05);
85
+ const darkTextRatio = (bg + 0.05) / (relativeLuminance("#111827") + 0.05);
86
+ return darkTextRatio >= whiteTextRatio ? "#111827" : "#ffffff";
87
+ }
88
+ function brandingToCssVars(branding) {
89
+ const primary = safeColor(branding?.primaryColor, DEFAULT_PRIMARY);
90
+ const radius = safeLength(branding?.radius, DEFAULT_RADIUS);
91
+ return {
92
+ "--optare-primary": primary,
93
+ "--optare-primary-text": contrastText(primary),
94
+ // A translucent wash of the primary for hover/focus rings — layered over
95
+ // white it reads as a tint without needing a second colour from branding.
96
+ "--optare-primary-soft": "color-mix(in srgb, var(--optare-primary) 12%, #ffffff)",
97
+ "--optare-ring": "color-mix(in srgb, var(--optare-primary) 35%, transparent)",
98
+ "--optare-radius": radius,
99
+ "--optare-bg": "#ffffff",
100
+ "--optare-bg-subtle": "#f9fafb",
101
+ "--optare-fg": "#111827",
102
+ "--optare-fg-muted": "#4b5563",
103
+ "--optare-muted": "#6b7280",
104
+ "--optare-border": "#e5e7eb",
105
+ "--optare-border-strong": "#d1d5db",
106
+ "--optare-error": "#dc2626",
107
+ "--optare-error-soft": "#fef2f2",
108
+ "--optare-success": "#059669",
109
+ "--optare-shadow": "0 1px 2px rgba(16,24,40,0.04), 0 8px 24px rgba(16,24,40,0.08)",
110
+ "--optare-font": 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
111
+ };
112
+ }
113
+
114
+ // src/context.tsx
115
+ var OptareContext = createContext(null);
116
+ function OptareProvider(props) {
117
+ const {
118
+ publishableKey,
119
+ bootstrap: bootstrapProp,
120
+ baseURL,
121
+ configURL,
122
+ fetch: fetchImpl,
123
+ headers,
124
+ loadingFallback,
125
+ errorFallback,
126
+ children
127
+ } = props;
128
+ const [bootstrap, setBootstrap] = useState(
129
+ bootstrapProp ?? null
130
+ );
131
+ const [status, setStatus] = useState(
132
+ bootstrapProp ? "ready" : "loading"
133
+ );
134
+ const [error, setError] = useState(null);
135
+ const [attempt, setAttempt] = useState(0);
136
+ useEffect(() => {
137
+ if (bootstrapProp) {
138
+ setBootstrap(bootstrapProp);
139
+ setStatus("ready");
140
+ setError(null);
141
+ return;
142
+ }
143
+ let cancelled = false;
144
+ setStatus("loading");
145
+ setError(null);
146
+ resolveOptareConfig({ publishableKey, baseURL, configURL, fetch: fetchImpl }).then((resolved) => {
147
+ if (cancelled) return;
148
+ setBootstrap(resolved);
149
+ setStatus("ready");
150
+ }).catch((err) => {
151
+ if (cancelled) return;
152
+ setError(
153
+ err instanceof Error ? err : new OptareConfigError("Failed to resolve Optare config")
154
+ );
155
+ setStatus("error");
156
+ });
157
+ return () => {
158
+ cancelled = true;
159
+ };
160
+ }, [publishableKey, baseURL, configURL, fetchImpl, bootstrapProp, attempt]);
161
+ const authRef = useRef(
162
+ null
163
+ );
164
+ const auth = useMemo(() => {
165
+ if (!bootstrap) return null;
166
+ const cached = authRef.current;
167
+ if (cached && cached.baseURL === bootstrap.baseURL) return cached.client;
168
+ const client = createReactAuthClient({
169
+ bootstrap,
170
+ publishableKey,
171
+ fetch: fetchImpl,
172
+ headers
173
+ });
174
+ authRef.current = { baseURL: bootstrap.baseURL, client };
175
+ return client;
176
+ }, [bootstrap, publishableKey, fetchImpl, headers]);
177
+ const retry = useMemo(() => () => setAttempt((n) => n + 1), []);
178
+ const value = useMemo(
179
+ () => ({
180
+ status,
181
+ error,
182
+ bootstrap,
183
+ branding: bootstrap?.branding ?? null,
184
+ project: bootstrap?.project ?? null,
185
+ authMethods: bootstrap?.authMethods ?? DEFAULT_AUTH_METHODS,
186
+ cssVars: brandingToCssVars(bootstrap?.branding ?? null),
187
+ auth,
188
+ publishableKey,
189
+ retry
190
+ }),
191
+ [status, error, bootstrap, auth, publishableKey, retry]
192
+ );
193
+ let body = children;
194
+ if (status === "loading" && loadingFallback !== void 0) body = loadingFallback;
195
+ if (status === "error" && errorFallback !== void 0 && error) {
196
+ body = typeof errorFallback === "function" ? errorFallback(error, retry) : errorFallback;
197
+ }
198
+ return createElement(OptareContext.Provider, { value }, body);
199
+ }
200
+ function useOptare() {
201
+ const ctx = useContext(OptareContext);
202
+ if (!ctx) {
203
+ throw new Error("useOptare must be used inside <OptareProvider>");
204
+ }
205
+ return ctx;
206
+ }
207
+ function useOptareAuth() {
208
+ const { auth, status, error } = useOptare();
209
+ if (!auth) {
210
+ throw new Error(
211
+ status === "error" ? `Optare provider failed to initialise: ${error?.message ?? "unknown error"}` : "Optare provider is still resolving \u2014 gate on `useOptare().status === 'ready'`"
212
+ );
213
+ }
214
+ return auth;
215
+ }
216
+ function useOptareConfig() {
217
+ const { bootstrap, project, branding, status } = useOptare();
218
+ return { bootstrap, project, branding, status };
219
+ }
220
+ function useBranding() {
221
+ return useOptare().branding;
222
+ }
223
+ function useAuthMethods() {
224
+ return useOptare().authMethods;
225
+ }
226
+
227
+ // src/hooks.ts
228
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
229
+
230
+ // src/session.ts
231
+ var listeners = /* @__PURE__ */ new Set();
232
+ function subscribeSessionChanged(listener) {
233
+ listeners.add(listener);
234
+ return () => listeners.delete(listener);
235
+ }
236
+ function notifySessionChanged() {
237
+ for (const l of [...listeners]) l();
238
+ }
239
+
240
+ // src/hooks.ts
241
+ function useSession() {
242
+ const { auth, status } = useOptare();
243
+ const [data, setData] = useState2(null);
244
+ const [error, setError] = useState2(null);
245
+ const [isPending, setIsPending] = useState2(true);
246
+ const [tick, setTick] = useState2(0);
247
+ const refetch = useCallback(() => setTick((n) => n + 1), []);
248
+ useEffect2(() => subscribeSessionChanged(refetch), [refetch]);
249
+ useEffect2(() => {
250
+ if (status === "error") {
251
+ setIsPending(false);
252
+ return;
253
+ }
254
+ if (!auth) return;
255
+ let cancelled = false;
256
+ setIsPending(true);
257
+ Promise.resolve(auth.getSession()).then((res) => {
258
+ if (cancelled) return;
259
+ const r = res;
260
+ setData(r?.data ?? null);
261
+ setError(r?.error ? new Error(r.error.message ?? "Session error") : null);
262
+ }).catch((err) => {
263
+ if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
264
+ }).finally(() => {
265
+ if (!cancelled) setIsPending(false);
266
+ });
267
+ return () => {
268
+ cancelled = true;
269
+ };
270
+ }, [auth, status, tick]);
271
+ useEffect2(() => {
272
+ if (typeof window === "undefined") return;
273
+ const onFocus = () => refetch();
274
+ window.addEventListener("focus", onFocus);
275
+ return () => window.removeEventListener("focus", onFocus);
276
+ }, [refetch]);
277
+ return useMemo2(
278
+ () => ({
279
+ data,
280
+ user: data?.user ?? null,
281
+ isPending,
282
+ isAuthenticated: !!data,
283
+ error,
284
+ refetch
285
+ }),
286
+ [data, isPending, error, refetch]
287
+ );
288
+ }
289
+ function useUser() {
290
+ return useSession().user;
291
+ }
292
+ function useSignOut() {
293
+ const { auth } = useOptare();
294
+ const [isPending, setIsPending] = useState2(false);
295
+ const [error, setError] = useState2(null);
296
+ const signOut = useCallback(async () => {
297
+ if (!auth) return;
298
+ setIsPending(true);
299
+ setError(null);
300
+ try {
301
+ await auth.signOut();
302
+ notifySessionChanged();
303
+ } catch (err) {
304
+ setError(err instanceof Error ? err : new Error(String(err)));
305
+ throw err;
306
+ } finally {
307
+ setIsPending(false);
308
+ }
309
+ }, [auth]);
310
+ return { signOut, isPending, error };
311
+ }
312
+ function useActiveOrganization() {
313
+ const { auth } = useOptare();
314
+ const { data } = useSession();
315
+ const [isPending, setIsPending] = useState2(false);
316
+ const activeOrganizationId = data?.session?.activeOrganizationId ?? null;
317
+ const setActive = useCallback(
318
+ async (organizationId) => {
319
+ if (!auth) return;
320
+ setIsPending(true);
321
+ try {
322
+ await auth.organization.setActive({ organizationId });
323
+ notifySessionChanged();
324
+ } finally {
325
+ setIsPending(false);
326
+ }
327
+ },
328
+ [auth]
329
+ );
330
+ return { activeOrganizationId, setActive, isPending };
331
+ }
332
+
333
+ // src/validation.ts
334
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
335
+ function isValidEmail(value) {
336
+ return EMAIL_RE.test(value.trim());
337
+ }
338
+ function checkPassword(value, min = 8) {
339
+ if (value.length === 0) return { ok: false, score: 0, message: "Password is required" };
340
+ if (value.length < min)
341
+ return { ok: false, score: 1, message: `Use at least ${min} characters` };
342
+ let score = 1;
343
+ if (value.length >= 12) score++;
344
+ if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score++;
345
+ if (/\d/.test(value)) score++;
346
+ if (/[^A-Za-z0-9]/.test(value)) score++;
347
+ return { ok: true, score: Math.min(score, 4), message: null };
348
+ }
349
+ function validateSignIn(fields) {
350
+ const errors = {};
351
+ if (!isValidEmail(fields.email)) errors.email = "Enter a valid email address";
352
+ if (fields.password.length === 0) errors.password = "Password is required";
353
+ return errors;
354
+ }
355
+ function validateSignUp(fields, passwordMin = 8) {
356
+ const errors = {};
357
+ if (fields.name.trim().length === 0) errors.name = "Name is required";
358
+ if (!isValidEmail(fields.email)) errors.email = "Enter a valid email address";
359
+ const pw = checkPassword(fields.password, passwordMin);
360
+ if (!pw.ok && pw.message) errors.password = pw.message;
361
+ return errors;
362
+ }
363
+
364
+ // src/components/SignIn.tsx
365
+ import { useState as useState4 } from "react";
366
+
367
+ // src/ui.tsx
368
+ import {
369
+ forwardRef
370
+ } from "react";
371
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
372
+ var base = {
373
+ card: {
374
+ boxSizing: "border-box",
375
+ width: "100%",
376
+ maxWidth: 400,
377
+ padding: 28,
378
+ background: "var(--optare-bg)",
379
+ color: "var(--optare-fg)",
380
+ border: "1px solid var(--optare-border)",
381
+ borderRadius: "var(--optare-radius)",
382
+ boxShadow: "var(--optare-shadow)",
383
+ fontFamily: "var(--optare-font)",
384
+ fontSize: 14,
385
+ lineHeight: 1.5,
386
+ WebkitFontSmoothing: "antialiased",
387
+ display: "flex",
388
+ flexDirection: "column",
389
+ gap: 20
390
+ },
391
+ header: {
392
+ display: "flex",
393
+ flexDirection: "column",
394
+ alignItems: "center",
395
+ gap: 6,
396
+ textAlign: "center"
397
+ },
398
+ logo: { height: 32, maxWidth: 160, objectFit: "contain", marginBottom: 2 },
399
+ brandName: {
400
+ fontSize: 13,
401
+ fontWeight: 600,
402
+ letterSpacing: 0.2,
403
+ color: "var(--optare-fg-muted)",
404
+ textTransform: "uppercase"
405
+ },
406
+ title: {
407
+ margin: 0,
408
+ fontSize: 19,
409
+ fontWeight: 650,
410
+ color: "var(--optare-fg)"
411
+ },
412
+ subtitle: {
413
+ margin: 0,
414
+ fontSize: 13.5,
415
+ color: "var(--optare-muted)"
416
+ },
417
+ field: { display: "flex", flexDirection: "column", gap: 6 },
418
+ label: { fontSize: 13, fontWeight: 550, color: "var(--optare-fg)" },
419
+ input: {
420
+ boxSizing: "border-box",
421
+ width: "100%",
422
+ padding: "10px 12px",
423
+ fontSize: 14,
424
+ fontFamily: "inherit",
425
+ color: "var(--optare-fg)",
426
+ background: "var(--optare-bg)",
427
+ border: "1px solid var(--optare-border-strong)",
428
+ borderRadius: "var(--optare-radius)",
429
+ outline: "none",
430
+ transition: "border-color 120ms, box-shadow 120ms"
431
+ },
432
+ fieldError: { fontSize: 12, color: "var(--optare-error)" },
433
+ button: {
434
+ boxSizing: "border-box",
435
+ display: "inline-flex",
436
+ alignItems: "center",
437
+ justifyContent: "center",
438
+ gap: 8,
439
+ width: "100%",
440
+ padding: "10px 16px",
441
+ fontSize: 14,
442
+ fontWeight: 600,
443
+ fontFamily: "inherit",
444
+ lineHeight: 1.2,
445
+ cursor: "pointer",
446
+ color: "var(--optare-primary-text)",
447
+ background: "var(--optare-primary)",
448
+ border: "1px solid transparent",
449
+ borderRadius: "var(--optare-radius)",
450
+ transition: "filter 120ms, opacity 120ms"
451
+ },
452
+ buttonSecondary: {
453
+ color: "var(--optare-fg)",
454
+ background: "var(--optare-bg)",
455
+ border: "1px solid var(--optare-border-strong)"
456
+ },
457
+ buttonDisabled: { opacity: 0.55, cursor: "not-allowed" },
458
+ alert: {
459
+ padding: "9px 12px",
460
+ fontSize: 13,
461
+ borderRadius: "var(--optare-radius)",
462
+ background: "var(--optare-error-soft)",
463
+ color: "var(--optare-error)",
464
+ border: "1px solid color-mix(in srgb, var(--optare-error) 30%, transparent)"
465
+ },
466
+ notice: {
467
+ padding: "9px 12px",
468
+ fontSize: 13,
469
+ borderRadius: "var(--optare-radius)",
470
+ background: "var(--optare-bg-subtle)",
471
+ color: "var(--optare-fg-muted)",
472
+ border: "1px solid var(--optare-border)"
473
+ },
474
+ link: {
475
+ background: "none",
476
+ border: "none",
477
+ padding: 0,
478
+ font: "inherit",
479
+ fontSize: 13,
480
+ color: "var(--optare-primary)",
481
+ cursor: "pointer",
482
+ textDecoration: "none",
483
+ fontWeight: 550
484
+ },
485
+ footer: { fontSize: 13, color: "var(--optare-muted)", textAlign: "center" },
486
+ divider: {
487
+ display: "flex",
488
+ alignItems: "center",
489
+ gap: 10,
490
+ fontSize: 12,
491
+ color: "var(--optare-muted)"
492
+ },
493
+ dividerRule: { flex: 1, height: 1, background: "var(--optare-border)" },
494
+ socialGrid: { display: "grid", gap: 8 },
495
+ spinner: {
496
+ display: "inline-block",
497
+ width: 14,
498
+ height: 14,
499
+ border: "2px solid currentColor",
500
+ borderTopColor: "transparent",
501
+ borderRadius: "50%",
502
+ animation: "optare-spin 0.6s linear infinite"
503
+ }
504
+ };
505
+ function SpinnerKeyframes() {
506
+ return /* @__PURE__ */ jsx("style", { children: "@keyframes optare-spin{to{transform:rotate(360deg)}}" });
507
+ }
508
+ function Spinner() {
509
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
510
+ /* @__PURE__ */ jsx(SpinnerKeyframes, {}),
511
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: base.spinner })
512
+ ] });
513
+ }
514
+ function Card(props) {
515
+ const { cssVars } = useOptare();
516
+ return /* @__PURE__ */ jsx(
517
+ "div",
518
+ {
519
+ className: props.className,
520
+ style: { ...cssVars, ...base.card, ...props.style },
521
+ children: props.children
522
+ }
523
+ );
524
+ }
525
+ function BrandHeader(props) {
526
+ const { branding } = useOptare();
527
+ return /* @__PURE__ */ jsxs("div", { style: base.header, children: [
528
+ branding?.logoUrl ? /* @__PURE__ */ jsx("img", { src: branding.logoUrl, alt: branding.name, style: base.logo }) : branding?.name ? /* @__PURE__ */ jsx("span", { style: base.brandName, children: branding.name }) : null,
529
+ /* @__PURE__ */ jsx("h2", { style: base.title, children: props.title }),
530
+ props.subtitle ? /* @__PURE__ */ jsx("p", { style: base.subtitle, children: props.subtitle }) : null
531
+ ] });
532
+ }
533
+ var TextInput = forwardRef(function TextInput2({ label, error, hint, id, onFocus, onBlur, ...rest }, ref) {
534
+ const inputId = id ?? `optare-${label.toLowerCase().replace(/\s+/g, "-")}`;
535
+ return /* @__PURE__ */ jsxs("div", { style: base.field, children: [
536
+ /* @__PURE__ */ jsx("label", { htmlFor: inputId, style: base.label, children: label }),
537
+ /* @__PURE__ */ jsx(
538
+ "input",
539
+ {
540
+ ref,
541
+ id: inputId,
542
+ style: {
543
+ ...base.input,
544
+ ...error ? { borderColor: "var(--optare-error)" } : null
545
+ },
546
+ "aria-invalid": error ? true : void 0,
547
+ onFocus: (e) => {
548
+ if (!error) {
549
+ e.currentTarget.style.borderColor = "var(--optare-primary)";
550
+ e.currentTarget.style.boxShadow = "0 0 0 3px var(--optare-ring)";
551
+ }
552
+ onFocus?.(e);
553
+ },
554
+ onBlur: (e) => {
555
+ e.currentTarget.style.borderColor = error ? "var(--optare-error)" : "var(--optare-border-strong)";
556
+ e.currentTarget.style.boxShadow = "none";
557
+ onBlur?.(e);
558
+ },
559
+ ...rest
560
+ }
561
+ ),
562
+ error ? /* @__PURE__ */ jsx("span", { style: base.fieldError, children: error }) : hint ? /* @__PURE__ */ jsx("span", { style: { ...base.subtitle, fontSize: 12 }, children: hint }) : null
563
+ ] });
564
+ });
565
+ function Button({
566
+ variant = "primary",
567
+ busy = false,
568
+ style,
569
+ disabled,
570
+ children,
571
+ ...rest
572
+ }) {
573
+ const isDisabled = disabled || busy;
574
+ return /* @__PURE__ */ jsxs(
575
+ "button",
576
+ {
577
+ style: {
578
+ ...base.button,
579
+ ...variant === "secondary" ? base.buttonSecondary : null,
580
+ ...isDisabled ? base.buttonDisabled : null,
581
+ ...style
582
+ },
583
+ disabled: isDisabled,
584
+ onMouseEnter: (e) => {
585
+ if (!isDisabled) e.currentTarget.style.filter = "brightness(0.94)";
586
+ },
587
+ onMouseLeave: (e) => {
588
+ e.currentTarget.style.filter = "none";
589
+ },
590
+ ...rest,
591
+ children: [
592
+ busy ? /* @__PURE__ */ jsx(Spinner, {}) : null,
593
+ children
594
+ ]
595
+ }
596
+ );
597
+ }
598
+ function Alert({ children }) {
599
+ return /* @__PURE__ */ jsx("div", { role: "alert", style: base.alert, children });
600
+ }
601
+ function Notice({ children }) {
602
+ return /* @__PURE__ */ jsx("div", { style: base.notice, children });
603
+ }
604
+ function LinkButton(props) {
605
+ return /* @__PURE__ */ jsx("button", { type: "button", style: { ...base.link, ...props.style }, ...props });
606
+ }
607
+ function Footer({ children }) {
608
+ return /* @__PURE__ */ jsx("div", { style: base.footer, children });
609
+ }
610
+ function Divider({ children }) {
611
+ if (!children) return /* @__PURE__ */ jsx("div", { style: base.dividerRule });
612
+ return /* @__PURE__ */ jsxs("div", { style: base.divider, children: [
613
+ /* @__PURE__ */ jsx("span", { style: base.dividerRule }),
614
+ /* @__PURE__ */ jsx("span", { children }),
615
+ /* @__PURE__ */ jsx("span", { style: base.dividerRule })
616
+ ] });
617
+ }
618
+
619
+ // src/components/SocialButtons.tsx
620
+ import { useState as useState3 } from "react";
621
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
622
+ var MARKS = {
623
+ google: {
624
+ name: "Google",
625
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 48 48", "aria-hidden": true, children: [
626
+ /* @__PURE__ */ jsx2("path", { fill: "#EA4335", d: "M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.4 30.2 0 24 0 14.6 0 6.5 5.4 2.6 13.2l7.8 6.1C12.2 13.3 17.6 9.5 24 9.5z" }),
627
+ /* @__PURE__ */ jsx2("path", { fill: "#4285F4", d: "M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.5-4.9 7.2l7.6 5.9c4.4-4.1 7.1-10.1 7.1-17.6z" }),
628
+ /* @__PURE__ */ jsx2("path", { fill: "#FBBC05", d: "M10.4 28.3c-.5-1.5-.8-3.1-.8-4.8s.3-3.3.8-4.8l-7.8-6.1C.9 15.9 0 19.8 0 23.5s.9 7.6 2.6 10.9l7.8-6.1z" }),
629
+ /* @__PURE__ */ jsx2("path", { fill: "#34A853", d: "M24 47c6.2 0 11.5-2 15.3-5.5l-7.6-5.9c-2.1 1.4-4.8 2.3-7.7 2.3-6.4 0-11.8-3.8-13.6-9.3l-7.8 6.1C6.5 41.6 14.6 47 24 47z" })
630
+ ] })
631
+ },
632
+ github: {
633
+ name: "GitHub",
634
+ icon: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "M12 .5A11.5 11.5 0 0 0 .5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.53-1.34-1.3-1.7-1.3-1.7-1.06-.72.08-.71.08-.71 1.17.08 1.79 1.2 1.79 1.2 1.04 1.78 2.73 1.27 3.4.97.1-.76.41-1.27.74-1.56-2.55-.29-5.24-1.28-5.24-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.8 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.41-2.69 5.38-5.25 5.67.42.36.8 1.08.8 2.18v3.23c0 .31.21.67.8.56A11.5 11.5 0 0 0 23.5 12 11.5 11.5 0 0 0 12 .5z" }) })
635
+ },
636
+ microsoft: {
637
+ name: "Microsoft",
638
+ icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 23 23", "aria-hidden": true, children: [
639
+ /* @__PURE__ */ jsx2("path", { fill: "#F25022", d: "M1 1h10v10H1z" }),
640
+ /* @__PURE__ */ jsx2("path", { fill: "#7FBA00", d: "M12 1h10v10H12z" }),
641
+ /* @__PURE__ */ jsx2("path", { fill: "#00A4EF", d: "M1 12h10v10H1z" }),
642
+ /* @__PURE__ */ jsx2("path", { fill: "#FFB900", d: "M12 12h10v10H12z" })
643
+ ] })
644
+ },
645
+ facebook: {
646
+ name: "Facebook",
647
+ icon: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "#1877F2", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.95.93-1.95 1.89v2.26h3.32l-.53 3.49h-2.79V24C19.61 23.1 24 18.1 24 12.07z" }) })
648
+ },
649
+ twitter: {
650
+ name: "X",
651
+ icon: /* @__PURE__ */ jsx2("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "M18.9 1.15h3.68l-8.04 9.19L24 22.85h-7.41l-5.8-7.58-6.64 7.58H.46l8.6-9.83L0 1.15h7.6l5.24 6.93 6.06-6.93zm-1.29 19.5h2.04L6.49 3.24H4.3l13.31 17.41z" }) })
652
+ }
653
+ };
654
+ function SocialButtons(props) {
655
+ const { auth, status } = useOptare();
656
+ const { social } = useAuthMethods();
657
+ const [pending, setPending] = useState3(null);
658
+ const providers = (props.only ?? social).filter((p) => social.includes(p));
659
+ if (providers.length === 0) return null;
660
+ const verb = props.label ?? "Continue with";
661
+ const nameOnly = providers.length > 1;
662
+ const ready = status === "ready" && !!auth;
663
+ async function go(provider) {
664
+ if (!auth) return;
665
+ setPending(provider);
666
+ try {
667
+ await auth.signIn.social({
668
+ provider,
669
+ callbackURL: props.callbackURL ?? (typeof window !== "undefined" ? window.location.href : void 0),
670
+ errorCallbackURL: props.errorCallbackURL
671
+ });
672
+ } catch {
673
+ setPending(null);
674
+ }
675
+ }
676
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
677
+ props.divider !== false ? /* @__PURE__ */ jsx2(Divider, { children: props.dividerLabel ?? "or" }) : null,
678
+ /* @__PURE__ */ jsx2(
679
+ "div",
680
+ {
681
+ style: {
682
+ display: "grid",
683
+ gap: 8,
684
+ gridTemplateColumns: providers.length > 1 ? "1fr 1fr" : "1fr"
685
+ },
686
+ children: providers.map((p) => /* @__PURE__ */ jsxs2(
687
+ Button,
688
+ {
689
+ type: "button",
690
+ variant: "secondary",
691
+ busy: pending === p,
692
+ disabled: !ready || pending !== null && pending !== p,
693
+ onClick: () => go(p),
694
+ style: { fontWeight: 550 },
695
+ children: [
696
+ pending === p ? null : MARKS[p].icon,
697
+ /* @__PURE__ */ jsx2("span", { children: nameOnly ? MARKS[p].name : `${verb} ${MARKS[p].name}` })
698
+ ]
699
+ },
700
+ p
701
+ ))
702
+ }
703
+ )
704
+ ] });
705
+ }
706
+
707
+ // src/components/SignIn.tsx
708
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
709
+ function SignIn(props) {
710
+ const { auth, status } = useOptare();
711
+ const methods = useAuthMethods();
712
+ const allowMagicLink = (props.allowMagicLink ?? methods.magicLink) && methods.magicLink;
713
+ const allowEmailOtp = (props.allowEmailOtp ?? methods.emailOtp) && methods.emailOtp;
714
+ const allowSocial = props.allowSocial ?? true;
715
+ const allowSso = (props.allowSso ?? methods.sso) && methods.sso;
716
+ const callbackURL = props.callbackURL ?? (typeof window !== "undefined" ? window.location.href : void 0);
717
+ const [mode, setMode] = useState4("password");
718
+ const [email, setEmail] = useState4("");
719
+ const [password, setPassword] = useState4("");
720
+ const [otp, setOtp] = useState4("");
721
+ const [otpSent, setOtpSent] = useState4(false);
722
+ const [fieldErrors, setFieldErrors] = useState4({});
723
+ const [formError, setFormError] = useState4(null);
724
+ const [busy, setBusy] = useState4(false);
725
+ const [magicLinkSent, setMagicLinkSent] = useState4(false);
726
+ const ready = status === "ready" && !!auth;
727
+ function resetTransient() {
728
+ setFormError(null);
729
+ setFieldErrors({});
730
+ }
731
+ function switchMode(next) {
732
+ setMode(next);
733
+ setOtp("");
734
+ setOtpSent(false);
735
+ resetTransient();
736
+ }
737
+ async function onSubmit(e) {
738
+ e.preventDefault();
739
+ if (!auth) return;
740
+ setFormError(null);
741
+ if (mode === "sso") {
742
+ const errs2 = isValidEmail(email) ? {} : { email: "Enter your work email address" };
743
+ setFieldErrors(errs2);
744
+ if (Object.keys(errs2).length) return;
745
+ setBusy(true);
746
+ try {
747
+ const res = await auth.signIn.sso({
748
+ email,
749
+ callbackURL
750
+ });
751
+ if (res?.error) {
752
+ setFormError(
753
+ res.error.message ?? "We couldn't find a single sign-on provider for that email domain."
754
+ );
755
+ }
756
+ } catch (err) {
757
+ setFormError(err instanceof Error ? err.message : "SSO sign-in failed");
758
+ } finally {
759
+ setBusy(false);
760
+ }
761
+ return;
762
+ }
763
+ if (mode === "magic-link") {
764
+ const errs2 = isValidEmail(email) ? {} : { email: "Enter a valid email address" };
765
+ setFieldErrors(errs2);
766
+ if (Object.keys(errs2).length) return;
767
+ setBusy(true);
768
+ try {
769
+ const res = await auth.signIn.magicLink({ email, callbackURL });
770
+ if (res.error) {
771
+ setFormError(res.error.message ?? "Could not send the link");
772
+ } else {
773
+ setMagicLinkSent(true);
774
+ }
775
+ } catch (err) {
776
+ setFormError(err instanceof Error ? err.message : "Could not send the link");
777
+ } finally {
778
+ setBusy(false);
779
+ }
780
+ return;
781
+ }
782
+ if (mode === "email-otp") {
783
+ if (!otpSent) {
784
+ const errs3 = isValidEmail(email) ? {} : { email: "Enter a valid email address" };
785
+ setFieldErrors(errs3);
786
+ if (Object.keys(errs3).length) return;
787
+ setBusy(true);
788
+ try {
789
+ const res = await auth.emailOtp.sendVerificationOtp({ email, type: "sign-in" });
790
+ if (res.error) {
791
+ setFormError(res.error.message ?? "Could not send the code");
792
+ } else {
793
+ setOtpSent(true);
794
+ }
795
+ } catch (err) {
796
+ setFormError(err instanceof Error ? err.message : "Could not send the code");
797
+ } finally {
798
+ setBusy(false);
799
+ }
800
+ return;
801
+ }
802
+ const errs2 = otp.trim().length >= 4 ? {} : { otp: "Enter the code from your email" };
803
+ setFieldErrors(errs2);
804
+ if (Object.keys(errs2).length) return;
805
+ setBusy(true);
806
+ try {
807
+ const res = await auth.signIn.emailOtp({ email, otp: otp.trim() });
808
+ if (res.error) {
809
+ setFormError(res.error.message ?? "That code didn't work");
810
+ return;
811
+ }
812
+ if (res.data?.twoFactorRedirect) return props.onTwoFactor?.();
813
+ notifySessionChanged();
814
+ if (props.onSuccess) props.onSuccess();
815
+ else if (props.redirectTo && typeof window !== "undefined")
816
+ window.location.assign(props.redirectTo);
817
+ } catch (err) {
818
+ setFormError(err instanceof Error ? err.message : "Sign-in failed");
819
+ } finally {
820
+ setBusy(false);
821
+ }
822
+ return;
823
+ }
824
+ const errs = validateSignIn({ email, password });
825
+ setFieldErrors(errs);
826
+ if (Object.keys(errs).length) return;
827
+ setBusy(true);
828
+ try {
829
+ const res = await auth.signIn.email({
830
+ email,
831
+ password,
832
+ callbackURL
833
+ });
834
+ if (res.error) {
835
+ setFormError(res.error.message ?? "Incorrect email or password");
836
+ return;
837
+ }
838
+ if (res.data?.twoFactorRedirect) {
839
+ props.onTwoFactor?.();
840
+ if (!props.onTwoFactor) setFormError("Two-factor authentication is required to continue.");
841
+ return;
842
+ }
843
+ notifySessionChanged();
844
+ if (props.onSuccess) props.onSuccess();
845
+ else if (props.redirectTo && typeof window !== "undefined")
846
+ window.location.assign(props.redirectTo);
847
+ } catch (err) {
848
+ setFormError(err instanceof Error ? err.message : "Sign-in failed");
849
+ } finally {
850
+ setBusy(false);
851
+ }
852
+ }
853
+ if (magicLinkSent) {
854
+ return /* @__PURE__ */ jsxs3(Card, { children: [
855
+ /* @__PURE__ */ jsx3(BrandHeader, { title: "Check your email", subtitle: `We sent a sign-in link to ${email}.` }),
856
+ /* @__PURE__ */ jsx3(Footer, { children: /* @__PURE__ */ jsx3(LinkButton, { onClick: () => {
857
+ setMagicLinkSent(false);
858
+ switchMode("password");
859
+ }, children: "Back to sign in" }) })
860
+ ] });
861
+ }
862
+ const submitLabel = busy ? "Please wait\u2026" : mode === "magic-link" ? "Email me a link" : mode === "email-otp" ? otpSent ? "Sign in" : "Email me a code" : mode === "sso" ? "Continue with SSO" : "Sign in";
863
+ return /* @__PURE__ */ jsxs3(Card, { children: [
864
+ /* @__PURE__ */ jsx3(BrandHeader, { title: props.title ?? "Sign in", subtitle: props.subtitle }),
865
+ allowSocial ? /* @__PURE__ */ jsx3(SocialButtons, { callbackURL, divider: false }) : null,
866
+ allowSocial && methods.social.length > 0 ? /* @__PURE__ */ jsx3(Divider, { children: "or" }) : null,
867
+ /* @__PURE__ */ jsxs3("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
868
+ formError ? /* @__PURE__ */ jsx3(Alert, { children: formError }) : null,
869
+ mode === "email-otp" && otpSent ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
870
+ /* @__PURE__ */ jsxs3(Notice, { children: [
871
+ "Enter the 6-digit code we emailed to ",
872
+ email,
873
+ "."
874
+ ] }),
875
+ /* @__PURE__ */ jsx3(
876
+ TextInput,
877
+ {
878
+ label: "Code",
879
+ inputMode: "numeric",
880
+ autoComplete: "one-time-code",
881
+ value: otp,
882
+ onChange: (e) => setOtp(e.currentTarget.value),
883
+ error: fieldErrors.otp,
884
+ disabled: busy
885
+ }
886
+ )
887
+ ] }) : /* @__PURE__ */ jsxs3(Fragment3, { children: [
888
+ mode === "sso" ? /* @__PURE__ */ jsx3(Notice, { children: "Sign in through your organization's identity provider." }) : null,
889
+ /* @__PURE__ */ jsx3(
890
+ TextInput,
891
+ {
892
+ label: mode === "sso" ? "Work email" : "Email",
893
+ type: "email",
894
+ autoComplete: "email",
895
+ value: email,
896
+ onChange: (e) => setEmail(e.currentTarget.value),
897
+ error: fieldErrors.email,
898
+ disabled: busy
899
+ }
900
+ ),
901
+ mode === "password" ? /* @__PURE__ */ jsx3(
902
+ TextInput,
903
+ {
904
+ label: "Password",
905
+ type: "password",
906
+ autoComplete: "current-password",
907
+ value: password,
908
+ onChange: (e) => setPassword(e.currentTarget.value),
909
+ error: fieldErrors.password,
910
+ disabled: busy
911
+ }
912
+ ) : null
913
+ ] }),
914
+ /* @__PURE__ */ jsx3(Button, { type: "submit", busy, disabled: !ready, children: submitLabel }),
915
+ mode === "password" && props.onForgotPassword ? /* @__PURE__ */ jsx3(Footer, { children: /* @__PURE__ */ jsx3(LinkButton, { onClick: props.onForgotPassword, children: "Forgot password?" }) }) : null,
916
+ allowMagicLink || allowEmailOtp || allowSso ? /* @__PURE__ */ jsx3(Footer, { children: /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap" }, children: [
917
+ mode !== "password" ? /* @__PURE__ */ jsx3(LinkButton, { onClick: () => switchMode("password"), children: "Use a password" }) : null,
918
+ allowMagicLink && mode !== "magic-link" ? /* @__PURE__ */ jsx3(LinkButton, { onClick: () => switchMode("magic-link"), children: "Email me a link" }) : null,
919
+ allowEmailOtp && mode !== "email-otp" ? /* @__PURE__ */ jsx3(LinkButton, { onClick: () => switchMode("email-otp"), children: "Email me a code" }) : null,
920
+ allowSso && mode !== "sso" ? /* @__PURE__ */ jsx3(LinkButton, { onClick: () => switchMode("sso"), children: "Single sign-on" }) : null
921
+ ] }) }) : null
922
+ ] }),
923
+ props.footer ? /* @__PURE__ */ jsx3(Footer, { children: props.footer }) : null
924
+ ] });
925
+ }
926
+
927
+ // src/components/SignUp.tsx
928
+ import { useState as useState5 } from "react";
929
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
930
+ function SignUp(props) {
931
+ const passwordMin = props.passwordMinLength ?? 8;
932
+ const { auth, status } = useOptare();
933
+ const methods = useAuthMethods();
934
+ const allowSocial = props.allowSocial ?? true;
935
+ const callbackURL = props.callbackURL ?? (typeof window !== "undefined" ? window.location.href : void 0);
936
+ const [name, setName] = useState5("");
937
+ const [email, setEmail] = useState5("");
938
+ const [password, setPassword] = useState5("");
939
+ const [fieldErrors, setFieldErrors] = useState5({});
940
+ const [formError, setFormError] = useState5(null);
941
+ const [busy, setBusy] = useState5(false);
942
+ const ready = status === "ready" && !!auth;
943
+ const pwCheck = checkPassword(password, passwordMin);
944
+ async function onSubmit(e) {
945
+ e.preventDefault();
946
+ if (!auth) return;
947
+ setFormError(null);
948
+ const errs = validateSignUp({ name, email, password }, passwordMin);
949
+ setFieldErrors(errs);
950
+ if (Object.keys(errs).length) return;
951
+ setBusy(true);
952
+ try {
953
+ const res = await auth.signUp.email({
954
+ name,
955
+ email,
956
+ password,
957
+ callbackURL
958
+ });
959
+ if (res.error) {
960
+ setFormError(res.error.message ?? "Could not create your account");
961
+ return;
962
+ }
963
+ if (res.data && !res.data.token) {
964
+ props.onVerificationRequired?.(email);
965
+ if (!props.onVerificationRequired)
966
+ setFormError("Check your email to confirm your address, then sign in.");
967
+ return;
968
+ }
969
+ notifySessionChanged();
970
+ if (props.onSuccess) props.onSuccess();
971
+ else if (props.redirectTo && typeof window !== "undefined")
972
+ window.location.assign(props.redirectTo);
973
+ } catch (err) {
974
+ setFormError(err instanceof Error ? err.message : "Sign-up failed");
975
+ } finally {
976
+ setBusy(false);
977
+ }
978
+ }
979
+ return /* @__PURE__ */ jsxs4(Card, { children: [
980
+ /* @__PURE__ */ jsx4(BrandHeader, { title: props.title ?? "Create your account", subtitle: props.subtitle }),
981
+ allowSocial ? /* @__PURE__ */ jsx4(SocialButtons, { label: "Sign up with", callbackURL, divider: false }) : null,
982
+ allowSocial && methods.social.length > 0 ? /* @__PURE__ */ jsx4(Divider, { children: "or" }) : null,
983
+ /* @__PURE__ */ jsxs4("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
984
+ formError ? /* @__PURE__ */ jsx4(Alert, { children: formError }) : null,
985
+ /* @__PURE__ */ jsx4(
986
+ TextInput,
987
+ {
988
+ label: "Name",
989
+ autoComplete: "name",
990
+ value: name,
991
+ onChange: (e) => setName(e.currentTarget.value),
992
+ error: fieldErrors.name,
993
+ disabled: busy
994
+ }
995
+ ),
996
+ /* @__PURE__ */ jsx4(
997
+ TextInput,
998
+ {
999
+ label: "Email",
1000
+ type: "email",
1001
+ autoComplete: "email",
1002
+ value: email,
1003
+ onChange: (e) => setEmail(e.currentTarget.value),
1004
+ error: fieldErrors.email,
1005
+ disabled: busy
1006
+ }
1007
+ ),
1008
+ /* @__PURE__ */ jsx4(
1009
+ TextInput,
1010
+ {
1011
+ label: "Password",
1012
+ type: "password",
1013
+ autoComplete: "new-password",
1014
+ value: password,
1015
+ onChange: (e) => setPassword(e.currentTarget.value),
1016
+ error: fieldErrors.password,
1017
+ disabled: busy
1018
+ }
1019
+ ),
1020
+ password && !fieldErrors.password ? /* @__PURE__ */ jsx4(
1021
+ "div",
1022
+ {
1023
+ "aria-hidden": true,
1024
+ style: {
1025
+ height: 4,
1026
+ borderRadius: 2,
1027
+ background: "var(--optare-border)",
1028
+ overflow: "hidden"
1029
+ },
1030
+ children: /* @__PURE__ */ jsx4(
1031
+ "div",
1032
+ {
1033
+ style: {
1034
+ height: "100%",
1035
+ width: `${pwCheck.score / 4 * 100}%`,
1036
+ background: pwCheck.score >= 3 ? "var(--optare-primary)" : "var(--optare-muted)",
1037
+ transition: "width 150ms"
1038
+ }
1039
+ }
1040
+ )
1041
+ }
1042
+ ) : null,
1043
+ /* @__PURE__ */ jsx4(Button, { type: "submit", busy, disabled: !ready, children: busy ? "Creating account\u2026" : "Sign up" })
1044
+ ] }),
1045
+ props.footer ? /* @__PURE__ */ jsx4(Footer, { children: props.footer }) : null
1046
+ ] });
1047
+ }
1048
+
1049
+ // src/components/AccountButton.tsx
1050
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState6 } from "react";
1051
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1052
+ function initials(nameOrEmail) {
1053
+ const parts = nameOrEmail.trim().split(/\s+/);
1054
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
1055
+ return nameOrEmail.slice(0, 2).toUpperCase();
1056
+ }
1057
+ var s = {
1058
+ wrap: { position: "relative", display: "inline-block", fontFamily: "var(--optare-font)" },
1059
+ trigger: {
1060
+ display: "flex",
1061
+ alignItems: "center",
1062
+ gap: 8,
1063
+ padding: "6px 10px",
1064
+ background: "var(--optare-bg)",
1065
+ color: "var(--optare-fg)",
1066
+ border: "1px solid var(--optare-border)",
1067
+ borderRadius: "var(--optare-radius)",
1068
+ cursor: "pointer",
1069
+ font: "inherit",
1070
+ fontSize: 14
1071
+ },
1072
+ avatar: {
1073
+ width: 28,
1074
+ height: 28,
1075
+ borderRadius: "50%",
1076
+ background: "var(--optare-primary)",
1077
+ color: "var(--optare-primary-text)",
1078
+ display: "flex",
1079
+ alignItems: "center",
1080
+ justifyContent: "center",
1081
+ fontSize: 12,
1082
+ fontWeight: 700,
1083
+ objectFit: "cover",
1084
+ flexShrink: 0
1085
+ },
1086
+ menu: {
1087
+ position: "absolute",
1088
+ right: 0,
1089
+ marginTop: 6,
1090
+ minWidth: 200,
1091
+ background: "var(--optare-bg)",
1092
+ color: "var(--optare-fg)",
1093
+ border: "1px solid var(--optare-border)",
1094
+ borderRadius: "var(--optare-radius)",
1095
+ boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
1096
+ padding: 6,
1097
+ zIndex: 50
1098
+ },
1099
+ meta: { padding: "8px 10px", borderBottom: "1px solid var(--optare-border)", marginBottom: 4 },
1100
+ name: { fontSize: 14, fontWeight: 600 },
1101
+ email: { fontSize: 12, color: "var(--optare-muted)" },
1102
+ item: {
1103
+ display: "block",
1104
+ width: "100%",
1105
+ textAlign: "left",
1106
+ padding: "8px 10px",
1107
+ background: "none",
1108
+ border: "none",
1109
+ borderRadius: "calc(var(--optare-radius) - 2px)",
1110
+ font: "inherit",
1111
+ fontSize: 13,
1112
+ color: "inherit",
1113
+ cursor: "pointer"
1114
+ }
1115
+ };
1116
+ function AccountButton(props) {
1117
+ const { cssVars } = useOptare();
1118
+ const { user, isPending, isAuthenticated } = useSession();
1119
+ const { signOut, isPending: signingOut } = useSignOut();
1120
+ const [open, setOpen] = useState6(false);
1121
+ const wrapRef = useRef2(null);
1122
+ useEffect3(() => {
1123
+ if (!open) return;
1124
+ const onDoc = (e) => {
1125
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
1126
+ };
1127
+ const onEsc = (e) => e.key === "Escape" && setOpen(false);
1128
+ document.addEventListener("mousedown", onDoc);
1129
+ document.addEventListener("keydown", onEsc);
1130
+ return () => {
1131
+ document.removeEventListener("mousedown", onDoc);
1132
+ document.removeEventListener("keydown", onEsc);
1133
+ };
1134
+ }, [open]);
1135
+ if (isPending) return null;
1136
+ if (!isAuthenticated || !user) return /* @__PURE__ */ jsx5(Fragment4, { children: props.signInSlot ?? null });
1137
+ const u = user;
1138
+ const display = u.name || u.email || "Account";
1139
+ return /* @__PURE__ */ jsxs5("div", { ref: wrapRef, style: { ...cssVars, ...s.wrap }, children: [
1140
+ /* @__PURE__ */ jsxs5(
1141
+ "button",
1142
+ {
1143
+ type: "button",
1144
+ style: s.trigger,
1145
+ "aria-haspopup": "menu",
1146
+ "aria-expanded": open,
1147
+ onClick: () => setOpen((o) => !o),
1148
+ children: [
1149
+ u.image ? /* @__PURE__ */ jsx5("img", { src: u.image, alt: "", style: s.avatar }) : /* @__PURE__ */ jsx5("span", { style: s.avatar, children: initials(display) }),
1150
+ !props.compact ? /* @__PURE__ */ jsx5("span", { children: display }) : null
1151
+ ]
1152
+ }
1153
+ ),
1154
+ open ? /* @__PURE__ */ jsxs5("div", { role: "menu", style: s.menu, children: [
1155
+ /* @__PURE__ */ jsxs5("div", { style: s.meta, children: [
1156
+ /* @__PURE__ */ jsx5("div", { style: s.name, children: u.name || "\u2014" }),
1157
+ u.email ? /* @__PURE__ */ jsx5("div", { style: s.email, children: u.email }) : null
1158
+ ] }),
1159
+ props.menuItems,
1160
+ /* @__PURE__ */ jsx5(
1161
+ "button",
1162
+ {
1163
+ type: "button",
1164
+ role: "menuitem",
1165
+ style: s.item,
1166
+ disabled: signingOut,
1167
+ onClick: async () => {
1168
+ await signOut();
1169
+ setOpen(false);
1170
+ props.onSignedOut?.();
1171
+ },
1172
+ children: signingOut ? "Signing out\u2026" : "Sign out"
1173
+ }
1174
+ )
1175
+ ] }) : null
1176
+ ] });
1177
+ }
1178
+
1179
+ // src/components/ForgotPassword.tsx
1180
+ import { useState as useState7 } from "react";
1181
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1182
+ function ForgotPassword(props) {
1183
+ const { auth, status } = useOptare();
1184
+ const [email, setEmail] = useState7("");
1185
+ const [fieldError, setFieldError] = useState7();
1186
+ const [formError, setFormError] = useState7(null);
1187
+ const [busy, setBusy] = useState7(false);
1188
+ const [sent, setSent] = useState7(false);
1189
+ const ready = status === "ready" && !!auth;
1190
+ async function onSubmit(e) {
1191
+ e.preventDefault();
1192
+ if (!auth) return;
1193
+ if (!isValidEmail(email)) {
1194
+ setFieldError("Enter a valid email address");
1195
+ return;
1196
+ }
1197
+ setFieldError(void 0);
1198
+ setFormError(null);
1199
+ setBusy(true);
1200
+ try {
1201
+ const res = await auth.forgetPassword({
1202
+ email,
1203
+ // Absolute by default — the reset link is built on the Optare origin, so
1204
+ // a relative path would drop the user there instead of on this app.
1205
+ redirectTo: props.redirectTo ?? (typeof window !== "undefined" ? `${window.location.origin}/reset-password` : "/reset-password")
1206
+ });
1207
+ if (res?.error && res.error.message && /rate|too many/i.test(res.error.message)) {
1208
+ setFormError(res.error.message);
1209
+ } else {
1210
+ setSent(true);
1211
+ }
1212
+ } catch (err) {
1213
+ setFormError(err instanceof Error ? err.message : "Could not send the email");
1214
+ } finally {
1215
+ setBusy(false);
1216
+ }
1217
+ }
1218
+ if (sent) {
1219
+ return /* @__PURE__ */ jsxs6(Card, { children: [
1220
+ /* @__PURE__ */ jsx6(
1221
+ BrandHeader,
1222
+ {
1223
+ title: "Check your email",
1224
+ subtitle: `If an account exists for ${email}, a reset link is on its way.`
1225
+ }
1226
+ ),
1227
+ props.onBack ? /* @__PURE__ */ jsx6(Footer, { children: /* @__PURE__ */ jsx6(LinkButton, { onClick: props.onBack, children: "Back to sign in" }) }) : null
1228
+ ] });
1229
+ }
1230
+ return /* @__PURE__ */ jsxs6(Card, { children: [
1231
+ /* @__PURE__ */ jsx6(
1232
+ BrandHeader,
1233
+ {
1234
+ title: props.title ?? "Reset your password",
1235
+ subtitle: props.subtitle ?? "We'll email you a link to set a new one."
1236
+ }
1237
+ ),
1238
+ /* @__PURE__ */ jsxs6("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
1239
+ formError ? /* @__PURE__ */ jsx6(Alert, { children: formError }) : null,
1240
+ /* @__PURE__ */ jsx6(
1241
+ TextInput,
1242
+ {
1243
+ label: "Email",
1244
+ type: "email",
1245
+ autoComplete: "email",
1246
+ value: email,
1247
+ onChange: (e) => setEmail(e.currentTarget.value),
1248
+ error: fieldError,
1249
+ disabled: busy
1250
+ }
1251
+ ),
1252
+ /* @__PURE__ */ jsx6(Button, { type: "submit", busy, disabled: !ready, children: busy ? "Sending\u2026" : "Send reset link" }),
1253
+ props.onBack ? /* @__PURE__ */ jsx6(Footer, { children: /* @__PURE__ */ jsx6(LinkButton, { onClick: props.onBack, children: "Back to sign in" }) }) : null
1254
+ ] }),
1255
+ props.footer ? /* @__PURE__ */ jsx6(Footer, { children: props.footer }) : null
1256
+ ] });
1257
+ }
1258
+
1259
+ // src/components/ResetPassword.tsx
1260
+ import { useEffect as useEffect4, useState as useState8 } from "react";
1261
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1262
+ function tokenFromUrl() {
1263
+ if (typeof window === "undefined") return null;
1264
+ const q = new URLSearchParams(window.location.search);
1265
+ return q.get("token") || q.get("t") || null;
1266
+ }
1267
+ function ResetPassword(props) {
1268
+ const { auth, status } = useOptare();
1269
+ const min = props.passwordMinLength ?? 12;
1270
+ const [token, setToken] = useState8(props.token ?? null);
1271
+ const [password, setPassword] = useState8("");
1272
+ const [confirm, setConfirm] = useState8("");
1273
+ const [fieldErrors, setFieldErrors] = useState8({});
1274
+ const [formError, setFormError] = useState8(null);
1275
+ const [busy, setBusy] = useState8(false);
1276
+ const [done, setDone] = useState8(false);
1277
+ useEffect4(() => {
1278
+ if (!props.token) setToken(tokenFromUrl());
1279
+ }, [props.token]);
1280
+ const ready = status === "ready" && !!auth;
1281
+ const pw = checkPassword(password, min);
1282
+ async function onSubmit(e) {
1283
+ e.preventDefault();
1284
+ if (!auth || !token) return;
1285
+ const errs = {};
1286
+ if (!pw.ok && pw.message) errs.password = pw.message;
1287
+ if (confirm !== password) errs.confirm = "Passwords don't match";
1288
+ setFieldErrors(errs);
1289
+ if (Object.keys(errs).length) return;
1290
+ setFormError(null);
1291
+ setBusy(true);
1292
+ try {
1293
+ const res = await auth.resetPassword({ newPassword: password, token });
1294
+ if (res?.error) {
1295
+ setFormError(res.error.message ?? "That reset link is no longer valid");
1296
+ return;
1297
+ }
1298
+ setDone(true);
1299
+ if (props.onSuccess) props.onSuccess();
1300
+ else if (props.redirectTo && typeof window !== "undefined")
1301
+ window.location.assign(props.redirectTo);
1302
+ } catch (err) {
1303
+ setFormError(err instanceof Error ? err.message : "Could not reset the password");
1304
+ } finally {
1305
+ setBusy(false);
1306
+ }
1307
+ }
1308
+ if (done) {
1309
+ return /* @__PURE__ */ jsxs7(Card, { children: [
1310
+ /* @__PURE__ */ jsx7(BrandHeader, { title: "Password updated", subtitle: "You can sign in with your new password now." }),
1311
+ props.onBack ? /* @__PURE__ */ jsx7(Footer, { children: /* @__PURE__ */ jsx7(LinkButton, { onClick: props.onBack, children: "Go to sign in" }) }) : null
1312
+ ] });
1313
+ }
1314
+ if (!token) {
1315
+ return /* @__PURE__ */ jsxs7(Card, { children: [
1316
+ /* @__PURE__ */ jsx7(
1317
+ BrandHeader,
1318
+ {
1319
+ title: "Link expired",
1320
+ subtitle: "This reset link is missing or has already been used. Request a new one."
1321
+ }
1322
+ ),
1323
+ props.onBack ? /* @__PURE__ */ jsx7(Footer, { children: /* @__PURE__ */ jsx7(LinkButton, { onClick: props.onBack, children: "Back to sign in" }) }) : null
1324
+ ] });
1325
+ }
1326
+ return /* @__PURE__ */ jsxs7(Card, { children: [
1327
+ /* @__PURE__ */ jsx7(BrandHeader, { title: props.title ?? "Set a new password" }),
1328
+ /* @__PURE__ */ jsxs7("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
1329
+ formError ? /* @__PURE__ */ jsx7(Alert, { children: formError }) : null,
1330
+ /* @__PURE__ */ jsx7(
1331
+ TextInput,
1332
+ {
1333
+ label: "New password",
1334
+ type: "password",
1335
+ autoComplete: "new-password",
1336
+ value: password,
1337
+ onChange: (e) => setPassword(e.currentTarget.value),
1338
+ error: fieldErrors.password,
1339
+ disabled: busy
1340
+ }
1341
+ ),
1342
+ /* @__PURE__ */ jsx7(
1343
+ TextInput,
1344
+ {
1345
+ label: "Confirm password",
1346
+ type: "password",
1347
+ autoComplete: "new-password",
1348
+ value: confirm,
1349
+ onChange: (e) => setConfirm(e.currentTarget.value),
1350
+ error: fieldErrors.confirm,
1351
+ disabled: busy
1352
+ }
1353
+ ),
1354
+ /* @__PURE__ */ jsx7(Button, { type: "submit", busy, disabled: !ready, children: busy ? "Saving\u2026" : "Update password" })
1355
+ ] }),
1356
+ props.footer ? /* @__PURE__ */ jsx7(Footer, { children: props.footer }) : null
1357
+ ] });
1358
+ }
1359
+
1360
+ // src/components/TwoFactorChallenge.tsx
1361
+ import { useState as useState9 } from "react";
1362
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1363
+ function TwoFactorChallenge(props) {
1364
+ const { auth, status } = useOptare();
1365
+ const allowBackup = props.allowBackupCode ?? true;
1366
+ const [useBackup, setUseBackup] = useState9(false);
1367
+ const [code, setCode] = useState9("");
1368
+ const [fieldError, setFieldError] = useState9();
1369
+ const [formError, setFormError] = useState9(null);
1370
+ const [busy, setBusy] = useState9(false);
1371
+ const ready = status === "ready" && !!auth;
1372
+ async function onSubmit(e) {
1373
+ e.preventDefault();
1374
+ if (!auth) return;
1375
+ const trimmed = code.trim().replace(/\s+/g, "");
1376
+ if (trimmed.length < (useBackup ? 8 : 6)) {
1377
+ setFieldError(useBackup ? "Enter a full backup code" : "Enter the 6-digit code");
1378
+ return;
1379
+ }
1380
+ setFieldError(void 0);
1381
+ setFormError(null);
1382
+ setBusy(true);
1383
+ try {
1384
+ const call = useBackup ? auth.twoFactor.verifyBackupCode({ code: trimmed }) : auth.twoFactor.verifyTotp({ code: trimmed });
1385
+ const res = await call;
1386
+ if (res?.error) {
1387
+ setFormError(res.error.message ?? "That code didn't work");
1388
+ return;
1389
+ }
1390
+ notifySessionChanged();
1391
+ if (props.onSuccess) props.onSuccess();
1392
+ else if (props.redirectTo && typeof window !== "undefined")
1393
+ window.location.assign(props.redirectTo);
1394
+ } catch (err) {
1395
+ setFormError(err instanceof Error ? err.message : "Verification failed");
1396
+ } finally {
1397
+ setBusy(false);
1398
+ }
1399
+ }
1400
+ return /* @__PURE__ */ jsxs8(Card, { children: [
1401
+ /* @__PURE__ */ jsx8(
1402
+ BrandHeader,
1403
+ {
1404
+ title: props.title ?? "Two-factor authentication",
1405
+ subtitle: props.subtitle ?? (useBackup ? "Enter one of your saved backup codes." : "Enter the code from your authenticator app.")
1406
+ }
1407
+ ),
1408
+ /* @__PURE__ */ jsxs8("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
1409
+ formError ? /* @__PURE__ */ jsx8(Alert, { children: formError }) : null,
1410
+ /* @__PURE__ */ jsx8(
1411
+ TextInput,
1412
+ {
1413
+ label: useBackup ? "Backup code" : "Authentication code",
1414
+ inputMode: useBackup ? "text" : "numeric",
1415
+ autoComplete: "one-time-code",
1416
+ value: code,
1417
+ onChange: (e) => setCode(e.currentTarget.value),
1418
+ error: fieldError,
1419
+ disabled: busy
1420
+ }
1421
+ ),
1422
+ /* @__PURE__ */ jsx8(Button, { type: "submit", busy, disabled: !ready, children: busy ? "Verifying\u2026" : "Verify" }),
1423
+ /* @__PURE__ */ jsx8(Footer, { children: /* @__PURE__ */ jsxs8("div", { style: { display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap" }, children: [
1424
+ allowBackup ? /* @__PURE__ */ jsx8(LinkButton, { onClick: () => {
1425
+ setUseBackup((b) => !b);
1426
+ setCode("");
1427
+ setFieldError(void 0);
1428
+ }, children: useBackup ? "Use an authenticator code" : "Use a backup code" }) : null,
1429
+ props.onCancel ? /* @__PURE__ */ jsx8(LinkButton, { onClick: props.onCancel, children: "Cancel" }) : null
1430
+ ] }) })
1431
+ ] }),
1432
+ props.footer ? /* @__PURE__ */ jsx8(Footer, { children: props.footer }) : null
1433
+ ] });
1434
+ }
1435
+
1436
+ // src/index.ts
1437
+ import {
1438
+ resolveOptareConfig as resolveOptareConfig2,
1439
+ OptareConfigError as OptareConfigError2,
1440
+ DEFAULT_OPTARE_ORIGIN,
1441
+ DEFAULT_AUTH_METHODS as DEFAULT_AUTH_METHODS2
1442
+ } from "@optare/client";
1443
+ export {
1444
+ AccountButton,
1445
+ DEFAULT_AUTH_METHODS2 as DEFAULT_AUTH_METHODS,
1446
+ DEFAULT_OPTARE_ORIGIN,
1447
+ DEFAULT_PRIMARY,
1448
+ DEFAULT_RADIUS,
1449
+ ForgotPassword,
1450
+ OptareConfigError2 as OptareConfigError,
1451
+ OptareProvider,
1452
+ ResetPassword,
1453
+ SignIn,
1454
+ SignUp,
1455
+ SocialButtons,
1456
+ TwoFactorChallenge,
1457
+ brandingToCssVars,
1458
+ checkPassword,
1459
+ contrastText,
1460
+ createReactAuthClient,
1461
+ isValidEmail,
1462
+ notifySessionChanged,
1463
+ resolveOptareConfig2 as resolveOptareConfig,
1464
+ safeColor,
1465
+ safeLength,
1466
+ useActiveOrganization,
1467
+ useAuthMethods,
1468
+ useBranding,
1469
+ useOptare,
1470
+ useOptareAuth,
1471
+ useOptareConfig,
1472
+ useSession,
1473
+ useSignOut,
1474
+ useUser,
1475
+ validateSignIn,
1476
+ validateSignUp
1477
+ };