@elvix.is/sdk 0.5.6 → 0.5.7

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/react.js CHANGED
@@ -363,6 +363,88 @@ async function runPasskeySignIn(baseUrl, clientId) {
363
363
  return { ok: false, error: "network", message: e instanceof Error ? e.message : void 0 };
364
364
  }
365
365
  }
366
+ async function runPasskeyRegister(baseUrl, surface) {
367
+ if (typeof window === "undefined" || !window.PublicKeyCredential || !navigator.credentials?.create) {
368
+ return { ok: false, error: "passkey_unsupported", message: "This browser can't use passkeys." };
369
+ }
370
+ const init = authInit();
371
+ const reqInit = {
372
+ headers: { "content-type": "application/json", ...init.headers },
373
+ credentials: init.credentials
374
+ };
375
+ let options;
376
+ try {
377
+ const res = await fetch(`${baseUrl}/api/auth/passkey/register/start`, {
378
+ method: "POST",
379
+ ...reqInit,
380
+ body: JSON.stringify({ surface })
381
+ });
382
+ const body = await res.json();
383
+ if (!res.ok || !body.success || !body.data?.options) {
384
+ return { ok: false, error: body.errorMessage ?? "passkey_register_failed" };
385
+ }
386
+ options = body.data.options;
387
+ } catch (e) {
388
+ return { ok: false, error: "network", message: e instanceof Error ? e.message : void 0 };
389
+ }
390
+ let attestation;
391
+ try {
392
+ const publicKey = {
393
+ challenge: b64urlToBuf(options.challenge),
394
+ rp: options.rp,
395
+ user: {
396
+ id: b64urlToBuf(options.user.id),
397
+ name: options.user.name,
398
+ displayName: options.user.displayName
399
+ },
400
+ pubKeyCredParams: options.pubKeyCredParams,
401
+ timeout: options.timeout,
402
+ attestation: options.attestation,
403
+ authenticatorSelection: options.authenticatorSelection,
404
+ excludeCredentials: options.excludeCredentials?.map((c) => ({
405
+ id: b64urlToBuf(c.id),
406
+ type: c.type,
407
+ transports: c.transports
408
+ })),
409
+ extensions: options.extensions
410
+ };
411
+ const cred = await navigator.credentials.create({ publicKey });
412
+ if (!cred) return { ok: false, error: "passkey_cancelled" };
413
+ const resp = cred.response;
414
+ attestation = {
415
+ id: cred.id,
416
+ rawId: bufToB64url(cred.rawId),
417
+ type: "public-key",
418
+ clientExtensionResults: cred.getClientExtensionResults(),
419
+ authenticatorAttachment: cred.authenticatorAttachment ?? void 0,
420
+ response: {
421
+ clientDataJSON: bufToB64url(resp.clientDataJSON),
422
+ attestationObject: bufToB64url(resp.attestationObject),
423
+ transports: resp.getTransports?.() ?? void 0
424
+ }
425
+ };
426
+ } catch (e) {
427
+ const name = e?.name;
428
+ if (name === "NotAllowedError" || name === "AbortError") {
429
+ return { ok: false, error: "passkey_cancelled" };
430
+ }
431
+ return { ok: false, error: "passkey_register_failed", message: e instanceof Error ? e.message : void 0 };
432
+ }
433
+ try {
434
+ const res = await fetch(`${baseUrl}/api/auth/passkey/register/finish`, {
435
+ method: "POST",
436
+ ...reqInit,
437
+ body: JSON.stringify({ surface, response: attestation })
438
+ });
439
+ const body = await res.json();
440
+ if (!res.ok || !body.success) {
441
+ return { ok: false, error: body.errorMessage ?? "passkey_register_failed" };
442
+ }
443
+ return { ok: true };
444
+ } catch (e) {
445
+ return { ok: false, error: "network", message: e instanceof Error ? e.message : void 0 };
446
+ }
447
+ }
366
448
 
367
449
  // src/react/elvix-sign-in.tsx
368
450
  import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -554,552 +636,1693 @@ function ElvixSignIn({
554
636
  }
555
637
 
556
638
  // src/react/elvix-sign-in-form.tsx
557
- import { useMemo as useMemo2, useState as useState3 } from "react";
639
+ import { ArrowLeft, Check, Fingerprint, Loader2, X as X2 } from "lucide-react";
640
+ import { useCallback as useCallback3, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState5 } from "react";
558
641
 
559
- // src/react/elvix-shield.tsx
642
+ // src/react/elvix-logo.tsx
560
643
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
561
- function ElvixShield({
562
- size,
563
- fill,
564
- accent
565
- }) {
566
- return /* @__PURE__ */ jsxs3("svg", { width: size, height: size, viewBox: "2 2 20 20", "aria-hidden": true, style: { display: "block" }, children: [
567
- /* @__PURE__ */ jsx4(
568
- "path",
644
+ function ElvixLogo({ size = 22, withText = false, className }) {
645
+ const wordmarkSize = Math.round(size * 0.84);
646
+ return /* @__PURE__ */ jsxs3("div", { className: `inline-flex items-center gap-[7px] ${className ?? ""}`, "aria-label": "elvix", children: [
647
+ /* @__PURE__ */ jsxs3(
648
+ "svg",
569
649
  {
570
- fill,
571
- fillRule: "evenodd",
572
- d: "M 6 2.5 C 4.34 2.5 3 3.84 3 5.5 L 3 12.5 C 3 17.5 7 20.7 12 22 C 17 20.7 21 17.5 21 12.5 L 21 5.5 C 21 3.84 19.66 2.5 18 2.5 L 6 2.5 Z M 12 8.4 C 9.79 8.4 8 10.19 8 12.4 C 8 14.61 9.79 16.4 12 16.4 C 13.21 16.4 14.3 15.86 15.04 15 L 13.6 13.77 C 13.21 14.23 12.64 14.5 12 14.5 C 11.04 14.5 10.21 13.86 9.91 13 L 15.95 13 C 15.98 12.8 16 12.6 16 12.4 C 16 10.19 14.21 8.4 12 8.4 Z M 9.91 11.8 L 14.09 11.8 C 13.79 10.94 12.96 10.3 12 10.3 C 11.04 10.3 10.21 10.94 9.91 11.8 Z"
650
+ width: size,
651
+ height: size,
652
+ viewBox: "0 0 24 24",
653
+ "aria-hidden": "true",
654
+ style: { display: "block", overflow: "visible" },
655
+ children: [
656
+ /* @__PURE__ */ jsx4(
657
+ "path",
658
+ {
659
+ fill: "currentColor",
660
+ fillRule: "evenodd",
661
+ d: "\n M 6 2.5\n C 4.34 2.5 3 3.84 3 5.5\n L 3 12.5\n C 3 17.5 7 20.7 12 22\n C 17 20.7 21 17.5 21 12.5\n L 21 5.5\n C 21 3.84 19.66 2.5 18 2.5\n L 6 2.5\n Z\n\n M 12 8.4\n C 9.79 8.4 8 10.19 8 12.4\n C 8 14.61 9.79 16.4 12 16.4\n C 13.21 16.4 14.3 15.86 15.04 15\n L 13.6 13.77\n C 13.21 14.23 12.64 14.5 12 14.5\n C 11.04 14.5 10.21 13.86 9.91 13\n L 15.95 13\n C 15.98 12.8 16 12.6 16 12.4\n C 16 10.19 14.21 8.4 12 8.4\n Z\n\n M 9.91 11.8\n L 14.09 11.8\n C 13.79 10.94 12.96 10.3 12 10.3\n C 11.04 10.3 10.21 10.94 9.91 11.8\n Z\n "
662
+ }
663
+ ),
664
+ /* @__PURE__ */ jsx4("circle", { cx: "19.5", cy: "4.5", r: "2.4", fill: "#8e7dff" })
665
+ ]
573
666
  }
574
667
  ),
575
- /* @__PURE__ */ jsx4("circle", { cx: "19.5", cy: "4.5", r: "2.4", fill: accent })
668
+ withText && /* @__PURE__ */ jsx4(
669
+ "span",
670
+ {
671
+ style: { fontSize: wordmarkSize, lineHeight: 1, letterSpacing: "-0.025em" },
672
+ className: "font-semibold lowercase",
673
+ children: "elvix"
674
+ }
675
+ )
576
676
  ] });
577
677
  }
578
678
 
579
- // src/react/elvix-secured-badge.tsx
580
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
581
- var ELVIX_URL = "https://elvix.is";
582
- var SIZE = {
583
- sm: { height: 28, padX: 10, font: 11.5, icon: 14, gap: 6 },
584
- md: { height: 32, padX: 12, font: 12.5, icon: 16, gap: 7 },
585
- lg: { height: 36, padX: 14, font: 13, icon: 18, gap: 8 }
586
- };
587
- var TONE = {
588
- white: {
589
- light: { bg: "#ffffff", border: "#e4e4e7", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" },
590
- dark: { bg: "#ffffff", border: "transparent", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" }
591
- },
592
- dark: {
593
- light: { bg: "#0a0a0b", border: "rgba(0,0,0,0.1)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" },
594
- dark: { bg: "#0a0a0b", border: "rgba(255,255,255,0.1)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" }
595
- },
596
- outline: {
597
- light: { bg: "transparent", border: "rgba(0,0,0,0.15)", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" },
598
- dark: { bg: "transparent", border: "rgba(142,125,255,0.4)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" }
599
- }
600
- };
601
- function ElvixSecuredBadge({
602
- variant = "white",
603
- size = "md",
604
- theme = "dark",
605
- accentColor = "#8e7dff",
606
- href = ELVIX_URL,
607
- className = "",
608
- width,
609
- height,
610
- minWidth,
611
- maxWidth,
612
- minHeight,
613
- maxHeight
679
+ // src/react/elvix-recover-gate.tsx
680
+ import { AnimatePresence, motion } from "framer-motion";
681
+ import { CheckCircle2, EyeOff, LogOut, Undo2, X } from "lucide-react";
682
+ import { useState as useState3 } from "react";
683
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
684
+ function ElvixRecoverGate({
685
+ baseUrl,
686
+ appName,
687
+ state,
688
+ sinceAt,
689
+ onRestore,
690
+ onCancel,
691
+ onFail
614
692
  }) {
615
- const s = SIZE[size];
616
- const t = TONE[variant][theme];
617
- const style = {
618
- display: "inline-flex",
619
- alignItems: "center",
620
- gap: s.gap,
621
- height: s.height,
622
- paddingLeft: s.padX,
623
- paddingRight: s.padX,
624
- fontSize: s.font,
625
- fontWeight: 500,
626
- borderRadius: 9999,
627
- background: t.bg,
628
- border: `1px solid ${t.border}`,
629
- color: t.brand,
630
- textDecoration: "none",
631
- userSelect: "none",
632
- lineHeight: 1,
633
- // Dimensional overrides win over the size preset above.
634
- ...sizeStyle({ width, height, minWidth, maxWidth, minHeight, maxHeight })
635
- };
636
- return /* @__PURE__ */ jsxs4(
637
- "a",
638
- {
639
- href,
640
- target: "_blank",
641
- rel: "noopener noreferrer",
642
- className,
643
- style,
644
- "data-elvix-secured-badge": "",
645
- children: [
646
- /* @__PURE__ */ jsx5(ElvixShield, { size: s.icon, fill: t.shield, accent: accentColor }),
647
- /* @__PURE__ */ jsxs4("span", { style: { display: "inline-flex", alignItems: "baseline", gap: 4 }, children: [
648
- /* @__PURE__ */ jsx5("span", { style: { color: t.lead }, children: "Secured by" }),
649
- /* @__PURE__ */ jsx5("span", { style: { color: t.brand, fontWeight: 600 }, children: "elvix" })
650
- ] })
651
- ]
652
- }
693
+ const [pane, setPane] = useState3("decide");
694
+ const [busy, setBusy] = useState3("none");
695
+ const [error, setError] = useState3(null);
696
+ const ninetyDaysMs = 90 * 24 * 60 * 60 * 1e3;
697
+ const daysLeft = (
698
+ // LEGACY: spine-lint-disable-next-line spine/enum-over-string
699
+ state === "soft_deleted_by_user" ? Math.max(
700
+ 0,
701
+ Math.ceil(
702
+ (new Date(sinceAt).getTime() + ninetyDaysMs - Date.now()) / (24 * 60 * 60 * 1e3)
703
+ )
704
+ ) : null
653
705
  );
654
- }
655
-
656
- // src/react/elvix-sign-in-form.tsx
657
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
658
- function PasskeyIcon({ size = 18 }) {
659
- return /* @__PURE__ */ jsxs5(
660
- "svg",
661
- {
662
- width: size,
663
- height: size,
664
- viewBox: "0 0 24 24",
665
- fill: "none",
666
- stroke: "currentColor",
667
- strokeWidth: 1.8,
668
- strokeLinecap: "round",
669
- strokeLinejoin: "round",
670
- "aria-hidden": true,
671
- style: { display: "block" },
672
- children: [
673
- /* @__PURE__ */ jsx6("circle", { cx: "9", cy: "8", r: "4" }),
674
- /* @__PURE__ */ jsx6("path", { d: "M4 20c0-3 2.5-5 5-5 1 0 1.9.3 2.7.8" }),
675
- /* @__PURE__ */ jsx6("path", { d: "M17 12.5a2.5 2.5 0 1 0-2.5 2.5v5l1.2-1.2 1.3 1.2v-5a2.5 2.5 0 0 0 0-2.5Z" })
676
- ]
706
+ async function decide(decision) {
707
+ if (busy !== "none") return;
708
+ setBusy(decision);
709
+ setError(null);
710
+ try {
711
+ const init = authInit();
712
+ const res = await fetch(`${baseUrl}/api/auth/recover-membership`, {
713
+ method: "POST",
714
+ headers: { "Content-Type": "application/json", ...init.headers },
715
+ credentials: init.credentials,
716
+ body: JSON.stringify({ decision })
717
+ });
718
+ const raw = await res.json().catch(() => ({}));
719
+ const errorKey = raw.errorKey ?? raw.errorMessage;
720
+ if (!res.ok || !raw.success) {
721
+ const msg = errorKey === "grace_expired" ? "The 90-day window to restore has closed." : errorKey === "unauthenticated" ? "Your session ended. Sign in again." : "Something went wrong. Try again.";
722
+ setError(msg);
723
+ onFail?.(msg);
724
+ return;
725
+ }
726
+ const redirect = raw.data?.redirect ?? "/";
727
+ if (decision === "restore") {
728
+ onRestore?.({ redirect });
729
+ if (!onRestore) setPane("done_restored");
730
+ } else {
731
+ onCancel?.({ redirect });
732
+ if (!onCancel) setPane("done_cancelled");
733
+ }
734
+ } catch {
735
+ const msg = "Network hiccup. Try again.";
736
+ setError(msg);
737
+ onFail?.(msg);
738
+ } finally {
739
+ setBusy("none");
677
740
  }
678
- );
679
- }
680
- function GoogleG({ size = 18 }) {
681
- return /* @__PURE__ */ jsxs5("svg", { width: size, height: size, viewBox: "0 0 18 18", "aria-hidden": true, style: { display: "block" }, children: [
682
- /* @__PURE__ */ jsx6(
683
- "path",
741
+ }
742
+ return /* @__PURE__ */ jsx5("div", { className: "relative overflow-hidden", children: /* @__PURE__ */ jsxs4(AnimatePresence, { mode: "wait", children: [
743
+ pane === "decide" && /* @__PURE__ */ jsx5(
744
+ motion.div,
684
745
  {
685
- fill: "#4285F4",
686
- d: "M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62Z"
687
- }
746
+ initial: { opacity: 0, y: 6 },
747
+ animate: { opacity: 1, y: 0 },
748
+ exit: { opacity: 0, y: -4 },
749
+ transition: { duration: 0.24, ease: [0.22, 0.61, 0.36, 1] },
750
+ children: /* @__PURE__ */ jsx5(
751
+ DecidePane,
752
+ {
753
+ appName,
754
+ state,
755
+ daysLeft,
756
+ busy,
757
+ error,
758
+ onRestore: () => void decide("restore"),
759
+ onCancel: () => void decide("cancel")
760
+ }
761
+ )
762
+ },
763
+ "decide"
688
764
  ),
689
- /* @__PURE__ */ jsx6(
690
- "path",
765
+ pane === "done_restored" && /* @__PURE__ */ jsx5(
766
+ motion.div,
691
767
  {
692
- fill: "#34A853",
693
- d: "M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.33A9 9 0 0 0 9 18Z"
694
- }
768
+ initial: { opacity: 0, y: 6 },
769
+ animate: { opacity: 1, y: 0 },
770
+ exit: { opacity: 0, y: -4 },
771
+ transition: { duration: 0.24, ease: [0.22, 0.61, 0.36, 1] },
772
+ children: /* @__PURE__ */ jsx5(
773
+ DonePane,
774
+ {
775
+ icon: "check",
776
+ title: `You're back on ${appName}.`,
777
+ body: "Your membership is active again. Continue to the app whenever you're ready."
778
+ }
779
+ )
780
+ },
781
+ "done_restored"
695
782
  ),
696
- /* @__PURE__ */ jsx6(
697
- "path",
783
+ pane === "done_cancelled" && /* @__PURE__ */ jsx5(
784
+ motion.div,
698
785
  {
699
- fill: "#FBBC05",
700
- d: "M3.97 10.72A5.4 5.4 0 0 1 3.68 9c0-.6.1-1.18.29-1.72V4.95H.96A9 9 0 0 0 0 9c0 1.45.35 2.83.96 4.05l3.01-2.33Z"
786
+ initial: { opacity: 0, y: 6 },
787
+ animate: { opacity: 1, y: 0 },
788
+ exit: { opacity: 0, y: -4 },
789
+ transition: { duration: 0.24, ease: [0.22, 0.61, 0.36, 1] },
790
+ children: /* @__PURE__ */ jsx5(
791
+ DonePane,
792
+ {
793
+ icon: "logout",
794
+ title: "You're signed out.",
795
+ body: `You stayed off ${appName}. Come back any time.`
796
+ }
797
+ )
798
+ },
799
+ "done_cancelled"
800
+ )
801
+ ] }) });
802
+ }
803
+ function DecidePane({
804
+ appName,
805
+ state,
806
+ daysLeft,
807
+ busy,
808
+ error,
809
+ onRestore,
810
+ onCancel
811
+ }) {
812
+ const isDeleted = state === "soft_deleted_by_user";
813
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-4", children: [
814
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-start gap-3", children: [
815
+ /* @__PURE__ */ jsx5(
816
+ "span",
817
+ {
818
+ className: "size-10 rounded-full inline-flex items-center justify-center shrink-0 " + (isDeleted ? "bg-red-500/15" : ""),
819
+ style: isDeleted ? void 0 : { background: "var(--elvix-primary-12)" },
820
+ children: isDeleted ? /* @__PURE__ */ jsx5(Undo2, { className: "size-5 text-red-500", strokeWidth: 2.2 }) : /* @__PURE__ */ jsx5(
821
+ EyeOff,
822
+ {
823
+ className: "size-5",
824
+ strokeWidth: 2.2,
825
+ style: { color: "var(--elvix-primary-strong)" }
826
+ }
827
+ )
828
+ }
829
+ ),
830
+ /* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
831
+ /* @__PURE__ */ jsxs4("div", { className: "text-[15px] font-semibold tracking-tight text-fg-1 leading-tight", children: [
832
+ "Welcome back to ",
833
+ appName
834
+ ] }),
835
+ /* @__PURE__ */ jsx5("p", { className: "text-[12.5px] text-fg-3 leading-[1.55] mt-1", children: isDeleted ? `You left ${appName} earlier. ${daysLeft != null ? `You have ${daysLeft} ${daysLeft === 1 ? "day" : "days"} left to come back.` : "You're still inside the restore window."} Restore your spot now or sign back out.` : `Your membership on ${appName} is on pause right now. Restore to pick up where you left off, or sign back out and stay away.` })
836
+ ] })
837
+ ] }),
838
+ error ? /* @__PURE__ */ jsx5("p", { className: "text-[12px] text-red-500 leading-tight", children: error }) : null,
839
+ /* @__PURE__ */ jsx5(
840
+ "button",
841
+ {
842
+ type: "button",
843
+ onClick: onRestore,
844
+ disabled: busy !== "none",
845
+ autoFocus: true,
846
+ className: "w-full h-10 rounded-[10px] text-[14px] font-semibold tracking-tight cursor-pointer transition ring-1 ring-black/10 inline-flex items-center justify-center gap-1.5 disabled:opacity-60 disabled:cursor-not-allowed",
847
+ style: {
848
+ background: "var(--elvix-primary-strong)",
849
+ color: "var(--elvix-on-primary)",
850
+ backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,0.12), rgba(255,255,255,0) 40%)",
851
+ boxShadow: "0 1px 0 rgba(255,255,255,0.06) inset, 0 2px 3px -1px rgba(0,0,0,0.18), 0 0 0 1px rgba(25,28,33,0.08)"
852
+ },
853
+ children: busy === "restore" ? "Restoring\u2026" : /* @__PURE__ */ jsxs4(Fragment2, { children: [
854
+ /* @__PURE__ */ jsx5(Undo2, { className: "size-3.5" }),
855
+ isDeleted ? `Restore my spot on ${appName}` : `Bring me back on ${appName}`
856
+ ] })
701
857
  }
702
858
  ),
703
- /* @__PURE__ */ jsx6(
704
- "path",
859
+ /* @__PURE__ */ jsx5(
860
+ "button",
705
861
  {
706
- fill: "#EA4335",
707
- d: "M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.59C13.46.89 11.43 0 9 0A9 9 0 0 0 .96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58Z"
862
+ type: "button",
863
+ onClick: onCancel,
864
+ disabled: busy !== "none",
865
+ className: "w-full h-10 rounded-[10px] text-[13px] font-medium text-fg-2 hover:text-fg-1 hover:bg-surface-hover cursor-pointer transition inline-flex items-center justify-center gap-1.5 disabled:opacity-60 disabled:cursor-not-allowed",
866
+ children: busy === "cancel" ? "Signing you out\u2026" : /* @__PURE__ */ jsxs4(Fragment2, { children: [
867
+ /* @__PURE__ */ jsx5(X, { className: "size-3.5" }),
868
+ "Not now, sign me out"
869
+ ] })
708
870
  }
709
871
  )
710
872
  ] });
711
873
  }
712
- function ElvixSignInForm({
713
- onResult,
714
- redirectAfterSignIn,
715
- copy: copyProp,
716
- className = "",
717
- minWidth,
718
- maxWidth,
719
- minHeight,
720
- maxHeight,
721
- width,
722
- height
874
+ function DonePane({
875
+ icon,
876
+ title,
877
+ body
723
878
  }) {
724
- const ctx = useElvixContext();
725
- const app = useElvixApp();
726
- const copy = resolveCopy(app?.strings, copyProp);
727
- const [step, setStep] = useState3("identify");
728
- const [email, setEmail] = useState3("");
729
- const [code, setCode] = useState3("");
730
- const [challengeId, setChallengeId] = useState3(null);
731
- const [busy, setBusy] = useState3(false);
732
- const [error, setError] = useState3(null);
733
- const brand = app?.brandColor || "#6c5ce7";
734
- const onBrand = app?.onBrandColor || "#fff";
735
- const appName = app?.appName ?? "your app";
736
- const verb = app?.signInVerb === "login" ? "Log in" : "Sign in";
737
- const defaultTitle = app?.appName ? `${verb} to ${app.appName}` : verb;
738
- const title = copy.title ? fillCopy(copy.title, { app: app?.appName ?? "" }) : defaultTitle;
739
- const submitLabel = copy.submitButton ?? verb;
740
- const showGoogle = Boolean(app?.methodGoogle);
741
- const showPasskey = Boolean(app?.methodPasskey);
742
- const showEmail = Boolean(app?.methodEmailOtp);
743
- const showDivider = (showGoogle || showPasskey) && showEmail;
744
- const logoSrc = app?.iconUrl || app?.logoUrl || null;
745
- const privacyUrl = app?.privacyPolicyUrl || null;
746
- const termsUrl = app?.termsOfServiceUrl || null;
747
- const hasLegal = Boolean(privacyUrl || termsUrl);
748
- const cardStyle = useMemo2(
749
- () => ({
750
- boxSizing: "border-box",
751
- // Defaults first; the shared sizeStyle() overrides only the keys the host set,
752
- // so the form keeps its width "100%" / maxWidth 400 defaults when unsized.
753
- width: "100%",
754
- maxWidth: 400,
755
- ...sizeStyle({ width, height, minWidth, maxWidth, minHeight, maxHeight }),
756
- margin: "0 auto",
757
- display: "flex",
758
- flexDirection: "column",
759
- gap: 18,
760
- background: "#fff",
761
- color: "#18181b",
762
- border: "1px solid rgba(0,0,0,0.08)",
763
- borderRadius: 16,
764
- boxShadow: "0 1px 2px rgba(0,0,0,0.04), 0 12px 32px -12px rgba(0,0,0,0.18)",
765
- padding: 28,
766
- fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
767
- textAlign: "center"
768
- }),
769
- [width, maxWidth, minWidth, height, minHeight, maxHeight]
770
- );
771
- function fail(error2, message) {
772
- setError(message ?? error2);
773
- onResult?.({ ok: false, error: error2, message });
774
- }
775
- async function startGoogle() {
776
- if (!ctx.clientId) return fail("missing_client_id", "ElvixProvider needs a clientId.");
777
- window.location.assign(
778
- `${ctx.baseUrl}/api/auth/google/start?intent=app&clientId=${encodeURIComponent(ctx.clientId)}`
779
- );
780
- }
781
- async function startPasskey() {
782
- setBusy(true);
783
- setError(null);
784
- try {
785
- const result = await runPasskeySignIn(ctx.baseUrl, ctx.clientId);
786
- if (!result.ok) {
787
- if (result.error === "passkey_cancelled") {
788
- onResult?.({ ok: false, error: result.error });
789
- return;
790
- }
791
- return fail(result.error, result.message);
879
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col items-center text-center gap-4 py-2", children: [
880
+ /* @__PURE__ */ jsx5(
881
+ motion.span,
882
+ {
883
+ initial: { scale: 0.6, opacity: 0 },
884
+ animate: { scale: 1, opacity: 1 },
885
+ transition: { duration: 0.34, ease: [0.22, 0.61, 0.36, 1] },
886
+ className: "size-12 rounded-full inline-flex items-center justify-center",
887
+ style: { background: "var(--elvix-primary-12)" },
888
+ children: icon === "check" ? /* @__PURE__ */ jsx5(
889
+ CheckCircle2,
890
+ {
891
+ className: "size-7",
892
+ strokeWidth: 2.2,
893
+ style: { color: "var(--elvix-primary-strong)" }
894
+ }
895
+ ) : /* @__PURE__ */ jsx5(LogOut, { className: "size-7 text-fg-2", strokeWidth: 2.2 })
792
896
  }
793
- setStep("done");
794
- onResult?.({
795
- ok: true,
796
- method: "passkey",
797
- redirect: result.redirect ?? redirectAfterSignIn,
798
- token: result.token
897
+ ),
898
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-1 max-w-[320px]", children: [
899
+ /* @__PURE__ */ jsx5("div", { className: "text-[15px] font-semibold tracking-tight text-fg-1", children: title }),
900
+ /* @__PURE__ */ jsx5("div", { className: "text-[12.5px] text-fg-3 leading-[1.55]", children: body })
901
+ ] })
902
+ ] });
903
+ }
904
+
905
+ // src/react/google-one-tap.tsx
906
+ import { memo, useCallback, useEffect as useEffect2, useRef, useState as useState4 } from "react";
907
+
908
+ // src/react/spine-fetch.ts
909
+ function unwrapEnvelope(input) {
910
+ if (!input || typeof input !== "object") return input;
911
+ const e = input;
912
+ if (!("success" in e)) return input;
913
+ const dataObject = e.data && typeof e.data === "object" && !Array.isArray(e.data) ? e.data : {};
914
+ const arrayData = Array.isArray(e.data) ? { data: e.data } : {};
915
+ return {
916
+ ok: Boolean(e.success),
917
+ error: e.errorKey ?? e.errorMessage ?? void 0,
918
+ ...dataObject,
919
+ ...arrayData
920
+ };
921
+ }
922
+
923
+ // src/react/google-one-tap.tsx
924
+ import { jsxs as jsxs5 } from "react/jsx-runtime";
925
+ var GIS_SRC = "https://accounts.google.com/gsi/client";
926
+ var scriptPromise = null;
927
+ function loadGisScript() {
928
+ if (typeof window === "undefined") return Promise.reject(new Error("ssr"));
929
+ if (window.google?.accounts?.id) return Promise.resolve();
930
+ if (scriptPromise) return scriptPromise;
931
+ scriptPromise = new Promise((resolve, reject) => {
932
+ const existing = document.querySelector(`script[src="${GIS_SRC}"]`);
933
+ if (existing) {
934
+ existing.addEventListener("load", () => resolve(), { once: true });
935
+ existing.addEventListener("error", () => reject(new Error("gis_load_failed")), {
936
+ once: true
799
937
  });
800
- } finally {
801
- setBusy(false);
938
+ return;
802
939
  }
803
- }
804
- async function startOtp(e) {
805
- e.preventDefault();
806
- if (!email.trim()) return fail("invalid_input", copy.errorEnterEmail);
807
- setBusy(true);
808
- setError(null);
809
- try {
810
- const res = await fetch(`${ctx.baseUrl}/api/auth/otp/start`, {
811
- method: "POST",
812
- headers: { "content-type": "application/json" },
813
- credentials: isSameOrigin(ctx.baseUrl) ? "include" : "omit",
814
- body: JSON.stringify({
815
- email: email.trim(),
816
- intent: "app",
817
- clientId: ctx.clientId
818
- })
940
+ const s = document.createElement("script");
941
+ s.src = GIS_SRC;
942
+ s.async = true;
943
+ s.defer = true;
944
+ s.onload = () => resolve();
945
+ s.onerror = () => reject(new Error("gis_load_failed"));
946
+ document.head.appendChild(s);
947
+ });
948
+ return scriptPromise;
949
+ }
950
+ var GoogleOneTap = memo(function GoogleOneTap2({
951
+ baseUrl,
952
+ clientId,
953
+ intent,
954
+ appClientId,
955
+ config,
956
+ /** When true, also render the GIS button (used for popup ux_mode). */
957
+ renderButton = false,
958
+ /** Visible-on-page slot for the rendered GIS button. */
959
+ buttonContainerRef
960
+ }) {
961
+ const [err, setErr] = useState4(null);
962
+ const initialised = useRef(false);
963
+ const handleCredential = useCallback(
964
+ async (response) => {
965
+ try {
966
+ const res = await fetch(`${baseUrl}/api/auth/google/credential`, {
967
+ method: "POST",
968
+ headers: { "Content-Type": "application/json" },
969
+ credentials: isSameOrigin(baseUrl) ? "include" : "omit",
970
+ body: JSON.stringify({
971
+ credential: response.credential,
972
+ intent,
973
+ clientId: appClientId
974
+ })
975
+ });
976
+ const body = unwrapEnvelope(await res.json());
977
+ if (!res.ok || !body.ok) {
978
+ setErr(body.error ?? "Sign-in failed");
979
+ return;
980
+ }
981
+ if (body.token) setElvixToken(body.token);
982
+ if (body.redirect) {
983
+ window.location.assign(body.redirect);
984
+ }
985
+ } catch (e) {
986
+ setErr(e instanceof Error ? e.message : "Network error");
987
+ }
988
+ },
989
+ [baseUrl, intent, appClientId]
990
+ );
991
+ useEffect2(() => {
992
+ if (!clientId) return;
993
+ if (!config.oneTap && !renderButton) return;
994
+ let cancelled = false;
995
+ (async () => {
996
+ try {
997
+ await loadGisScript();
998
+ if (cancelled) return;
999
+ const g = window.google?.accounts?.id;
1000
+ if (!g) return;
1001
+ if (!initialised.current) {
1002
+ const initConfig = {
1003
+ client_id: clientId,
1004
+ callback: handleCredential,
1005
+ auto_select: config.autoSelect,
1006
+ use_fedcm_for_prompt: config.fedcm,
1007
+ ux_mode: "popup",
1008
+ itp_support: true,
1009
+ cancel_on_tap_outside: false,
1010
+ context: "signin"
1011
+ };
1012
+ if (config.hostedDomain) initConfig.hosted_domain = config.hostedDomain;
1013
+ g.initialize(initConfig);
1014
+ initialised.current = true;
1015
+ }
1016
+ if (renderButton && buttonContainerRef?.current) {
1017
+ buttonContainerRef.current.innerHTML = "";
1018
+ const measured = buttonContainerRef.current.clientWidth;
1019
+ const safeWidth = Math.min(400, Math.max(240, measured || 360));
1020
+ g.renderButton(buttonContainerRef.current, {
1021
+ theme: "outline",
1022
+ size: "large",
1023
+ shape: "rectangular",
1024
+ text: "continue_with",
1025
+ logo_alignment: "left",
1026
+ width: safeWidth
1027
+ });
1028
+ }
1029
+ if (config.oneTap) {
1030
+ g.prompt();
1031
+ }
1032
+ } catch (e) {
1033
+ setErr(e instanceof Error ? e.message : "GIS load failed");
1034
+ }
1035
+ })();
1036
+ return () => {
1037
+ cancelled = true;
1038
+ };
1039
+ }, [
1040
+ clientId,
1041
+ config.oneTap,
1042
+ config.autoSelect,
1043
+ config.popup,
1044
+ config.fedcm,
1045
+ config.hostedDomain,
1046
+ renderButton,
1047
+ handleCredential,
1048
+ buttonContainerRef
1049
+ ]);
1050
+ useEffect2(() => {
1051
+ return () => {
1052
+ const g = window.google?.accounts?.id;
1053
+ if (g) g.cancel();
1054
+ };
1055
+ }, []);
1056
+ if (err) {
1057
+ return /* @__PURE__ */ jsxs5("p", { className: "text-[11.5px] text-red-400 bg-red-500/10 border border-red-500/20 rounded-[8px] px-3 py-2", children: [
1058
+ "Google sign-in: ",
1059
+ err
1060
+ ] });
1061
+ }
1062
+ return null;
1063
+ });
1064
+
1065
+ // src/react/otp-input.tsx
1066
+ import {
1067
+ useCallback as useCallback2,
1068
+ useEffect as useEffect3,
1069
+ useRef as useRef2
1070
+ } from "react";
1071
+ import { jsx as jsx6 } from "react/jsx-runtime";
1072
+ var OTP_LENGTH = 6;
1073
+ function OtpInput({
1074
+ value,
1075
+ onChange,
1076
+ disabled,
1077
+ autoFocus
1078
+ }) {
1079
+ const refs = useRef2([]);
1080
+ const digits = Array.from({ length: OTP_LENGTH }, (_, i) => value[i] ?? "");
1081
+ useEffect3(() => {
1082
+ if (autoFocus) refs.current[0]?.focus();
1083
+ }, [autoFocus]);
1084
+ const focus = useCallback2((i) => refs.current[i]?.focus(), []);
1085
+ const onCharChange = useCallback2(
1086
+ (i, e) => {
1087
+ const stripped = e.target.value.replace(/\D/g, "");
1088
+ if (stripped.length > 1) {
1089
+ const fill = stripped.slice(0, OTP_LENGTH - i);
1090
+ const next2 = [...digits];
1091
+ for (let k = 0; k < fill.length; k += 1) {
1092
+ next2[i + k] = fill[k];
1093
+ }
1094
+ const merged = next2.join("");
1095
+ onChange(merged);
1096
+ focus(Math.min(i + fill.length, OTP_LENGTH - 1));
1097
+ return;
1098
+ }
1099
+ const ch = stripped.slice(-1);
1100
+ const next = [...digits];
1101
+ next[i] = ch;
1102
+ onChange(next.join(""));
1103
+ if (ch && i < OTP_LENGTH - 1) focus(i + 1);
1104
+ },
1105
+ [digits, onChange, focus]
1106
+ );
1107
+ const onKeyDown = useCallback2(
1108
+ (i, e) => {
1109
+ if (e.key === "Backspace" && !digits[i] && i > 0) {
1110
+ e.preventDefault();
1111
+ const next = [...digits];
1112
+ next[i - 1] = "";
1113
+ onChange(next.join(""));
1114
+ focus(i - 1);
1115
+ } else if (e.key === "ArrowLeft" && i > 0) {
1116
+ focus(i - 1);
1117
+ } else if (e.key === "ArrowRight" && i < OTP_LENGTH - 1) {
1118
+ focus(i + 1);
1119
+ }
1120
+ },
1121
+ [digits, onChange, focus]
1122
+ );
1123
+ const onPaste = useCallback2(
1124
+ (e) => {
1125
+ e.preventDefault();
1126
+ const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, OTP_LENGTH);
1127
+ if (pasted) {
1128
+ onChange(pasted);
1129
+ focus(Math.min(pasted.length, OTP_LENGTH - 1));
1130
+ }
1131
+ },
1132
+ [onChange, focus]
1133
+ );
1134
+ return (
1135
+ // 6-column grid keeps every box equal-width and never overflows
1136
+ // its container, so the input fits cleanly inside an ElvixCard
1137
+ // on the narrowest mobile viewports without a horizontal scroll.
1138
+ // `aspect-square` keeps each box visually balanced as width
1139
+ // shrinks. `max-w-12` caps the boxes at the original 48px on
1140
+ // wide surfaces so they don't bloat on desktop.
1141
+ /* @__PURE__ */ jsx6("div", { className: "grid grid-cols-6 gap-2 w-full", children: digits.map((d, i) => /* @__PURE__ */ jsx6(
1142
+ "input",
1143
+ {
1144
+ ref: (el) => {
1145
+ refs.current[i] = el;
1146
+ },
1147
+ type: "text",
1148
+ inputMode: "numeric",
1149
+ autoComplete: "one-time-code",
1150
+ maxLength: OTP_LENGTH,
1151
+ value: d,
1152
+ disabled,
1153
+ onChange: (e) => onCharChange(i, e),
1154
+ onKeyDown: (e) => onKeyDown(i, e),
1155
+ onPaste,
1156
+ onFocus: (e) => e.target.select(),
1157
+ className: "w-full aspect-square min-w-0 max-w-12 mx-auto text-center text-[20px] font-semibold tabular-nums rounded-[10px] bg-surface border border-border-base text-fg-1 focus:outline-none focus:border-[#8e7dff] focus:ring-2 focus:ring-[#8e7dff]/20 transition disabled:opacity-50"
1158
+ },
1159
+ i
1160
+ )) })
1161
+ );
1162
+ }
1163
+
1164
+ // src/react/username-rules.ts
1165
+ var USERNAME_MIN_LENGTH = 4;
1166
+ var USERNAME_MAX_LENGTH = 30;
1167
+ var RESERVED_USERNAMES = /* @__PURE__ */ new Set([
1168
+ // system + routing
1169
+ "admin",
1170
+ "administrator",
1171
+ "root",
1172
+ "system",
1173
+ "sys",
1174
+ "api",
1175
+ "auth",
1176
+ "oauth",
1177
+ "app",
1178
+ "apps",
1179
+ "account",
1180
+ "accounts",
1181
+ "billing",
1182
+ "checkout",
1183
+ "pay",
1184
+ "payment",
1185
+ "payments",
1186
+ "brand",
1187
+ "console",
1188
+ "dashboard",
1189
+ "dev",
1190
+ "developer",
1191
+ "developers",
1192
+ "docs",
1193
+ "documentation",
1194
+ "explore",
1195
+ "help",
1196
+ "home",
1197
+ "index",
1198
+ "legal",
1199
+ "privacy",
1200
+ "terms",
1201
+ "tos",
1202
+ "cookies",
1203
+ "cookie",
1204
+ "imprint",
1205
+ "login",
1206
+ "signin",
1207
+ "logout",
1208
+ "signout",
1209
+ "signup",
1210
+ "register",
1211
+ "me",
1212
+ "my",
1213
+ "profile",
1214
+ "profiles",
1215
+ "user",
1216
+ "users",
1217
+ "new",
1218
+ "edit",
1219
+ "create",
1220
+ "delete",
1221
+ "public",
1222
+ "private",
1223
+ "static",
1224
+ "assets",
1225
+ "settings",
1226
+ "setting",
1227
+ "preferences",
1228
+ "support",
1229
+ "contact",
1230
+ "www",
1231
+ "http",
1232
+ "https",
1233
+ "ftp",
1234
+ "mail",
1235
+ "next",
1236
+ "_next",
1237
+ "src",
1238
+ "test",
1239
+ "tests",
1240
+ // brand
1241
+ "elvix",
1242
+ "edvone",
1243
+ "axum",
1244
+ "aixum",
1245
+ "mithra",
1246
+ "delvix",
1247
+ "buildza",
1248
+ "danceclub",
1249
+ "pulse",
1250
+ "pulseai",
1251
+ // pseudo-paths / common reserved
1252
+ "null",
1253
+ "undefined",
1254
+ "true",
1255
+ "false"
1256
+ ]);
1257
+ function usernameReason(raw) {
1258
+ const v = raw.trim().toLowerCase();
1259
+ if (v.length < USERNAME_MIN_LENGTH) return "too_short";
1260
+ if (v.length > USERNAME_MAX_LENGTH) return "too_long";
1261
+ if (!/^[a-z0-9._]+$/.test(v)) return "bad_chars";
1262
+ if (!/^[a-z]/.test(v)) return "must_start_letter";
1263
+ if (!/[a-z0-9]$/.test(v)) return "must_end_alnum";
1264
+ if (/[._]{2}/.test(v)) return "no_double_special";
1265
+ if (RESERVED_USERNAMES.has(v)) return "reserved";
1266
+ return "ok";
1267
+ }
1268
+ function isValidUsername(raw) {
1269
+ return usernameReason(raw) === "ok";
1270
+ }
1271
+
1272
+ // src/react/elvix-sign-in-form.tsx
1273
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1274
+ var ELVIX_SITE_URL = "https://elvix.is";
1275
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1276
+ var ELVIX_LIGHT_VARS = {
1277
+ ["--canvas"]: "#fafafa",
1278
+ ["--surface"]: "#ffffff",
1279
+ ["--surface-2"]: "#f4f4f5",
1280
+ ["--surface-hover"]: "rgba(0, 0, 0, 0.04)",
1281
+ ["--surface-active"]: "rgba(0, 0, 0, 0.06)",
1282
+ ["--border"]: "rgba(0, 0, 0, 0.08)",
1283
+ ["--border-strong"]: "rgba(0, 0, 0, 0.14)",
1284
+ ["--fg-1"]: "#18181b",
1285
+ ["--fg-2"]: "#52525b",
1286
+ ["--fg-3"]: "#71717a",
1287
+ ["--placeholder"]: "#a1a1aa"
1288
+ };
1289
+ var ELVIX_DARK_VARS = {
1290
+ ["--canvas"]: "#0a0a0b",
1291
+ ["--surface"]: "#0d0d10",
1292
+ ["--surface-2"]: "#08080a",
1293
+ ["--surface-hover"]: "rgba(255, 255, 255, 0.04)",
1294
+ ["--surface-active"]: "rgba(255, 255, 255, 0.06)",
1295
+ ["--border"]: "rgba(255, 255, 255, 0.06)",
1296
+ ["--border-strong"]: "rgba(255, 255, 255, 0.12)",
1297
+ ["--fg-1"]: "#fafafa",
1298
+ ["--fg-2"]: "#a1a1aa",
1299
+ ["--fg-3"]: "#71717a",
1300
+ ["--placeholder"]: "#52525b"
1301
+ };
1302
+ function ElvixSignInForm(props) {
1303
+ const app = useElvixApp();
1304
+ const resolved = {
1305
+ ...props,
1306
+ mode: props.mode ?? "interactive",
1307
+ appName: props.appName ?? app?.appName ?? "your app",
1308
+ logoUrl: props.logoUrl ?? app?.logoUrl ?? null,
1309
+ logoUrlDark: props.logoUrlDark ?? app?.logoUrlDark ?? null,
1310
+ brandColor: props.brandColor ?? app?.brandColor ?? "#5d4dff",
1311
+ onBrandColor: props.onBrandColor ?? app?.onBrandColor ?? "#ffffff",
1312
+ methodGoogle: props.methodGoogle ?? app?.methodGoogle ?? false,
1313
+ methodEmailOtp: props.methodEmailOtp ?? app?.methodEmailOtp ?? true,
1314
+ methodPasskey: props.methodPasskey ?? app?.methodPasskey ?? false,
1315
+ methodUsername: props.methodUsername ?? app?.methodUsername ?? false,
1316
+ privacyPolicyUrl: props.privacyPolicyUrl ?? app?.privacyPolicyUrl ?? null,
1317
+ termsOfServiceUrl: props.termsOfServiceUrl ?? app?.termsOfServiceUrl ?? null,
1318
+ intent: props.intent ?? "app",
1319
+ clientId: props.clientId ?? app?.clientId ?? void 0,
1320
+ layout: props.layout ?? app?.layout ?? "centered",
1321
+ socialLayout: props.socialLayout ?? app?.socialLayout ?? "stacked",
1322
+ presentation: props.presentation ?? app?.presentation ?? "card",
1323
+ theme: props.theme ?? app?.theme ?? "light",
1324
+ showHeader: props.showHeader ?? app?.showHeader ?? true,
1325
+ transparentBg: props.transparentBg ?? app?.transparentBg ?? false,
1326
+ signInVerb: props.signInVerb ?? app?.signInVerb ?? "signin",
1327
+ googleConfig: props.googleConfig ?? app?.googleConfig ?? void 0,
1328
+ googleClientId: props.googleClientId ?? app?.googleClientId ?? void 0,
1329
+ websiteUrl: props.websiteUrl ?? app?.websiteUrl ?? null
1330
+ };
1331
+ const { framed = true, presentation = "card", theme = "light" } = resolved;
1332
+ const card = /* @__PURE__ */ jsx7(AuthCard, { ...resolved });
1333
+ let content = card;
1334
+ if (presentation === "drawer") {
1335
+ content = /* @__PURE__ */ jsx7(DrawerPresentation, { children: card });
1336
+ } else if (presentation === "modal") {
1337
+ content = /* @__PURE__ */ jsx7(ModalPresentation, { children: card });
1338
+ }
1339
+ const wrapped = (
1340
+ // LEGACY: spine-lint-disable-next-line spine/enum-over-string
1341
+ theme === "auto" ? content : /* @__PURE__ */ jsx7(
1342
+ "div",
1343
+ {
1344
+ className: framed ? "bg-canvas" : void 0,
1345
+ style: theme === "dark" ? ELVIX_DARK_VARS : ELVIX_LIGHT_VARS,
1346
+ children: content
1347
+ }
1348
+ )
1349
+ );
1350
+ if (!framed) return wrapped;
1351
+ return /* @__PURE__ */ jsx7(FramedPreview, { children: wrapped });
1352
+ }
1353
+ function DrawerPresentation({ children }) {
1354
+ return /* @__PURE__ */ jsxs6("div", { className: "relative bg-surface-2 rounded-t-[20px] border-t border-x border-border-base shadow-[0_-12px_40px_-12px_rgba(0,0,0,0.18)] pt-2 pb-0 overflow-hidden", children: [
1355
+ /* @__PURE__ */ jsx7("div", { className: "mx-auto mt-1 mb-3 h-1 w-9 rounded-full bg-border-strong" }),
1356
+ /* @__PURE__ */ jsx7("div", { className: "px-3 pb-3", children })
1357
+ ] });
1358
+ }
1359
+ function ModalPresentation({ children }) {
1360
+ return /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
1361
+ /* @__PURE__ */ jsx7(
1362
+ "div",
1363
+ {
1364
+ "aria-hidden": true,
1365
+ className: "absolute inset-0 -m-4 rounded-[18px] bg-black/30 backdrop-blur-sm pointer-events-none"
1366
+ }
1367
+ ),
1368
+ /* @__PURE__ */ jsx7("div", { className: "relative", children })
1369
+ ] });
1370
+ }
1371
+ function AuthCard(props) {
1372
+ const { transparentBg = false } = props;
1373
+ const cardClass = transparentBg ? "overflow-hidden" : "rounded-[14px] bg-surface shadow-[0_2px_8px_rgba(0,0,0,0.04),0_20px_40px_-20px_rgba(0,0,0,0.12)] border border-border-base overflow-hidden";
1374
+ return /* @__PURE__ */ jsxs6("div", { className: cardClass, children: [
1375
+ /* @__PURE__ */ jsx7("div", { className: transparentBg ? "px-1 py-1" : "px-7 py-7", children: /* @__PURE__ */ jsx7(AuthBody, { ...props }) }),
1376
+ /* @__PURE__ */ jsx7(
1377
+ "div",
1378
+ {
1379
+ className: (transparentBg ? "mt-3 " : "border-t border-border-base bg-surface-hover px-7 py-3 ") + "flex items-center justify-center",
1380
+ children: /* @__PURE__ */ jsxs6(
1381
+ "a",
1382
+ {
1383
+ href: ELVIX_SITE_URL,
1384
+ target: "_blank",
1385
+ rel: "noopener noreferrer",
1386
+ className: "inline-flex items-center gap-1.5 pl-1.5 pr-2.5 py-1 rounded-full bg-surface ring-1 ring-border-base shadow-sm hover:bg-surface-hover hover:ring-border-strong transition",
1387
+ children: [
1388
+ /* @__PURE__ */ jsxs6("span", { className: "relative inline-flex items-center", children: [
1389
+ /* @__PURE__ */ jsx7(ElvixLogo, { size: 12, className: "text-fg-1" }),
1390
+ /* @__PURE__ */ jsx7(
1391
+ "span",
1392
+ {
1393
+ "aria-hidden": true,
1394
+ className: "absolute -right-px -top-px size-1 rounded-full bg-emerald-500 ring-1 ring-surface"
1395
+ }
1396
+ )
1397
+ ] }),
1398
+ /* @__PURE__ */ jsxs6("span", { className: "text-[11px] tracking-tight leading-none", children: [
1399
+ /* @__PURE__ */ jsx7("span", { className: "text-fg-3", children: "Secured by " }),
1400
+ /* @__PURE__ */ jsx7("span", { className: "font-semibold text-fg-1", children: "elvix" })
1401
+ ] })
1402
+ ]
1403
+ }
1404
+ )
1405
+ }
1406
+ )
1407
+ ] });
1408
+ }
1409
+ function AuthBody({
1410
+ mode,
1411
+ appName,
1412
+ logoUrl,
1413
+ logoUrlDark,
1414
+ logoNode,
1415
+ brandColor,
1416
+ onBrandColor = "#ffffff",
1417
+ methodGoogle,
1418
+ methodEmailOtp,
1419
+ methodPasskey,
1420
+ methodUsername = false,
1421
+ privacyPolicyUrl,
1422
+ termsOfServiceUrl,
1423
+ intent = "app",
1424
+ clientId,
1425
+ websiteUrl,
1426
+ layout = "centered",
1427
+ socialLayout = "stacked",
1428
+ showHeader = true,
1429
+ theme = "light",
1430
+ signInVerb = "signin",
1431
+ belowHeading,
1432
+ belowMethods,
1433
+ googleConfig,
1434
+ googleClientId,
1435
+ onAuthenticated,
1436
+ onResult
1437
+ }) {
1438
+ const { baseUrl } = useElvixContext();
1439
+ const isPreview = mode === "preview";
1440
+ const anyMethod = methodGoogle || methodEmailOtp || methodPasskey || methodUsername;
1441
+ const gisEnabled = !isPreview && methodGoogle && Boolean(googleConfig) && Boolean(
1442
+ googleConfig?.oneTap || googleConfig?.popup || googleConfig?.autoSelect || googleConfig?.fedcm
1443
+ );
1444
+ const gisButtonRef = useRef3(null);
1445
+ const useGisRenderedButton = gisEnabled && Boolean(googleClientId);
1446
+ const [step, setStep] = useState5(
1447
+ "identifier"
1448
+ );
1449
+ const [recoverState, setRecoverState] = useState5(null);
1450
+ const [identifier, setIdentifier] = useState5("");
1451
+ const [code, setCode] = useState5("");
1452
+ const [challengeId, setChallengeId] = useState5(null);
1453
+ const [sendingOtp, setSendingOtp] = useState5(false);
1454
+ const [verifyingOtp, setVerifyingOtp] = useState5(false);
1455
+ const [error, setError] = useState5(null);
1456
+ const [resendIn, setResendIn] = useState5(0);
1457
+ const [passkeyBusy, setPasskeyBusy] = useState5(false);
1458
+ const [usernameValue, setUsernameValue] = useState5("");
1459
+ const [usernameSuggestions, setUsernameSuggestions] = useState5([]);
1460
+ const [usernameCheck, setUsernameCheck] = useState5({ kind: "idle" });
1461
+ const [onboardingBusy, setOnboardingBusy] = useState5(null);
1462
+ const [finalRedirect, setFinalRedirect] = useState5("/");
1463
+ useEffect4(() => {
1464
+ if (resendIn <= 0) return;
1465
+ const t = setInterval(() => setResendIn((s) => Math.max(0, s - 1)), 1e3);
1466
+ return () => clearInterval(t);
1467
+ }, [resendIn]);
1468
+ const reportError = useCallback3(
1469
+ (code2, message) => {
1470
+ setError(message);
1471
+ onResult?.({
1472
+ ok: false,
1473
+ error: code2 ?? "unknown",
1474
+ message
819
1475
  });
820
- const body = await res.json();
821
- if (!res.ok || !body.success || !body.data?.challengeId) {
822
- return fail(body.errorMessage ?? "otp_start_failed");
1476
+ },
1477
+ [onResult]
1478
+ );
1479
+ const applyLanding = useCallback3(
1480
+ (body) => {
1481
+ if (body.token) setElvixToken(body.token);
1482
+ if (body.next_step === "username") {
1483
+ setUsernameSuggestions(body.suggestions ?? []);
1484
+ setUsernameValue(body.suggestions?.[0] ?? "");
1485
+ setFinalRedirect(body.final ?? "/");
1486
+ setStep("username");
1487
+ return;
823
1488
  }
824
- setChallengeId(body.data.challengeId);
825
- setStep("code");
826
- } catch (e2) {
827
- fail("network", e2 instanceof Error ? e2.message : void 0);
1489
+ if (body.next_step === "passkey") {
1490
+ setFinalRedirect(body.final ?? "/");
1491
+ setStep("passkey");
1492
+ return;
1493
+ }
1494
+ if (body.next_step === "recover" && body.recover) {
1495
+ setRecoverState(body.recover);
1496
+ setFinalRedirect(body.final ?? "/");
1497
+ setStep("recover");
1498
+ return;
1499
+ }
1500
+ const redirect = body.redirect ?? defaultRedirect(intent);
1501
+ onResult?.({ ok: true, redirect });
1502
+ if (onAuthenticated) {
1503
+ onAuthenticated({ ok: true, redirect, token: body.token });
1504
+ return;
1505
+ }
1506
+ window.location.href = redirect;
1507
+ },
1508
+ [intent, onAuthenticated, onResult]
1509
+ );
1510
+ useEffect4(() => {
1511
+ if (isPreview) return;
1512
+ if (typeof window === "undefined") return;
1513
+ const params = new URLSearchParams(window.location.search);
1514
+ if (params.get("onboarding") !== "1") return;
1515
+ (async () => {
1516
+ try {
1517
+ const init = authInit();
1518
+ const res = await fetch(`${baseUrl}/api/onboarding/state`, {
1519
+ headers: init.headers,
1520
+ credentials: init.credentials
1521
+ });
1522
+ if (!res.ok) return;
1523
+ const body = unwrapEnvelope(await res.json());
1524
+ if (body.ok) applyLanding(body);
1525
+ } catch {
1526
+ } finally {
1527
+ const url = new URL(window.location.href);
1528
+ url.searchParams.delete("onboarding");
1529
+ url.searchParams.delete("next");
1530
+ window.history.replaceState({}, "", url.toString());
1531
+ }
1532
+ })();
1533
+ }, []);
1534
+ const autoSubmittedCodeRef = useRef3("");
1535
+ useEffect4(() => {
1536
+ if (code.length < 6) autoSubmittedCodeRef.current = "";
1537
+ }, [code]);
1538
+ const identifierValid = useMemo2(() => {
1539
+ const v = identifier.trim();
1540
+ if (!v) return false;
1541
+ if (v.includes("@")) return methodEmailOtp && EMAIL_RE.test(v);
1542
+ if (methodUsername) return isValidUsername(v);
1543
+ return false;
1544
+ }, [identifier, methodEmailOtp, methodUsername]);
1545
+ const identifierPlaceholder = methodEmailOtp && methodUsername ? "Email or username" : methodUsername ? "Username" : "Enter your email";
1546
+ const ctaBase = brandColor;
1547
+ const ctaFg = onBrandColor;
1548
+ const ctaStyle = {
1549
+ backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,0.12), rgba(255,255,255,0) 40%)",
1550
+ backgroundColor: ctaBase,
1551
+ color: ctaFg,
1552
+ boxShadow: "0 1px 0 rgba(255,255,255,0.06) inset, 0 2px 3px -1px rgba(0,0,0,0.18), 0 0 0 1px rgba(25,28,33,0.08)"
1553
+ };
1554
+ const ctaLabelStyle = {
1555
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.18))"
1556
+ };
1557
+ const onSubmitIdentifier = useCallback3(
1558
+ async (e) => {
1559
+ e?.preventDefault();
1560
+ if (!identifierValid || isPreview || sendingOtp) return;
1561
+ setError(null);
1562
+ setSendingOtp(true);
1563
+ const v = identifier.trim();
1564
+ try {
1565
+ if (!v.includes("@")) {
1566
+ const res2 = await fetch(`${baseUrl}/api/auth/identifier/resolve`, {
1567
+ method: "POST",
1568
+ headers: { "Content-Type": "application/json" },
1569
+ credentials: isSameOrigin(baseUrl) ? "include" : "omit",
1570
+ body: JSON.stringify({
1571
+ username: v.toLowerCase(),
1572
+ intent,
1573
+ ...clientId ? { clientId } : {}
1574
+ })
1575
+ });
1576
+ const body2 = unwrapEnvelope(await res2.json().catch(() => ({})));
1577
+ if (!res2.ok || !body2.ok || !body2.challengeId) {
1578
+ if (body2.error === "too_recent" || body2.error === "too_many") {
1579
+ setResendIn(body2.retryAfterSeconds ?? 45);
1580
+ }
1581
+ reportError(body2.error, humanError(body2.error, body2.retryAfterSeconds));
1582
+ return;
1583
+ }
1584
+ setChallengeId(body2.challengeId);
1585
+ setStep("code");
1586
+ setResendIn(45);
1587
+ return;
1588
+ }
1589
+ const res = await fetch(`${baseUrl}/api/auth/otp/start`, {
1590
+ method: "POST",
1591
+ headers: { "Content-Type": "application/json" },
1592
+ credentials: isSameOrigin(baseUrl) ? "include" : "omit",
1593
+ body: JSON.stringify({ email: v, intent, clientId })
1594
+ });
1595
+ const body = unwrapEnvelope(await res.json().catch(() => ({})));
1596
+ if (!res.ok || !body.ok || !body.challengeId) {
1597
+ if (body.error === "too_recent" || body.error === "too_many") {
1598
+ setResendIn(body.retryAfterSeconds ?? 30);
1599
+ }
1600
+ reportError(body.error, humanError(body.error, body.retryAfterSeconds));
1601
+ return;
1602
+ }
1603
+ setChallengeId(body.challengeId);
1604
+ setStep("code");
1605
+ setResendIn(45);
1606
+ } catch {
1607
+ reportError("network_error", "Network hiccup. Try again.");
1608
+ } finally {
1609
+ setSendingOtp(false);
1610
+ }
1611
+ },
1612
+ [identifier, identifierValid, isPreview, sendingOtp, intent, clientId, reportError, baseUrl]
1613
+ );
1614
+ const onSubmitCode = useCallback3(
1615
+ async (e) => {
1616
+ e?.preventDefault();
1617
+ if (isPreview || verifyingOtp || code.length !== 6 || !challengeId) return;
1618
+ setError(null);
1619
+ setVerifyingOtp(true);
1620
+ try {
1621
+ const res = await fetch(`${baseUrl}/api/auth/otp/verify`, {
1622
+ method: "POST",
1623
+ headers: { "Content-Type": "application/json" },
1624
+ credentials: isSameOrigin(baseUrl) ? "include" : "omit",
1625
+ body: JSON.stringify({ challengeId, code })
1626
+ });
1627
+ const body = unwrapEnvelope(await res.json().catch(() => ({})));
1628
+ if (!res.ok || !body.ok) {
1629
+ reportError(body.error, humanError(body.error));
1630
+ return;
1631
+ }
1632
+ applyLanding(body);
1633
+ } catch {
1634
+ reportError("network_error", "Network hiccup. Try again.");
1635
+ } finally {
1636
+ setVerifyingOtp(false);
1637
+ }
1638
+ },
1639
+ [code, challengeId, isPreview, verifyingOtp, applyLanding, reportError, baseUrl]
1640
+ );
1641
+ useEffect4(() => {
1642
+ if (step !== "code") return;
1643
+ if (code.length !== 6) return;
1644
+ if (verifyingOtp || isPreview || !challengeId) return;
1645
+ if (autoSubmittedCodeRef.current === code) return;
1646
+ autoSubmittedCodeRef.current = code;
1647
+ void onSubmitCode();
1648
+ }, [code, step, verifyingOtp, isPreview, challengeId, onSubmitCode]);
1649
+ const onPasskey = useCallback3(async () => {
1650
+ if (isPreview || passkeyBusy) return;
1651
+ setError(null);
1652
+ setPasskeyBusy(true);
1653
+ try {
1654
+ const result = await runPasskeySignIn(baseUrl, clientId);
1655
+ if (!result.ok) {
1656
+ if (result.error === "passkey_cancelled") {
1657
+ onResult?.({ ok: false, error: result.error });
1658
+ return;
1659
+ }
1660
+ reportError(result.error, result.message ?? humanError(result.error) ?? "Passkey verification failed.");
1661
+ return;
1662
+ }
1663
+ applyLanding({ next_step: "done", redirect: result.redirect, token: result.token });
828
1664
  } finally {
829
- setBusy(false);
1665
+ setPasskeyBusy(false);
830
1666
  }
831
- }
832
- async function verifyOtp(e) {
833
- e.preventDefault();
834
- if (!challengeId) return;
835
- if (code.trim().length !== 6) return fail("invalid_input", copy.errorEnterCode);
836
- setBusy(true);
1667
+ }, [isPreview, passkeyBusy, baseUrl, clientId, applyLanding, reportError, onResult]);
1668
+ const usernameAbortRef = useRef3(null);
1669
+ useEffect4(() => {
1670
+ if (step !== "username") return;
1671
+ const candidate = usernameValue.trim().toLowerCase();
1672
+ if (!candidate) {
1673
+ setUsernameCheck({ kind: "idle" });
1674
+ return;
1675
+ }
1676
+ setUsernameCheck({ kind: "checking" });
1677
+ const t = setTimeout(() => {
1678
+ usernameAbortRef.current?.abort();
1679
+ const ctrl = new AbortController();
1680
+ usernameAbortRef.current = ctrl;
1681
+ const init = authInit();
1682
+ fetch(`${baseUrl}/api/onboarding/username/check?u=${encodeURIComponent(candidate)}`, {
1683
+ signal: ctrl.signal,
1684
+ headers: init.headers,
1685
+ credentials: init.credentials
1686
+ }).then((r) => r.json()).then((raw) => unwrapEnvelope(raw)).then((b) => {
1687
+ if (b.ok === false) {
1688
+ setUsernameCheck({ kind: "rejected", reason: b.reason ?? "invalid" });
1689
+ return;
1690
+ }
1691
+ if (b.available) setUsernameCheck({ kind: "available" });
1692
+ else setUsernameCheck({ kind: "rejected", reason: b.reason ?? "taken" });
1693
+ }).catch((err) => {
1694
+ if (err.name !== "AbortError") setUsernameCheck({ kind: "idle" });
1695
+ });
1696
+ }, 220);
1697
+ return () => clearTimeout(t);
1698
+ }, [usernameValue, step, baseUrl]);
1699
+ const onSubmitUsername = useCallback3(
1700
+ async (auto) => {
1701
+ if (onboardingBusy || isPreview) return;
1702
+ setError(null);
1703
+ setOnboardingBusy(auto ? "skip" : "claim");
1704
+ try {
1705
+ const init = authInit();
1706
+ const res = await fetch(`${baseUrl}/api/onboarding/username`, {
1707
+ method: "POST",
1708
+ headers: { "Content-Type": "application/json", ...init.headers },
1709
+ credentials: init.credentials,
1710
+ body: JSON.stringify(auto ? {} : { username: usernameValue.trim().toLowerCase() })
1711
+ });
1712
+ const body = unwrapEnvelope(await res.json().catch(() => ({})));
1713
+ if (!res.ok || !body.ok) {
1714
+ reportError(body.error, humanError(body.error));
1715
+ return;
1716
+ }
1717
+ applyLanding(body);
1718
+ } catch {
1719
+ reportError("network_error", "Network hiccup. Try again.");
1720
+ } finally {
1721
+ setOnboardingBusy(null);
1722
+ }
1723
+ },
1724
+ [usernameValue, isPreview, onboardingBusy, applyLanding, reportError, baseUrl]
1725
+ );
1726
+ const onAddPasskey = useCallback3(async () => {
1727
+ if (onboardingBusy || isPreview) return;
837
1728
  setError(null);
1729
+ setOnboardingBusy("add");
838
1730
  try {
839
- const res = await fetch(`${ctx.baseUrl}/api/auth/otp/verify`, {
840
- method: "POST",
841
- headers: { "content-type": "application/json" },
842
- credentials: isSameOrigin(ctx.baseUrl) ? "include" : "omit",
843
- body: JSON.stringify({ challengeId, code: code.trim() })
844
- });
845
- const body = await res.json();
846
- if (!res.ok || !body.success) {
847
- return fail(body.errorMessage ?? "otp_verify_failed");
1731
+ const surface = intent === "app" ? "app" : intent;
1732
+ const result = await runPasskeyRegister(baseUrl, surface);
1733
+ if (!result.ok) {
1734
+ if (result.error === "passkey_cancelled") return;
1735
+ reportError(
1736
+ result.error,
1737
+ result.message ?? humanError(result.error) ?? "Passkey couldn't be added. Try again or skip for now."
1738
+ );
1739
+ return;
848
1740
  }
849
- if (body.data?.token) setElvixToken(body.data.token);
850
- setStep("done");
851
- onResult?.({
852
- ok: true,
853
- method: "email_otp",
854
- redirect: body.data?.redirect ?? redirectAfterSignIn,
855
- token: body.data?.token
856
- });
857
- } catch (e2) {
858
- fail("network", e2 instanceof Error ? e2.message : void 0);
1741
+ onResult?.({ ok: true, redirect: finalRedirect });
1742
+ if (onAuthenticated) {
1743
+ onAuthenticated({ ok: true, redirect: finalRedirect });
1744
+ return;
1745
+ }
1746
+ window.location.href = finalRedirect;
859
1747
  } finally {
860
- setBusy(false);
1748
+ setOnboardingBusy(null);
861
1749
  }
862
- }
863
- const labelStyle = {
864
- display: "block",
865
- textAlign: "left",
866
- fontSize: 12.5,
867
- fontWeight: 500,
868
- color: "#52525b",
869
- marginBottom: 6
870
- };
871
- const inputStyle = {
872
- boxSizing: "border-box",
873
- width: "100%",
874
- height: 44,
875
- padding: "0 14px",
876
- borderRadius: 10,
877
- border: "1px solid rgba(0,0,0,0.14)",
878
- background: "#fff",
879
- color: "#18181b",
880
- fontSize: 14,
881
- outline: "none"
882
- };
883
- const primaryBtnStyle = {
884
- boxSizing: "border-box",
885
- width: "100%",
886
- height: 44,
887
- marginTop: 10,
888
- border: "1px solid rgba(0,0,0,0.08)",
889
- borderRadius: 10,
890
- background: brand,
891
- color: onBrand,
892
- fontSize: 14,
893
- fontWeight: 600,
894
- cursor: busy ? "not-allowed" : "pointer",
895
- opacity: busy ? 0.65 : 1,
896
- backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,0.12), rgba(255,255,255,0) 40%)",
897
- boxShadow: "0 1px 0 rgba(255,255,255,0.06) inset, 0 2px 3px -1px rgba(0,0,0,0.18), 0 0 0 1px rgba(25,28,33,0.06)"
898
- };
899
- const googleBtnStyle = {
900
- boxSizing: "border-box",
901
- width: "100%",
902
- height: 44,
903
- display: "inline-flex",
904
- alignItems: "center",
905
- justifyContent: "center",
906
- gap: 10,
907
- border: "1px solid rgba(0,0,0,0.14)",
908
- borderRadius: 10,
909
- background: "#fff",
910
- color: "#18181b",
911
- fontSize: 14,
912
- fontWeight: 500,
913
- cursor: busy ? "not-allowed" : "pointer",
914
- opacity: busy ? 0.65 : 1
915
- };
916
- const tileTint = hexToRgba(brand, 0.12);
917
- const root = `${className}`.trim() || void 0;
918
- return /* @__PURE__ */ jsxs5("div", { className: root, style: cardStyle, "data-elvix-pane": step, children: [
919
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }, children: [
920
- /* @__PURE__ */ jsx6(
921
- "div",
922
- {
923
- style: {
924
- width: 52,
925
- height: 52,
926
- borderRadius: 14,
927
- display: "grid",
928
- placeItems: "center",
929
- overflow: "hidden",
930
- background: logoSrc ? "#fff" : tileTint,
931
- border: "1px solid rgba(0,0,0,0.08)"
932
- },
933
- children: logoSrc ? (
934
- // biome-ignore lint/a11y/useAltText: alt is set
935
- /* @__PURE__ */ jsx6(
936
- "img",
1750
+ }, [intent, isPreview, onboardingBusy, finalRedirect, onAuthenticated, onResult, reportError, baseUrl]);
1751
+ const onSkipPasskey = useCallback3(() => {
1752
+ if (onboardingBusy) return;
1753
+ setOnboardingBusy("skip");
1754
+ onResult?.({ ok: true, redirect: finalRedirect });
1755
+ if (onAuthenticated) {
1756
+ onAuthenticated({ ok: true, redirect: finalRedirect });
1757
+ return;
1758
+ }
1759
+ window.location.href = finalRedirect;
1760
+ }, [onboardingBusy, finalRedirect, onAuthenticated, onResult]);
1761
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
1762
+ showHeader && /* @__PURE__ */ jsxs6(
1763
+ "div",
1764
+ {
1765
+ className: "gap-3 mb-6 " + (layout === "left" ? "flex items-center text-left" : layout === "banner" ? "-mx-7 -mt-7 px-7 py-6 flex flex-col items-center text-center border-b border-border-base" : "flex flex-col items-center text-center"),
1766
+ style: layout === "banner" ? { background: `linear-gradient(135deg, ${brandColor}24, ${brandColor}08)` } : void 0,
1767
+ children: [
1768
+ (() => {
1769
+ const letter = /* @__PURE__ */ jsx7(
1770
+ "div",
937
1771
  {
938
- src: logoSrc,
939
- alt: appName,
940
- style: { width: "100%", height: "100%", objectFit: "cover" }
1772
+ className: "size-12 rounded-[10px] border border-border-base grid place-items-center overflow-hidden transition" + (websiteUrl ? " hover:border-border-strong hover:shadow-sm" : ""),
1773
+ style: { background: `${brandColor}1a` },
1774
+ children: /* @__PURE__ */ jsx7("span", { className: "text-[18px] font-semibold", style: { color: brandColor }, children: appName?.[0]?.toUpperCase() ?? "?" })
941
1775
  }
942
- )
943
- ) : /* @__PURE__ */ jsx6("span", { style: { fontSize: 22, fontWeight: 700, color: brand }, children: appName.charAt(0).toUpperCase() || "?" })
944
- }
945
- ),
946
- /* @__PURE__ */ jsxs5("div", { children: [
947
- /* @__PURE__ */ jsx6("h2", { style: { margin: 0, fontSize: 19, fontWeight: 600, letterSpacing: "-0.01em" }, children: step === "code" ? "Check your inbox" : title }),
948
- step !== "done" && /* @__PURE__ */ jsx6("p", { style: { margin: "4px 0 0", fontSize: 13, color: "#71717a" }, children: step === "code" ? fillCopy(copy.codeSentSubtitle ?? "", { email }) : copy.subtitle })
949
- ] })
950
- ] }),
951
- step === "done" && /* @__PURE__ */ jsx6("p", { style: { margin: 0, fontSize: 14, color: "#18181b" }, children: copy.signedInText }),
952
- step === "identify" && /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flexDirection: "column", gap: 14 }, children: [
953
- showGoogle && /* @__PURE__ */ jsxs5(
954
- "button",
1776
+ );
1777
+ const bareImg = (src) => /* @__PURE__ */ jsx7("img", { src, alt: appName, className: "h-10 w-auto max-w-[220px] object-contain" });
1778
+ let inner;
1779
+ if (logoNode) {
1780
+ inner = /* @__PURE__ */ jsx7("div", { className: "inline-flex items-center justify-center min-h-12 max-w-[220px]", children: logoNode });
1781
+ } else if (theme === "dark") {
1782
+ inner = logoUrlDark ? bareImg(logoUrlDark) : letter;
1783
+ } else if (theme === "auto" && logoUrlDark && logoUrl) {
1784
+ inner = /* @__PURE__ */ jsxs6("picture", { children: [
1785
+ /* @__PURE__ */ jsx7("source", { srcSet: logoUrlDark, media: "(prefers-color-scheme: dark)" }),
1786
+ /* @__PURE__ */ jsx7(
1787
+ "img",
1788
+ {
1789
+ src: logoUrl,
1790
+ alt: appName,
1791
+ className: "h-10 w-auto max-w-[220px] object-contain"
1792
+ }
1793
+ )
1794
+ ] });
1795
+ } else if (logoUrl) {
1796
+ inner = bareImg(logoUrl);
1797
+ } else {
1798
+ inner = letter;
1799
+ }
1800
+ return websiteUrl ? /* @__PURE__ */ jsx7(
1801
+ "a",
1802
+ {
1803
+ href: websiteUrl,
1804
+ target: "_blank",
1805
+ rel: "noopener noreferrer",
1806
+ "aria-label": `Visit ${appName} website`,
1807
+ className: "cursor-pointer",
1808
+ children: inner
1809
+ }
1810
+ ) : inner;
1811
+ })(),
1812
+ /* @__PURE__ */ jsxs6("div", { children: [
1813
+ /* @__PURE__ */ jsx7("div", { className: "text-[18px] font-semibold tracking-tight text-fg-1", children: step === "code" ? "Check your inbox" : step === "username" ? "Pick a username" : step === "passkey" ? `${signInVerb === "login" ? "Log in" : "Sign in"} faster next time` : step === "recover" ? `Welcome back to ${recoverState?.appName ?? appName}` : `${signInVerb === "login" ? "Log in" : "Sign in"} to ${appName || "your app"}` }),
1814
+ /* @__PURE__ */ jsx7("div", { className: "text-[12.5px] text-fg-3 mt-0.5", children: step === "code" ? `We sent a code to ${identifier}` : step === "username" ? `This is how people see you${appName ? ` on ${appName}` : ""}.` : step === "passkey" ? "Touch ID, Face ID, or your security key. No more email codes." : step === "recover" ? "Pick whether to come back or stay away." : "Pick how you want to continue." }),
1815
+ step === "identifier" && belowHeading
1816
+ ] })
1817
+ ]
1818
+ }
1819
+ ),
1820
+ step === "code" ? /* @__PURE__ */ jsxs6("form", { onSubmit: onSubmitCode, className: "space-y-3", children: [
1821
+ /* @__PURE__ */ jsx7(
1822
+ OtpInput,
955
1823
  {
956
- type: "button",
957
- onClick: startGoogle,
958
- disabled: busy,
959
- style: googleBtnStyle,
960
- "data-elvix-method": "google",
961
- children: [
962
- /* @__PURE__ */ jsx6(GoogleG, {}),
963
- /* @__PURE__ */ jsx6("span", { children: copy.googleButton })
964
- ]
1824
+ value: code,
1825
+ onChange: setCode,
1826
+ disabled: isPreview || verifyingOtp,
1827
+ autoFocus: true
965
1828
  }
966
1829
  ),
967
- showPasskey && /* @__PURE__ */ jsxs5(
1830
+ /* @__PURE__ */ jsx7(
968
1831
  "button",
969
1832
  {
970
- type: "button",
971
- onClick: startPasskey,
972
- disabled: busy,
973
- style: googleBtnStyle,
974
- "data-elvix-method": "passkey",
975
- children: [
976
- /* @__PURE__ */ jsx6(PasskeyIcon, {}),
977
- /* @__PURE__ */ jsx6("span", { children: copy.passkeyButton })
978
- ]
979
- }
980
- ),
981
- showDivider && /* @__PURE__ */ jsxs5(
982
- "div",
983
- {
984
- "aria-hidden": true,
985
- style: { display: "flex", alignItems: "center", gap: 12, color: "#a1a1aa" },
986
- children: [
987
- /* @__PURE__ */ jsx6("span", { style: { flex: 1, height: 1, background: "rgba(0,0,0,0.08)" } }),
988
- /* @__PURE__ */ jsx6("span", { style: { fontSize: 11, fontWeight: 600, letterSpacing: "0.08em" }, children: "OR" }),
989
- /* @__PURE__ */ jsx6("span", { style: { flex: 1, height: 1, background: "rgba(0,0,0,0.08)" } })
990
- ]
1833
+ type: "submit",
1834
+ disabled: verifyingOtp || code.length !== 6,
1835
+ className: "cursor-pointer w-full inline-flex items-center justify-center h-9 px-4 rounded-[10px] font-semibold text-[13px] tracking-tight transition hover:brightness-[0.94] active:brightness-[0.88] disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:brightness-100",
1836
+ style: ctaStyle,
1837
+ children: /* @__PURE__ */ jsxs6("span", { className: "inline-flex items-center gap-1.5", style: ctaLabelStyle, children: [
1838
+ verifyingOtp ? /* @__PURE__ */ jsx7(Loader2, { className: "size-4 animate-spin" }) : "Verify",
1839
+ !verifyingOtp && /* @__PURE__ */ jsx7("svg", { width: "11", height: "10", viewBox: "0 0 11 10", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx7(
1840
+ "path",
1841
+ {
1842
+ d: "M7.75 5L4.25 2.75V7.25L7.75 5Z",
1843
+ fill: "currentColor",
1844
+ stroke: "currentColor",
1845
+ opacity: "0.6",
1846
+ strokeWidth: "1.5",
1847
+ strokeLinecap: "round",
1848
+ strokeLinejoin: "round"
1849
+ }
1850
+ ) })
1851
+ ] })
991
1852
  }
992
1853
  ),
993
- showEmail && /* @__PURE__ */ jsxs5("form", { onSubmit: startOtp, "data-elvix-method": "email_otp", children: [
994
- /* @__PURE__ */ jsx6("label", { htmlFor: "elvix-email", style: labelStyle, children: "Email" }),
995
- /* @__PURE__ */ jsx6(
996
- "input",
1854
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between pt-1", children: [
1855
+ /* @__PURE__ */ jsxs6(
1856
+ "button",
997
1857
  {
998
- id: "elvix-email",
999
- type: "email",
1000
- value: email,
1001
- onChange: (ev) => setEmail(ev.target.value),
1002
- placeholder: copy.emailPlaceholder,
1003
- required: true,
1004
- disabled: busy,
1005
- autoComplete: "email",
1006
- style: inputStyle
1858
+ type: "button",
1859
+ onClick: () => {
1860
+ setStep("identifier");
1861
+ setCode("");
1862
+ setError(null);
1863
+ },
1864
+ className: "cursor-pointer inline-flex items-center gap-1 text-[12px] text-fg-2 hover:text-fg-1 hover:bg-surface-hover rounded-md px-2 -mx-2 py-1 transition",
1865
+ children: [
1866
+ /* @__PURE__ */ jsx7(ArrowLeft, { className: "size-3" }),
1867
+ " Use a different email"
1868
+ ]
1007
1869
  }
1008
1870
  ),
1009
- /* @__PURE__ */ jsx6("button", { type: "submit", disabled: busy, style: primaryBtnStyle, children: busy ? copy.sendingLabel : copy.sendCodeButton })
1010
- ] })
1011
- ] }),
1012
- step === "code" && /* @__PURE__ */ jsxs5("form", { onSubmit: verifyOtp, children: [
1013
- /* @__PURE__ */ jsx6("label", { htmlFor: "elvix-code", style: labelStyle, children: "Verification code" }),
1014
- /* @__PURE__ */ jsx6(
1015
- "input",
1871
+ /* @__PURE__ */ jsx7(
1872
+ "button",
1873
+ {
1874
+ type: "button",
1875
+ disabled: resendIn > 0 || sendingOtp,
1876
+ onClick: () => onSubmitIdentifier(),
1877
+ className: "cursor-pointer text-[12px] text-fg-2 hover:text-fg-1 disabled:opacity-50 disabled:cursor-not-allowed disabled:text-fg-3 transition",
1878
+ children: resendIn > 0 ? `Resend in ${resendIn}s` : "Resend code"
1879
+ }
1880
+ )
1881
+ ] }),
1882
+ error && /* @__PURE__ */ jsx7("p", { className: "text-[11.5px] text-red-400 text-center", children: error })
1883
+ ] }) : step === "username" ? /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
1884
+ /* @__PURE__ */ jsxs6("div", { children: [
1885
+ /* @__PURE__ */ jsx7("label", { htmlFor: "onboarding-username", className: "block text-[12px] text-fg-3 mb-1.5", children: "Username" }),
1886
+ /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
1887
+ /* @__PURE__ */ jsx7("span", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-[14px] text-fg-3 pointer-events-none", children: "@" }),
1888
+ /* @__PURE__ */ jsx7(
1889
+ "input",
1890
+ {
1891
+ id: "onboarding-username",
1892
+ type: "text",
1893
+ autoFocus: true,
1894
+ autoComplete: "username",
1895
+ value: usernameValue,
1896
+ minLength: 4,
1897
+ maxLength: 30,
1898
+ onChange: (e) => setUsernameValue(e.target.value),
1899
+ placeholder: "yourname",
1900
+ disabled: onboardingBusy !== null,
1901
+ className: "w-full h-11 pl-7 pr-10 rounded-[10px] bg-surface border border-border-strong text-[14px] text-fg-1 placeholder:text-placeholder focus:outline-none focus:border-[#8e7dff] focus:ring-2 focus:ring-[#8e7dff]/20 transition disabled:opacity-60 disabled:cursor-not-allowed"
1902
+ }
1903
+ ),
1904
+ usernameCheck.kind === "checking" && /* @__PURE__ */ jsx7(Loader2, { className: "size-4 text-fg-3 absolute right-3 top-1/2 -translate-y-1/2 animate-spin pointer-events-none" }),
1905
+ usernameCheck.kind === "available" && /* @__PURE__ */ jsx7(Check, { className: "size-4 text-emerald-500 absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" }),
1906
+ usernameCheck.kind === "rejected" && /* @__PURE__ */ jsx7(X2, { className: "size-4 text-red-500 absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" })
1907
+ ] }),
1908
+ /* @__PURE__ */ jsxs6("p", { className: "text-[11px] mt-1.5 leading-relaxed min-h-[14px]", children: [
1909
+ usernameCheck.kind === "idle" && /* @__PURE__ */ jsx7("span", { className: "text-fg-3", children: "4\u201330 chars, a\u2013z 0\u20139 . _ \u2014 must start with a letter." }),
1910
+ usernameCheck.kind === "checking" && /* @__PURE__ */ jsx7("span", { className: "text-fg-3", children: "Checking\u2026" }),
1911
+ usernameCheck.kind === "available" && /* @__PURE__ */ jsx7("span", { className: "text-emerald-500", children: "Looks good. This one's yours." }),
1912
+ usernameCheck.kind === "rejected" && /* @__PURE__ */ jsx7("span", { className: "text-red-500", children: usernameReasonLabel(usernameCheck.reason) })
1913
+ ] })
1914
+ ] }),
1915
+ usernameSuggestions.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
1916
+ /* @__PURE__ */ jsx7("div", { className: "text-[11px] uppercase tracking-[0.08em] text-fg-3", children: "Suggestions" }),
1917
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 overflow-x-auto whitespace-nowrap pb-0.5 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden", children: [
1918
+ usernameSuggestions.slice(0, 3).map((s) => /* @__PURE__ */ jsxs6(
1919
+ "button",
1920
+ {
1921
+ type: "button",
1922
+ disabled: onboardingBusy !== null,
1923
+ onClick: () => setUsernameValue(s),
1924
+ className: "cursor-pointer shrink-0 inline-flex items-center h-7 px-2.5 rounded-full bg-surface-hover border border-border-base hover:border-border-strong hover:bg-surface-active transition text-[12px] text-fg-2 font-mono disabled:opacity-60 disabled:cursor-not-allowed",
1925
+ children: [
1926
+ "@",
1927
+ s
1928
+ ]
1929
+ },
1930
+ s
1931
+ )),
1932
+ /* @__PURE__ */ jsx7(
1933
+ "button",
1934
+ {
1935
+ type: "button",
1936
+ onClick: () => onSubmitUsername(true),
1937
+ disabled: onboardingBusy !== null,
1938
+ className: "cursor-pointer shrink-0 inline-flex items-center gap-1 h-7 px-2.5 rounded-full border border-dashed border-border-base hover:border-border-strong hover:bg-surface-hover transition text-[12px] text-fg-3 hover:text-fg-1 disabled:opacity-60 disabled:cursor-not-allowed",
1939
+ children: onboardingBusy === "skip" ? /* @__PURE__ */ jsx7(Loader2, { className: "size-3.5 animate-spin" }) : "Skip"
1940
+ }
1941
+ )
1942
+ ] })
1943
+ ] }),
1944
+ error && /* @__PURE__ */ jsx7("p", { className: "text-[11.5px] text-red-400", children: error }),
1945
+ /* @__PURE__ */ jsx7(
1946
+ "button",
1016
1947
  {
1017
- id: "elvix-code",
1018
- type: "text",
1019
- inputMode: "numeric",
1020
- pattern: "[0-9]*",
1021
- maxLength: 6,
1022
- value: code,
1023
- onChange: (ev) => setCode(ev.target.value.replace(/\D/g, "")),
1024
- placeholder: copy.codePlaceholder,
1025
- required: true,
1026
- disabled: busy,
1027
- autoComplete: "one-time-code",
1028
- autoFocus: true,
1029
- style: { ...inputStyle, letterSpacing: "0.3em", textAlign: "center", fontSize: 18 }
1948
+ type: "button",
1949
+ onClick: () => onSubmitUsername(false),
1950
+ disabled: onboardingBusy !== null || usernameCheck.kind !== "available",
1951
+ className: "cursor-pointer w-full inline-flex items-center justify-center h-9 px-4 rounded-[10px] font-semibold text-[13px] tracking-tight transition hover:brightness-[0.94] active:brightness-[0.88] disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:brightness-100",
1952
+ style: ctaStyle,
1953
+ children: /* @__PURE__ */ jsx7("span", { className: "inline-flex items-center gap-1.5", style: ctaLabelStyle, children: onboardingBusy === "claim" ? /* @__PURE__ */ jsx7(Loader2, { className: "size-4 animate-spin" }) : `Claim @${usernameValue || "yourname"}` })
1954
+ }
1955
+ )
1956
+ ] }) : step === "passkey" ? /* @__PURE__ */ jsxs6("div", { className: "space-y-3", children: [
1957
+ /* @__PURE__ */ jsxs6("ul", { className: "text-[12.5px] text-fg-2 leading-relaxed space-y-1.5", children: [
1958
+ /* @__PURE__ */ jsxs6("li", { className: "flex items-start gap-2", children: [
1959
+ /* @__PURE__ */ jsx7("span", { className: "mt-1.5 size-1 rounded-full", style: { background: brandColor } }),
1960
+ /* @__PURE__ */ jsx7("span", { children: "No more password or email codes" })
1961
+ ] }),
1962
+ /* @__PURE__ */ jsxs6("li", { className: "flex items-start gap-2", children: [
1963
+ /* @__PURE__ */ jsx7("span", { className: "mt-1.5 size-1 rounded-full", style: { background: brandColor } }),
1964
+ /* @__PURE__ */ jsx7("span", { children: "Phish-proof \u2014 works only on this site" })
1965
+ ] }),
1966
+ /* @__PURE__ */ jsxs6("li", { className: "flex items-start gap-2", children: [
1967
+ /* @__PURE__ */ jsx7("span", { className: "mt-1.5 size-1 rounded-full", style: { background: brandColor } }),
1968
+ /* @__PURE__ */ jsx7("span", { children: "Syncs across your devices via iCloud / Google / 1Password" })
1969
+ ] })
1970
+ ] }),
1971
+ error && /* @__PURE__ */ jsx7("p", { className: "text-[11.5px] text-red-400", children: error }),
1972
+ /* @__PURE__ */ jsx7(
1973
+ "button",
1974
+ {
1975
+ type: "button",
1976
+ onClick: onAddPasskey,
1977
+ disabled: onboardingBusy !== null,
1978
+ className: "cursor-pointer w-full inline-flex items-center justify-center h-9 px-4 rounded-[10px] font-semibold text-[13px] tracking-tight transition hover:brightness-[0.94] active:brightness-[0.88] disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:brightness-100",
1979
+ style: ctaStyle,
1980
+ children: /* @__PURE__ */ jsx7("span", { className: "inline-flex items-center gap-1.5", style: ctaLabelStyle, children: onboardingBusy === "add" ? /* @__PURE__ */ jsx7(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsxs6(Fragment3, { children: [
1981
+ /* @__PURE__ */ jsx7(Fingerprint, { className: "size-4" }),
1982
+ " Add a passkey"
1983
+ ] }) })
1030
1984
  }
1031
1985
  ),
1032
- /* @__PURE__ */ jsx6("button", { type: "submit", disabled: busy, style: primaryBtnStyle, children: busy ? copy.verifyingLabel : submitLabel }),
1033
- /* @__PURE__ */ jsx6(
1986
+ /* @__PURE__ */ jsx7(
1034
1987
  "button",
1035
1988
  {
1036
1989
  type: "button",
1037
- onClick: () => {
1038
- setStep("identify");
1039
- setCode("");
1040
- setError(null);
1041
- },
1042
- disabled: busy,
1043
- style: {
1044
- marginTop: 12,
1045
- background: "none",
1046
- border: "none",
1047
- color: "#71717a",
1048
- fontSize: 12.5,
1049
- cursor: busy ? "not-allowed" : "pointer"
1990
+ onClick: onSkipPasskey,
1991
+ disabled: onboardingBusy !== null,
1992
+ className: "cursor-pointer w-full inline-flex items-center justify-center h-9 px-3 rounded-[8px] text-[13px] text-fg-2 hover:text-fg-1 hover:bg-surface-hover transition disabled:opacity-60 disabled:cursor-not-allowed",
1993
+ children: onboardingBusy === "skip" ? /* @__PURE__ */ jsx7(Loader2, { className: "size-4 animate-spin" }) : "Skip for now"
1994
+ }
1995
+ )
1996
+ ] }) : step === "recover" && recoverState ? (
1997
+ // Recovery gateway — user just signed back in to an app where
1998
+ // their membership is in a reversible off-state. Sign-in won't
1999
+ // complete until they pick Restore or Cancel. The SDK
2000
+ // component handles the API call; we just route the result
2001
+ // (final redirect on restore, sign-in URL on cancel).
2002
+ /* @__PURE__ */ jsx7(
2003
+ ElvixRecoverGate,
2004
+ {
2005
+ baseUrl,
2006
+ appName: recoverState.appName,
2007
+ state: recoverState.state,
2008
+ sinceAt: recoverState.sinceAt,
2009
+ onRestore: ({ redirect }) => {
2010
+ if (onAuthenticated) {
2011
+ onAuthenticated({ ok: true, redirect });
2012
+ } else {
2013
+ window.location.href = redirect;
2014
+ }
1050
2015
  },
1051
- children: "Use a different email"
2016
+ onCancel: ({ redirect }) => {
2017
+ window.location.href = redirect;
2018
+ }
1052
2019
  }
1053
2020
  )
1054
- ] }),
1055
- error && /* @__PURE__ */ jsx6("p", { role: "alert", style: { margin: 0, fontSize: 12.5, color: "#dc2626" }, children: error }),
1056
- hasLegal && step !== "done" && /* @__PURE__ */ jsxs5("p", { style: { margin: 0, fontSize: 11.5, lineHeight: 1.5, color: "#a1a1aa" }, children: [
1057
- "By continuing, you agree to ",
1058
- appName,
1059
- "'s",
1060
- " ",
1061
- termsUrl && /* @__PURE__ */ jsx6(
1062
- "a",
2021
+ ) : !anyMethod ? /* @__PURE__ */ jsx7("div", { className: "rounded-[10px] border border-dashed border-border-base bg-surface-hover py-8 px-4 text-center", children: /* @__PURE__ */ jsx7("p", { className: "text-[12.5px] text-fg-3", children: "Enable at least one method to preview the sign-in." }) }) : /* @__PURE__ */ jsxs6("form", { onSubmit: onSubmitIdentifier, className: "space-y-2", children: [
2022
+ gisEnabled && googleClientId && /* @__PURE__ */ jsx7(
2023
+ GoogleOneTap,
1063
2024
  {
1064
- href: termsUrl,
1065
- target: "_blank",
1066
- rel: "noopener noreferrer",
1067
- style: { color: "#71717a", textDecoration: "underline" },
1068
- children: "Terms of Service"
2025
+ baseUrl,
2026
+ clientId: googleClientId,
2027
+ intent,
2028
+ appClientId: clientId,
2029
+ renderButton: useGisRenderedButton,
2030
+ buttonContainerRef: gisButtonRef,
2031
+ config: {
2032
+ oneTap: googleConfig?.oneTap ?? false,
2033
+ autoSelect: googleConfig?.autoSelect ?? false,
2034
+ popup: googleConfig?.popup ?? false,
2035
+ fedcm: googleConfig?.fedcm ?? false,
2036
+ hostedDomain: googleConfig?.hostedDomain ?? ""
2037
+ }
1069
2038
  }
1070
2039
  ),
1071
- termsUrl && privacyUrl && " \xB7 ",
1072
- privacyUrl && /* @__PURE__ */ jsx6(
1073
- "a",
2040
+ (methodGoogle || methodPasskey) && /* @__PURE__ */ jsxs6(
2041
+ "div",
1074
2042
  {
1075
- href: privacyUrl,
1076
- target: "_blank",
1077
- rel: "noopener noreferrer",
1078
- style: { color: "#71717a", textDecoration: "underline" },
1079
- children: "Privacy Policy"
2043
+ className: socialLayout === "grid" && methodGoogle && methodPasskey ? "grid grid-cols-2 gap-2" : "space-y-2",
2044
+ children: [
2045
+ methodGoogle && (useGisRenderedButton ? (
2046
+ // GIS-rendered button respects ux_mode='popup' so the
2047
+ // OAuth flow runs in a small window instead of a full-
2048
+ // page redirect. Google styles this themselves; we
2049
+ // reserve the slot at our button height so layout stays
2050
+ // stable while GIS hydrates.
2051
+ /* @__PURE__ */ jsx7(
2052
+ "div",
2053
+ {
2054
+ ref: gisButtonRef,
2055
+ className: "w-full min-h-10",
2056
+ "aria-label": "Continue with Google"
2057
+ }
2058
+ )
2059
+ ) : /* @__PURE__ */ jsxs6(
2060
+ "a",
2061
+ {
2062
+ href: isPreview ? "#" : `${baseUrl}/api/auth/google/start?intent=${intent}${clientId ? `&clientId=${clientId}` : ""}`,
2063
+ onClick: isPreview ? (e) => e.preventDefault() : void 0,
2064
+ className: "cursor-pointer w-full inline-flex items-center justify-center gap-2 h-10 rounded-[10px] font-medium text-[13px] border border-border-base bg-surface text-fg-1 hover:bg-surface-hover transition",
2065
+ children: [
2066
+ /* @__PURE__ */ jsx7(GoogleGlyph, {}),
2067
+ socialLayout === "grid" && methodPasskey ? "Google" : "Continue with Google"
2068
+ ]
2069
+ }
2070
+ )),
2071
+ methodPasskey && /* @__PURE__ */ jsxs6(
2072
+ "button",
2073
+ {
2074
+ type: "button",
2075
+ disabled: passkeyBusy,
2076
+ onClick: isPreview ? void 0 : onPasskey,
2077
+ className: "cursor-pointer w-full inline-flex items-center justify-center gap-2 h-10 rounded-[10px] font-medium text-[13px] border border-border-base bg-surface text-fg-1 hover:bg-surface-hover transition disabled:cursor-not-allowed disabled:opacity-60",
2078
+ children: [
2079
+ passkeyBusy ? /* @__PURE__ */ jsx7(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx7(Fingerprint, { className: "size-4" }),
2080
+ socialLayout === "grid" && methodGoogle ? "Passkey" : "Continue with passkey"
2081
+ ]
2082
+ }
2083
+ )
2084
+ ]
1080
2085
  }
1081
- )
2086
+ ),
2087
+ (methodEmailOtp || methodUsername) && /* @__PURE__ */ jsxs6(Fragment3, { children: [
2088
+ (methodGoogle || methodPasskey) && /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-3 my-3", children: [
2089
+ /* @__PURE__ */ jsx7("span", { className: "h-px flex-1 bg-border-base" }),
2090
+ /* @__PURE__ */ jsx7("span", { className: "text-[11px] uppercase tracking-[0.08em] text-fg-3", children: "or" }),
2091
+ /* @__PURE__ */ jsx7("span", { className: "h-px flex-1 bg-border-base" })
2092
+ ] }),
2093
+ /* @__PURE__ */ jsx7(
2094
+ "input",
2095
+ {
2096
+ type: "text",
2097
+ disabled: sendingOtp,
2098
+ value: identifier,
2099
+ onChange: (e) => setIdentifier(e.target.value),
2100
+ placeholder: identifierPlaceholder,
2101
+ "aria-label": identifierPlaceholder,
2102
+ autoComplete: methodUsername ? "username" : "email",
2103
+ inputMode: methodUsername ? "text" : "email",
2104
+ className: "w-full h-10 px-3 rounded-[10px] bg-surface border border-border-strong text-[13px] text-fg-1 placeholder:text-placeholder focus:outline-none focus:border-[#8e7dff] focus:ring-2 focus:ring-[#8e7dff]/20 transition disabled:cursor-not-allowed"
2105
+ }
2106
+ ),
2107
+ /* @__PURE__ */ jsx7(
2108
+ "button",
2109
+ {
2110
+ type: "submit",
2111
+ disabled: !identifierValid || sendingOtp,
2112
+ className: "cursor-pointer w-full inline-flex items-center justify-center h-9 px-4 mt-3 rounded-[10px] font-semibold text-[13px] tracking-tight transition hover:brightness-[0.94] active:brightness-[0.88] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:brightness-100",
2113
+ style: ctaStyle,
2114
+ children: /* @__PURE__ */ jsxs6("span", { className: "inline-flex items-center gap-1.5", style: ctaLabelStyle, children: [
2115
+ sendingOtp ? /* @__PURE__ */ jsx7(Loader2, { className: "size-4 animate-spin" }) : "Continue",
2116
+ !sendingOtp && /* @__PURE__ */ jsx7("svg", { width: "11", height: "10", viewBox: "0 0 11 10", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx7(
2117
+ "path",
2118
+ {
2119
+ d: "M7.75 5L4.25 2.75V7.25L7.75 5Z",
2120
+ fill: "currentColor",
2121
+ stroke: "currentColor",
2122
+ opacity: "0.6",
2123
+ strokeWidth: "1.5",
2124
+ strokeLinecap: "round",
2125
+ strokeLinejoin: "round"
2126
+ }
2127
+ ) })
2128
+ ] })
2129
+ }
2130
+ ),
2131
+ error && /* @__PURE__ */ jsx7("p", { className: "text-[11.5px] text-red-400 text-center mt-1", children: error })
2132
+ ] })
1082
2133
  ] }),
1083
- /* @__PURE__ */ jsx6("div", { style: { display: "flex", justifyContent: "center", paddingTop: 2 }, children: /* @__PURE__ */ jsx6(ElvixSecuredBadge, { variant: "outline", theme: "light", size: "sm", accentColor: brand }) })
2134
+ step === "identifier" && belowMethods,
2135
+ /* @__PURE__ */ jsxs6("div", { className: "text-center mt-5 leading-[1.45]", children: [
2136
+ /* @__PURE__ */ jsxs6("div", { className: "text-[11.5px] text-placeholder", children: [
2137
+ "By continuing, you agree to ",
2138
+ appName || "the app",
2139
+ "'s"
2140
+ ] }),
2141
+ /* @__PURE__ */ jsxs6("div", { className: "text-[11.5px] mt-1 flex items-center justify-center gap-1.5", children: [
2142
+ /* @__PURE__ */ jsx7(LegalLink, { href: termsOfServiceUrl, children: "Terms of Service" }),
2143
+ /* @__PURE__ */ jsx7("span", { className: "text-placeholder", children: "\xB7" }),
2144
+ /* @__PURE__ */ jsx7(LegalLink, { href: privacyPolicyUrl, children: "Privacy Policy" })
2145
+ ] })
2146
+ ] })
1084
2147
  ] });
1085
2148
  }
1086
- function hexToRgba(hex, alpha) {
1087
- const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
1088
- if (!m) return hex;
1089
- let h = m[1];
1090
- if (h.length === 3)
1091
- h = h.split("").map((c) => c + c).join("");
1092
- const n = Number.parseInt(h, 16);
1093
- const r = n >> 16 & 255;
1094
- const g = n >> 8 & 255;
1095
- const b = n & 255;
1096
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
2149
+ function FramedPreview({ children }) {
2150
+ const hDash = "repeating-linear-gradient(to right, rgba(0,0,0,0.22) 0 4px, transparent 4px 8px)";
2151
+ const vDash = "repeating-linear-gradient(to bottom, rgba(0,0,0,0.22) 0 4px, transparent 4px 8px)";
2152
+ const OVERSHOOT = 20;
2153
+ return /* @__PURE__ */ jsxs6("div", { className: "relative bg-[#f5f5f6] dark:bg-surface-hover px-4 pt-4 pb-2", children: [
2154
+ /* @__PURE__ */ jsx7(
2155
+ "div",
2156
+ {
2157
+ "aria-hidden": true,
2158
+ className: "absolute top-0 h-px pointer-events-none",
2159
+ style: { left: -OVERSHOOT, right: -OVERSHOOT, backgroundImage: hDash }
2160
+ }
2161
+ ),
2162
+ /* @__PURE__ */ jsx7(
2163
+ "div",
2164
+ {
2165
+ "aria-hidden": true,
2166
+ className: "absolute bottom-0 h-px pointer-events-none",
2167
+ style: { left: -OVERSHOOT, right: -OVERSHOOT, backgroundImage: hDash }
2168
+ }
2169
+ ),
2170
+ /* @__PURE__ */ jsx7(
2171
+ "div",
2172
+ {
2173
+ "aria-hidden": true,
2174
+ className: "absolute left-0 w-px pointer-events-none",
2175
+ style: { top: -OVERSHOOT, bottom: -OVERSHOOT, backgroundImage: vDash }
2176
+ }
2177
+ ),
2178
+ /* @__PURE__ */ jsx7(
2179
+ "div",
2180
+ {
2181
+ "aria-hidden": true,
2182
+ className: "absolute right-0 w-px pointer-events-none",
2183
+ style: { top: -OVERSHOOT, bottom: -OVERSHOOT, backgroundImage: vDash }
2184
+ }
2185
+ ),
2186
+ /* @__PURE__ */ jsx7("div", { className: "relative", children }),
2187
+ /* @__PURE__ */ jsx7(
2188
+ "div",
2189
+ {
2190
+ className: "relative mt-3 py-2.5 text-center text-[12px] font-medium text-fg-3",
2191
+ style: {
2192
+ backgroundImage: "repeating-linear-gradient(45deg, transparent 0 6px, rgba(0,0,0,0.06) 6px 7px)"
2193
+ },
2194
+ children: "This is a preview"
2195
+ }
2196
+ )
2197
+ ] });
2198
+ }
2199
+ function LegalLink({ href, children }) {
2200
+ const base = "cursor-pointer font-semibold text-fg-2 underline underline-offset-2 decoration-fg-3/60 hover:text-fg-1 hover:decoration-fg-1 transition";
2201
+ if (!href) return /* @__PURE__ */ jsx7("span", { className: base, children });
2202
+ return /* @__PURE__ */ jsx7("a", { href, target: "_blank", rel: "noopener noreferrer", className: base, children });
2203
+ }
2204
+ function GoogleGlyph() {
2205
+ return /* @__PURE__ */ jsxs6("svg", { viewBox: "0 0 18 18", className: "size-4", "aria-hidden": true, children: [
2206
+ /* @__PURE__ */ jsx7(
2207
+ "path",
2208
+ {
2209
+ fill: "#4285F4",
2210
+ d: "M16.51 8.18c0-.58-.05-1.13-.15-1.66H9v3.14h4.21a3.6 3.6 0 0 1-1.56 2.36v1.96h2.52c1.47-1.36 2.34-3.36 2.34-5.8z"
2211
+ }
2212
+ ),
2213
+ /* @__PURE__ */ jsx7(
2214
+ "path",
2215
+ {
2216
+ fill: "#34A853",
2217
+ d: "M9 17c2.1 0 3.87-.7 5.17-1.9l-2.52-1.96c-.7.47-1.6.74-2.65.74-2.04 0-3.77-1.38-4.38-3.23H2.02v2.03A8 8 0 0 0 9 17z"
2218
+ }
2219
+ ),
2220
+ /* @__PURE__ */ jsx7(
2221
+ "path",
2222
+ {
2223
+ fill: "#FBBC05",
2224
+ d: "M4.62 10.65A4.8 4.8 0 0 1 4.36 9c0-.57.1-1.12.26-1.65V5.32H2.02A8 8 0 0 0 1 9c0 1.29.31 2.5.86 3.58l2.51-1.93z"
2225
+ }
2226
+ ),
2227
+ /* @__PURE__ */ jsx7(
2228
+ "path",
2229
+ {
2230
+ fill: "#EA4335",
2231
+ d: "M9 4.77c1.14 0 2.17.4 2.98 1.17l2.23-2.23A7.84 7.84 0 0 0 9 1 8 8 0 0 0 1.86 5.32l2.51 1.93C5.23 5.17 6.96 4.77 9 4.77z"
2232
+ }
2233
+ )
2234
+ ] });
2235
+ }
2236
+ function humanError(code, retryAfterSeconds) {
2237
+ switch (code) {
2238
+ case "too_recent":
2239
+ return retryAfterSeconds ? `Wait ${retryAfterSeconds}s before requesting another code.` : "Wait a moment before requesting another code.";
2240
+ case "too_many":
2241
+ return retryAfterSeconds ? `Too many codes sent to this email. Try again in ${formatRetry(retryAfterSeconds)}.` : "Too many codes sent to this email. Try again later.";
2242
+ case "invalid_code":
2243
+ return "That code didn't work. Try again.";
2244
+ case "expired":
2245
+ return "That code expired. Send a new one.";
2246
+ case "send_failed":
2247
+ return "Couldn't send the email. Check the address.";
2248
+ case "user_paused":
2249
+ return "Your access has been paused. Contact support.";
2250
+ case "user_banned":
2251
+ return "Access denied.";
2252
+ case "username_not_found":
2253
+ return "No account with that username here.";
2254
+ case "method_disabled":
2255
+ return "That sign-in method isn't enabled for this app.";
2256
+ default:
2257
+ return "Something went wrong. Try again.";
2258
+ }
2259
+ }
2260
+ function formatRetry(seconds) {
2261
+ if (seconds < 60) return `${seconds}s`;
2262
+ const m = Math.ceil(seconds / 60);
2263
+ return m === 1 ? "1 minute" : `${m} minutes`;
2264
+ }
2265
+ function defaultRedirect(intent) {
2266
+ switch (intent) {
2267
+ case "console":
2268
+ return "/console";
2269
+ case "account":
2270
+ return "/account";
2271
+ default:
2272
+ return "/";
2273
+ }
2274
+ }
2275
+ function usernameReasonLabel(reason) {
2276
+ switch (reason) {
2277
+ case "blank":
2278
+ return "Pick a username.";
2279
+ case "too_short":
2280
+ return "Min 4 characters.";
2281
+ case "too_long":
2282
+ return "Max 30 characters.";
2283
+ case "only_numbers":
2284
+ return "Cannot be all numbers.";
2285
+ case "bad_start":
2286
+ return "Must start with a letter.";
2287
+ case "bad_chars":
2288
+ return "Only a\u2013z, 0\u20139, dot, underscore.";
2289
+ case "consecutive_special":
2290
+ return "No two dots or underscores in a row.";
2291
+ case "trailing_special":
2292
+ return "Cannot end with a dot or underscore.";
2293
+ case "taken":
2294
+ return "That one's taken.";
2295
+ default:
2296
+ return "That username isn't valid.";
2297
+ }
1097
2298
  }
1098
2299
 
1099
2300
  // src/react/elvix-sign-in-button.tsx
1100
- import { useState as useState4 } from "react";
1101
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1102
- var ELVIX_URL2 = "https://elvix.is";
2301
+ import { useState as useState6 } from "react";
2302
+
2303
+ // src/react/elvix-shield.tsx
2304
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2305
+ function ElvixShield({
2306
+ size,
2307
+ fill,
2308
+ accent
2309
+ }) {
2310
+ return /* @__PURE__ */ jsxs7("svg", { width: size, height: size, viewBox: "2 2 20 20", "aria-hidden": true, style: { display: "block" }, children: [
2311
+ /* @__PURE__ */ jsx8(
2312
+ "path",
2313
+ {
2314
+ fill,
2315
+ fillRule: "evenodd",
2316
+ d: "M 6 2.5 C 4.34 2.5 3 3.84 3 5.5 L 3 12.5 C 3 17.5 7 20.7 12 22 C 17 20.7 21 17.5 21 12.5 L 21 5.5 C 21 3.84 19.66 2.5 18 2.5 L 6 2.5 Z M 12 8.4 C 9.79 8.4 8 10.19 8 12.4 C 8 14.61 9.79 16.4 12 16.4 C 13.21 16.4 14.3 15.86 15.04 15 L 13.6 13.77 C 13.21 14.23 12.64 14.5 12 14.5 C 11.04 14.5 10.21 13.86 9.91 13 L 15.95 13 C 15.98 12.8 16 12.6 16 12.4 C 16 10.19 14.21 8.4 12 8.4 Z M 9.91 11.8 L 14.09 11.8 C 13.79 10.94 12.96 10.3 12 10.3 C 11.04 10.3 10.21 10.94 9.91 11.8 Z"
2317
+ }
2318
+ ),
2319
+ /* @__PURE__ */ jsx8("circle", { cx: "19.5", cy: "4.5", r: "2.4", fill: accent })
2320
+ ] });
2321
+ }
2322
+
2323
+ // src/react/elvix-sign-in-button.tsx
2324
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2325
+ var ELVIX_URL = "https://elvix.is";
1103
2326
  var PRESET_LABEL = {
1104
2327
  "sign-in-with-elvix": "Sign in with elvix",
1105
2328
  "continue-with-elvix": "Continue with elvix",
@@ -1138,7 +2361,7 @@ function shieldColor(variant, theme) {
1138
2361
  }
1139
2362
  function ElvixSignInButton({
1140
2363
  clientId,
1141
- baseUrl = ELVIX_URL2,
2364
+ baseUrl = ELVIX_URL,
1142
2365
  returnUrl,
1143
2366
  type = "standard",
1144
2367
  variant = "filled",
@@ -1160,7 +2383,7 @@ function ElvixSignInButton({
1160
2383
  maxHeight
1161
2384
  }) {
1162
2385
  const sized = sizeStyle({ width, height, minWidth, maxWidth, minHeight, maxHeight });
1163
- const [embedOpen, setEmbedOpen] = useState4(false);
2386
+ const [embedOpen, setEmbedOpen] = useState6(false);
1164
2387
  const isIcon = type === "icon";
1165
2388
  const resolvedLabel = label ?? PRESET_LABEL[preset];
1166
2389
  const tone = variantTone(variant, theme);
@@ -1185,16 +2408,16 @@ function ElvixSignInButton({
1185
2408
  // Dimensional overrides win over the size preset above.
1186
2409
  ...sized
1187
2410
  };
1188
- const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1189
- /* @__PURE__ */ jsx7(ElvixShield, { size: ICON_SIZE[size], fill: shieldColor(variant, theme), accent: "#8e7dff" }),
1190
- isIcon ? null : /* @__PURE__ */ jsx7("span", { children: resolvedLabel })
2411
+ const content = /* @__PURE__ */ jsxs8(Fragment4, { children: [
2412
+ /* @__PURE__ */ jsx9(ElvixShield, { size: ICON_SIZE[size], fill: shieldColor(variant, theme), accent: "#8e7dff" }),
2413
+ isIcon ? null : /* @__PURE__ */ jsx9("span", { children: resolvedLabel })
1191
2414
  ] });
1192
2415
  if (mode === "callback") {
1193
- return /* @__PURE__ */ jsx7("button", { type: "button", onClick, className, style, "aria-label": isIcon ? resolvedLabel : void 0, children: content });
2416
+ return /* @__PURE__ */ jsx9("button", { type: "button", onClick, className, style, "aria-label": isIcon ? resolvedLabel : void 0, children: content });
1194
2417
  }
1195
2418
  if (mode === "embed") {
1196
- return /* @__PURE__ */ jsxs6("div", { "data-elvix-signin-button-embed": "", style: sized, children: [
1197
- !embedOpen && /* @__PURE__ */ jsx7(
2419
+ return /* @__PURE__ */ jsxs8("div", { "data-elvix-signin-button-embed": "", style: sized, children: [
2420
+ !embedOpen && /* @__PURE__ */ jsx9(
1198
2421
  "button",
1199
2422
  {
1200
2423
  type: "button",
@@ -1205,7 +2428,7 @@ function ElvixSignInButton({
1205
2428
  children: content
1206
2429
  }
1207
2430
  ),
1208
- embedOpen && /* @__PURE__ */ jsx7(
2431
+ embedOpen && /* @__PURE__ */ jsx9(
1209
2432
  ElvixSignIn,
1210
2433
  {
1211
2434
  onResult: (r) => {
@@ -1222,18 +2445,95 @@ function ElvixSignInButton({
1222
2445
  const sep = base.includes("?") ? "&" : "?";
1223
2446
  return `${base}${sep}return=${encodeURIComponent(returnUrl)}`;
1224
2447
  })();
1225
- return /* @__PURE__ */ jsx7("a", { href: destination, className, style, "aria-label": isIcon ? resolvedLabel : void 0, children: content });
2448
+ return /* @__PURE__ */ jsx9("a", { href: destination, className, style, "aria-label": isIcon ? resolvedLabel : void 0, children: content });
2449
+ }
2450
+
2451
+ // src/react/elvix-secured-badge.tsx
2452
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2453
+ var ELVIX_URL2 = "https://elvix.is";
2454
+ var SIZE = {
2455
+ sm: { height: 28, padX: 10, font: 11.5, icon: 14, gap: 6 },
2456
+ md: { height: 32, padX: 12, font: 12.5, icon: 16, gap: 7 },
2457
+ lg: { height: 36, padX: 14, font: 13, icon: 18, gap: 8 }
2458
+ };
2459
+ var TONE = {
2460
+ white: {
2461
+ light: { bg: "#ffffff", border: "#e4e4e7", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" },
2462
+ dark: { bg: "#ffffff", border: "transparent", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" }
2463
+ },
2464
+ dark: {
2465
+ light: { bg: "#0a0a0b", border: "rgba(0,0,0,0.1)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" },
2466
+ dark: { bg: "#0a0a0b", border: "rgba(255,255,255,0.1)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" }
2467
+ },
2468
+ outline: {
2469
+ light: { bg: "transparent", border: "rgba(0,0,0,0.15)", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" },
2470
+ dark: { bg: "transparent", border: "rgba(142,125,255,0.4)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" }
2471
+ }
2472
+ };
2473
+ function ElvixSecuredBadge({
2474
+ variant = "white",
2475
+ size = "md",
2476
+ theme = "dark",
2477
+ accentColor = "#8e7dff",
2478
+ href = ELVIX_URL2,
2479
+ className = "",
2480
+ width,
2481
+ height,
2482
+ minWidth,
2483
+ maxWidth,
2484
+ minHeight,
2485
+ maxHeight
2486
+ }) {
2487
+ const s = SIZE[size];
2488
+ const t = TONE[variant][theme];
2489
+ const style = {
2490
+ display: "inline-flex",
2491
+ alignItems: "center",
2492
+ gap: s.gap,
2493
+ height: s.height,
2494
+ paddingLeft: s.padX,
2495
+ paddingRight: s.padX,
2496
+ fontSize: s.font,
2497
+ fontWeight: 500,
2498
+ borderRadius: 9999,
2499
+ background: t.bg,
2500
+ border: `1px solid ${t.border}`,
2501
+ color: t.brand,
2502
+ textDecoration: "none",
2503
+ userSelect: "none",
2504
+ lineHeight: 1,
2505
+ // Dimensional overrides win over the size preset above.
2506
+ ...sizeStyle({ width, height, minWidth, maxWidth, minHeight, maxHeight })
2507
+ };
2508
+ return /* @__PURE__ */ jsxs9(
2509
+ "a",
2510
+ {
2511
+ href,
2512
+ target: "_blank",
2513
+ rel: "noopener noreferrer",
2514
+ className,
2515
+ style,
2516
+ "data-elvix-secured-badge": "",
2517
+ children: [
2518
+ /* @__PURE__ */ jsx10(ElvixShield, { size: s.icon, fill: t.shield, accent: accentColor }),
2519
+ /* @__PURE__ */ jsxs9("span", { style: { display: "inline-flex", alignItems: "baseline", gap: 4 }, children: [
2520
+ /* @__PURE__ */ jsx10("span", { style: { color: t.lead }, children: "Secured by" }),
2521
+ /* @__PURE__ */ jsx10("span", { style: { color: t.brand, fontWeight: 600 }, children: "elvix" })
2522
+ ] })
2523
+ ]
2524
+ }
2525
+ );
1226
2526
  }
1227
2527
 
1228
2528
  // src/react/hooks.ts
1229
- import { useCallback, useEffect as useEffect2, useState as useState5 } from "react";
2529
+ import { useCallback as useCallback4, useEffect as useEffect5, useState as useState7 } from "react";
1230
2530
  var POLL_MS = 7e3;
1231
2531
  function useUserList(kind, opts) {
1232
2532
  const { applicationId, baseUrl = "", pollMs = POLL_MS } = opts;
1233
- const [slugs, setSlugs] = useState5([]);
1234
- const [loading, setLoading] = useState5(true);
1235
- const [error, setError] = useState5(null);
1236
- const refresh = useCallback(async () => {
2533
+ const [slugs, setSlugs] = useState7([]);
2534
+ const [loading, setLoading] = useState7(true);
2535
+ const [error, setError] = useState7(null);
2536
+ const refresh = useCallback4(async () => {
1237
2537
  setError(null);
1238
2538
  try {
1239
2539
  const res = await fetch(
@@ -1252,7 +2552,7 @@ function useUserList(kind, opts) {
1252
2552
  setLoading(false);
1253
2553
  }
1254
2554
  }, [applicationId, baseUrl, kind]);
1255
- useEffect2(() => {
2555
+ useEffect5(() => {
1256
2556
  refresh();
1257
2557
  const id = setInterval(refresh, pollMs);
1258
2558
  return () => clearInterval(id);
@@ -1264,13 +2564,13 @@ var useUserScopes = (opts) => useUserList("scopes", opts);
1264
2564
  var useUserMemberships = (opts) => useUserList("memberships", opts);
1265
2565
 
1266
2566
  // src/react/lifecycle-watcher.tsx
1267
- import { useEffect as useEffect3 } from "react";
2567
+ import { useEffect as useEffect6 } from "react";
1268
2568
  function ElvixLifecycleWatcher({
1269
2569
  baseUrl = "",
1270
2570
  pollMs = 7e3,
1271
2571
  onSignedOut
1272
2572
  }) {
1273
- useEffect3(() => {
2573
+ useEffect6(() => {
1274
2574
  let cancelled = false;
1275
2575
  let fired = false;
1276
2576
  const poll = async () => {
@@ -1299,7 +2599,7 @@ function ElvixLifecycleWatcher({
1299
2599
  }
1300
2600
 
1301
2601
  // src/react/elvix-username.tsx
1302
- import { useState as useState6 } from "react";
2602
+ import { useState as useState8 } from "react";
1303
2603
 
1304
2604
  // src/react/lib.ts
1305
2605
  async function appPost(opts, path, body) {
@@ -1357,7 +2657,7 @@ async function appDelete(opts, path) {
1357
2657
  }
1358
2658
 
1359
2659
  // src/react/elvix-username.tsx
1360
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2660
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1361
2661
  function ElvixUsername({
1362
2662
  onResult,
1363
2663
  width,
@@ -1369,10 +2669,10 @@ function ElvixUsername({
1369
2669
  }) {
1370
2670
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1371
2671
  const ctx = useElvixContext();
1372
- const [value, setValue] = useState6("");
1373
- const [busy, setBusy] = useState6(false);
1374
- const [error, setError] = useState6(null);
1375
- const [done, setDone] = useState6(null);
2672
+ const [value, setValue] = useState8("");
2673
+ const [busy, setBusy] = useState8(false);
2674
+ const [error, setError] = useState8(null);
2675
+ const [done, setDone] = useState8(null);
1376
2676
  async function submit(e) {
1377
2677
  e.preventDefault();
1378
2678
  if (!ctx.app) return;
@@ -1392,17 +2692,17 @@ function ElvixUsername({
1392
2692
  onResult?.(result);
1393
2693
  }
1394
2694
  if (done) {
1395
- return /* @__PURE__ */ jsx8(ElvixCard, { title: "Username saved", ...sizeProps, children: /* @__PURE__ */ jsxs7("p", { children: [
2695
+ return /* @__PURE__ */ jsx11(ElvixCard, { title: "Username saved", ...sizeProps, children: /* @__PURE__ */ jsxs10("p", { children: [
1396
2696
  "You are now ",
1397
- /* @__PURE__ */ jsxs7("strong", { children: [
2697
+ /* @__PURE__ */ jsxs10("strong", { children: [
1398
2698
  "@",
1399
2699
  done
1400
2700
  ] }),
1401
2701
  "."
1402
2702
  ] }) });
1403
2703
  }
1404
- return /* @__PURE__ */ jsx8(ElvixCard, { title: "Choose a username", ...sizeProps, children: /* @__PURE__ */ jsxs7("form", { onSubmit: submit, className: "elvix-form", children: [
1405
- /* @__PURE__ */ jsx8(
2704
+ return /* @__PURE__ */ jsx11(ElvixCard, { title: "Choose a username", ...sizeProps, children: /* @__PURE__ */ jsxs10("form", { onSubmit: submit, className: "elvix-form", children: [
2705
+ /* @__PURE__ */ jsx11(
1406
2706
  "input",
1407
2707
  {
1408
2708
  type: "text",
@@ -1415,14 +2715,14 @@ function ElvixUsername({
1415
2715
  className: "elvix-input"
1416
2716
  }
1417
2717
  ),
1418
- /* @__PURE__ */ jsx8("button", { type: "submit", disabled: busy || value.length < 4, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Claim" }),
1419
- error && /* @__PURE__ */ jsx8("p", { role: "alert", className: "elvix-error", children: error })
2718
+ /* @__PURE__ */ jsx11("button", { type: "submit", disabled: busy || value.length < 4, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Claim" }),
2719
+ error && /* @__PURE__ */ jsx11("p", { role: "alert", className: "elvix-error", children: error })
1420
2720
  ] }) });
1421
2721
  }
1422
2722
 
1423
2723
  // src/react/elvix-avatar.tsx
1424
- import { useState as useState7 } from "react";
1425
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2724
+ import { useState as useState9 } from "react";
2725
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1426
2726
  function ElvixAvatar({
1427
2727
  onResult,
1428
2728
  width,
@@ -1434,9 +2734,9 @@ function ElvixAvatar({
1434
2734
  }) {
1435
2735
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1436
2736
  const ctx = useElvixContext();
1437
- const [busy, setBusy] = useState7(false);
1438
- const [error, setError] = useState7(null);
1439
- const [preview, setPreview] = useState7(null);
2737
+ const [busy, setBusy] = useState9(false);
2738
+ const [error, setError] = useState9(null);
2739
+ const [preview, setPreview] = useState9(null);
1440
2740
  async function onFile(e) {
1441
2741
  const file = e.target.files?.[0];
1442
2742
  if (!file || !ctx.app) return;
@@ -1459,8 +2759,8 @@ function ElvixAvatar({
1459
2759
  if (!result.ok) setError(result.error);
1460
2760
  onResult?.(result);
1461
2761
  }
1462
- return /* @__PURE__ */ jsxs8(ElvixCard, { title: "Avatar", ...sizeProps, children: [
1463
- preview && /* @__PURE__ */ jsx9(
2762
+ return /* @__PURE__ */ jsxs11(ElvixCard, { title: "Avatar", ...sizeProps, children: [
2763
+ preview && /* @__PURE__ */ jsx12(
1464
2764
  "img",
1465
2765
  {
1466
2766
  src: preview,
@@ -1468,15 +2768,15 @@ function ElvixAvatar({
1468
2768
  style: { width: 96, height: 96, borderRadius: "50%", objectFit: "cover", marginBottom: 12 }
1469
2769
  }
1470
2770
  ),
1471
- /* @__PURE__ */ jsx9("input", { type: "file", accept: "image/png,image/jpeg,image/webp", onChange: onFile, disabled: busy }),
1472
- busy && /* @__PURE__ */ jsx9("p", { children: "Uploading\u2026" }),
1473
- error && /* @__PURE__ */ jsx9("p", { role: "alert", className: "elvix-error", children: error })
2771
+ /* @__PURE__ */ jsx12("input", { type: "file", accept: "image/png,image/jpeg,image/webp", onChange: onFile, disabled: busy }),
2772
+ busy && /* @__PURE__ */ jsx12("p", { children: "Uploading\u2026" }),
2773
+ error && /* @__PURE__ */ jsx12("p", { role: "alert", className: "elvix-error", children: error })
1474
2774
  ] });
1475
2775
  }
1476
2776
 
1477
2777
  // src/react/elvix-banner.tsx
1478
- import { useState as useState8 } from "react";
1479
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2778
+ import { useState as useState10 } from "react";
2779
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1480
2780
  function ElvixBanner({
1481
2781
  onResult,
1482
2782
  width,
@@ -1488,9 +2788,9 @@ function ElvixBanner({
1488
2788
  }) {
1489
2789
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1490
2790
  const ctx = useElvixContext();
1491
- const [busy, setBusy] = useState8(false);
1492
- const [error, setError] = useState8(null);
1493
- const [preview, setPreview] = useState8(null);
2791
+ const [busy, setBusy] = useState10(false);
2792
+ const [error, setError] = useState10(null);
2793
+ const [preview, setPreview] = useState10(null);
1494
2794
  async function onFile(e) {
1495
2795
  const file = e.target.files?.[0];
1496
2796
  if (!file || !ctx.app) return;
@@ -1513,8 +2813,8 @@ function ElvixBanner({
1513
2813
  if (!result.ok) setError(result.error);
1514
2814
  onResult?.(result);
1515
2815
  }
1516
- return /* @__PURE__ */ jsxs9(ElvixCard, { title: "Banner", ...sizeProps, children: [
1517
- preview && /* @__PURE__ */ jsx10(
2816
+ return /* @__PURE__ */ jsxs12(ElvixCard, { title: "Banner", ...sizeProps, children: [
2817
+ preview && /* @__PURE__ */ jsx13(
1518
2818
  "img",
1519
2819
  {
1520
2820
  src: preview,
@@ -1522,15 +2822,15 @@ function ElvixBanner({
1522
2822
  style: { width: "100%", aspectRatio: "16/9", objectFit: "cover", borderRadius: 10, marginBottom: 12 }
1523
2823
  }
1524
2824
  ),
1525
- /* @__PURE__ */ jsx10("input", { type: "file", accept: "image/png,image/jpeg,image/webp", onChange: onFile, disabled: busy }),
1526
- busy && /* @__PURE__ */ jsx10("p", { children: "Uploading\u2026" }),
1527
- error && /* @__PURE__ */ jsx10("p", { role: "alert", className: "elvix-error", children: error })
2825
+ /* @__PURE__ */ jsx13("input", { type: "file", accept: "image/png,image/jpeg,image/webp", onChange: onFile, disabled: busy }),
2826
+ busy && /* @__PURE__ */ jsx13("p", { children: "Uploading\u2026" }),
2827
+ error && /* @__PURE__ */ jsx13("p", { role: "alert", className: "elvix-error", children: error })
1528
2828
  ] });
1529
2829
  }
1530
2830
 
1531
2831
  // src/react/elvix-identity-form.tsx
1532
- import { useState as useState9 } from "react";
1533
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2832
+ import { useState as useState11 } from "react";
2833
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1534
2834
  function ElvixIdentityForm({
1535
2835
  initialName = "",
1536
2836
  initialBio = "",
@@ -1544,11 +2844,11 @@ function ElvixIdentityForm({
1544
2844
  }) {
1545
2845
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1546
2846
  const ctx = useElvixContext();
1547
- const [name, setName] = useState9(initialName);
1548
- const [bio, setBio] = useState9(initialBio);
1549
- const [busy, setBusy] = useState9(false);
1550
- const [error, setError] = useState9(null);
1551
- const [saved, setSaved] = useState9(false);
2847
+ const [name, setName] = useState11(initialName);
2848
+ const [bio, setBio] = useState11(initialBio);
2849
+ const [busy, setBusy] = useState11(false);
2850
+ const [error, setError] = useState11(null);
2851
+ const [saved, setSaved] = useState11(false);
1552
2852
  async function submit(e) {
1553
2853
  e.preventDefault();
1554
2854
  if (!ctx.app) return;
@@ -1564,24 +2864,24 @@ function ElvixIdentityForm({
1564
2864
  else setSaved(true);
1565
2865
  onResult?.(result);
1566
2866
  }
1567
- return /* @__PURE__ */ jsx11(ElvixCard, { title: "Identity", ...sizeProps, children: /* @__PURE__ */ jsxs10("form", { onSubmit: submit, className: "elvix-form", children: [
1568
- /* @__PURE__ */ jsxs10("label", { children: [
2867
+ return /* @__PURE__ */ jsx14(ElvixCard, { title: "Identity", ...sizeProps, children: /* @__PURE__ */ jsxs13("form", { onSubmit: submit, className: "elvix-form", children: [
2868
+ /* @__PURE__ */ jsxs13("label", { children: [
1569
2869
  "Name",
1570
- /* @__PURE__ */ jsx11("input", { value: name, onChange: (e) => setName(e.target.value), maxLength: 80, disabled: busy, className: "elvix-input" })
2870
+ /* @__PURE__ */ jsx14("input", { value: name, onChange: (e) => setName(e.target.value), maxLength: 80, disabled: busy, className: "elvix-input" })
1571
2871
  ] }),
1572
- /* @__PURE__ */ jsxs10("label", { children: [
2872
+ /* @__PURE__ */ jsxs13("label", { children: [
1573
2873
  "Bio",
1574
- /* @__PURE__ */ jsx11("textarea", { value: bio, onChange: (e) => setBio(e.target.value), maxLength: 500, rows: 3, disabled: busy, className: "elvix-input" })
2874
+ /* @__PURE__ */ jsx14("textarea", { value: bio, onChange: (e) => setBio(e.target.value), maxLength: 500, rows: 3, disabled: busy, className: "elvix-input" })
1575
2875
  ] }),
1576
- /* @__PURE__ */ jsx11("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" }),
1577
- saved && /* @__PURE__ */ jsx11("p", { className: "elvix-muted", children: "Saved." }),
1578
- error && /* @__PURE__ */ jsx11("p", { role: "alert", className: "elvix-error", children: error })
2876
+ /* @__PURE__ */ jsx14("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" }),
2877
+ saved && /* @__PURE__ */ jsx14("p", { className: "elvix-muted", children: "Saved." }),
2878
+ error && /* @__PURE__ */ jsx14("p", { role: "alert", className: "elvix-error", children: error })
1579
2879
  ] }) });
1580
2880
  }
1581
2881
 
1582
2882
  // src/react/elvix-region.tsx
1583
- import { useState as useState10 } from "react";
1584
- import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2883
+ import { useState as useState12 } from "react";
2884
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
1585
2885
  function ElvixRegion({
1586
2886
  initialCountry = "",
1587
2887
  initialTimezone = "",
@@ -1595,11 +2895,11 @@ function ElvixRegion({
1595
2895
  }) {
1596
2896
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1597
2897
  const ctx = useElvixContext();
1598
- const [country, setCountry] = useState10(initialCountry);
1599
- const [timezone, setTimezone] = useState10(initialTimezone);
1600
- const [busy, setBusy] = useState10(false);
1601
- const [error, setError] = useState10(null);
1602
- const [saved, setSaved] = useState10(false);
2898
+ const [country, setCountry] = useState12(initialCountry);
2899
+ const [timezone, setTimezone] = useState12(initialTimezone);
2900
+ const [busy, setBusy] = useState12(false);
2901
+ const [error, setError] = useState12(null);
2902
+ const [saved, setSaved] = useState12(false);
1603
2903
  async function submit(e) {
1604
2904
  e.preventDefault();
1605
2905
  if (!ctx.app) return;
@@ -1615,24 +2915,24 @@ function ElvixRegion({
1615
2915
  else setSaved(true);
1616
2916
  onResult?.(result);
1617
2917
  }
1618
- return /* @__PURE__ */ jsx12(ElvixCard, { title: "Region", ...sizeProps, children: /* @__PURE__ */ jsxs11("form", { onSubmit: submit, className: "elvix-form", children: [
1619
- /* @__PURE__ */ jsxs11("label", { children: [
2918
+ return /* @__PURE__ */ jsx15(ElvixCard, { title: "Region", ...sizeProps, children: /* @__PURE__ */ jsxs14("form", { onSubmit: submit, className: "elvix-form", children: [
2919
+ /* @__PURE__ */ jsxs14("label", { children: [
1620
2920
  "Country (ISO-2)",
1621
- /* @__PURE__ */ jsx12("input", { value: country, onChange: (e) => setCountry(e.target.value.toUpperCase()), maxLength: 2, pattern: "[A-Z]{2}", disabled: busy, className: "elvix-input" })
2921
+ /* @__PURE__ */ jsx15("input", { value: country, onChange: (e) => setCountry(e.target.value.toUpperCase()), maxLength: 2, pattern: "[A-Z]{2}", disabled: busy, className: "elvix-input" })
1622
2922
  ] }),
1623
- /* @__PURE__ */ jsxs11("label", { children: [
2923
+ /* @__PURE__ */ jsxs14("label", { children: [
1624
2924
  "Timezone",
1625
- /* @__PURE__ */ jsx12("input", { value: timezone, onChange: (e) => setTimezone(e.target.value), placeholder: "Europe/Berlin", disabled: busy, className: "elvix-input" })
2925
+ /* @__PURE__ */ jsx15("input", { value: timezone, onChange: (e) => setTimezone(e.target.value), placeholder: "Europe/Berlin", disabled: busy, className: "elvix-input" })
1626
2926
  ] }),
1627
- /* @__PURE__ */ jsx12("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" }),
1628
- saved && /* @__PURE__ */ jsx12("p", { className: "elvix-muted", children: "Saved." }),
1629
- error && /* @__PURE__ */ jsx12("p", { role: "alert", className: "elvix-error", children: error })
2927
+ /* @__PURE__ */ jsx15("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" }),
2928
+ saved && /* @__PURE__ */ jsx15("p", { className: "elvix-muted", children: "Saved." }),
2929
+ error && /* @__PURE__ */ jsx15("p", { role: "alert", className: "elvix-error", children: error })
1630
2930
  ] }) });
1631
2931
  }
1632
2932
 
1633
2933
  // src/react/elvix-languages.tsx
1634
- import { useState as useState11 } from "react";
1635
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2934
+ import { useState as useState13 } from "react";
2935
+ import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
1636
2936
  function ElvixLanguages({
1637
2937
  initial = [],
1638
2938
  onResult,
@@ -1645,10 +2945,10 @@ function ElvixLanguages({
1645
2945
  }) {
1646
2946
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1647
2947
  const ctx = useElvixContext();
1648
- const [raw, setRaw] = useState11(initial.join(", "));
1649
- const [busy, setBusy] = useState11(false);
1650
- const [error, setError] = useState11(null);
1651
- const [saved, setSaved] = useState11(false);
2948
+ const [raw, setRaw] = useState13(initial.join(", "));
2949
+ const [busy, setBusy] = useState13(false);
2950
+ const [error, setError] = useState13(null);
2951
+ const [saved, setSaved] = useState13(false);
1652
2952
  async function submit(e) {
1653
2953
  e.preventDefault();
1654
2954
  if (!ctx.app) return;
@@ -1665,20 +2965,20 @@ function ElvixLanguages({
1665
2965
  else setSaved(true);
1666
2966
  onResult?.(result);
1667
2967
  }
1668
- return /* @__PURE__ */ jsx13(ElvixCard, { title: "Languages", ...sizeProps, children: /* @__PURE__ */ jsxs12("form", { onSubmit: submit, className: "elvix-form", children: [
1669
- /* @__PURE__ */ jsxs12("label", { children: [
2968
+ return /* @__PURE__ */ jsx16(ElvixCard, { title: "Languages", ...sizeProps, children: /* @__PURE__ */ jsxs15("form", { onSubmit: submit, className: "elvix-form", children: [
2969
+ /* @__PURE__ */ jsxs15("label", { children: [
1670
2970
  "Preferred languages (comma-separated BCP-47 tags)",
1671
- /* @__PURE__ */ jsx13("input", { value: raw, onChange: (e) => setRaw(e.target.value), placeholder: "en-GB, de-DE", disabled: busy, className: "elvix-input" })
2971
+ /* @__PURE__ */ jsx16("input", { value: raw, onChange: (e) => setRaw(e.target.value), placeholder: "en-GB, de-DE", disabled: busy, className: "elvix-input" })
1672
2972
  ] }),
1673
- /* @__PURE__ */ jsx13("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" }),
1674
- saved && /* @__PURE__ */ jsx13("p", { className: "elvix-muted", children: "Saved." }),
1675
- error && /* @__PURE__ */ jsx13("p", { role: "alert", className: "elvix-error", children: error })
2973
+ /* @__PURE__ */ jsx16("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" }),
2974
+ saved && /* @__PURE__ */ jsx16("p", { className: "elvix-muted", children: "Saved." }),
2975
+ error && /* @__PURE__ */ jsx16("p", { role: "alert", className: "elvix-error", children: error })
1676
2976
  ] }) });
1677
2977
  }
1678
2978
 
1679
2979
  // src/react/elvix-sessions.tsx
1680
- import { useEffect as useEffect4, useState as useState12 } from "react";
1681
- import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2980
+ import { useEffect as useEffect7, useState as useState14 } from "react";
2981
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
1682
2982
  function ElvixSessions({
1683
2983
  onResult,
1684
2984
  width,
@@ -1690,10 +2990,10 @@ function ElvixSessions({
1690
2990
  }) {
1691
2991
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1692
2992
  const ctx = useElvixContext();
1693
- const [rows, setRows] = useState12(null);
1694
- const [error, setError] = useState12(null);
1695
- const [busy, setBusy] = useState12(false);
1696
- useEffect4(() => {
2993
+ const [rows, setRows] = useState14(null);
2994
+ const [error, setError] = useState14(null);
2995
+ const [busy, setBusy] = useState14(false);
2996
+ useEffect7(() => {
1697
2997
  if (!ctx.app) return;
1698
2998
  fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/sessions`, {
1699
2999
  ...authInit()
@@ -1713,29 +3013,29 @@ function ElvixSessions({
1713
3013
  if (result.ok) setRows((prev) => prev?.filter((s) => s.id !== id) ?? null);
1714
3014
  onResult?.(result);
1715
3015
  }
1716
- return /* @__PURE__ */ jsxs13(ElvixCard, { title: "Active sessions", ...sizeProps, children: [
1717
- error && /* @__PURE__ */ jsx14("p", { role: "alert", className: "elvix-error", children: error }),
1718
- !rows && !error && /* @__PURE__ */ jsx14("p", { children: "Loading\u2026" }),
1719
- rows && /* @__PURE__ */ jsx14("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: rows.map((s) => /* @__PURE__ */ jsx14("li", { style: { padding: "10px 0", borderBottom: "1px solid rgba(0,0,0,0.06)" }, children: /* @__PURE__ */ jsxs13("div", { style: { display: "flex", justifyContent: "space-between", gap: 12 }, children: [
1720
- /* @__PURE__ */ jsxs13("div", { children: [
1721
- /* @__PURE__ */ jsxs13("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
3016
+ return /* @__PURE__ */ jsxs16(ElvixCard, { title: "Active sessions", ...sizeProps, children: [
3017
+ error && /* @__PURE__ */ jsx17("p", { role: "alert", className: "elvix-error", children: error }),
3018
+ !rows && !error && /* @__PURE__ */ jsx17("p", { children: "Loading\u2026" }),
3019
+ rows && /* @__PURE__ */ jsx17("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: rows.map((s) => /* @__PURE__ */ jsx17("li", { style: { padding: "10px 0", borderBottom: "1px solid rgba(0,0,0,0.06)" }, children: /* @__PURE__ */ jsxs16("div", { style: { display: "flex", justifyContent: "space-between", gap: 12 }, children: [
3020
+ /* @__PURE__ */ jsxs16("div", { children: [
3021
+ /* @__PURE__ */ jsxs16("div", { style: { fontSize: 13, fontWeight: 500 }, children: [
1722
3022
  s.device,
1723
- s.current && /* @__PURE__ */ jsx14("span", { style: { marginLeft: 8, color: "var(--elvix-primary-strong)", fontSize: 11 }, children: "\xB7 this device" })
3023
+ s.current && /* @__PURE__ */ jsx17("span", { style: { marginLeft: 8, color: "var(--elvix-primary-strong)", fontSize: 11 }, children: "\xB7 this device" })
1724
3024
  ] }),
1725
- /* @__PURE__ */ jsxs13("div", { style: { fontSize: 11, color: "rgba(0,0,0,0.55)" }, children: [
3025
+ /* @__PURE__ */ jsxs16("div", { style: { fontSize: 11, color: "rgba(0,0,0,0.55)" }, children: [
1726
3026
  s.country ?? "\u2014",
1727
3027
  " \xB7 since ",
1728
3028
  new Date(s.createdAt).toLocaleDateString()
1729
3029
  ] })
1730
3030
  ] }),
1731
- !s.current && /* @__PURE__ */ jsx14("button", { type: "button", disabled: busy, onClick: () => revoke(s.id), className: "elvix-btn elvix-btn-ghost", children: "Revoke" })
3031
+ !s.current && /* @__PURE__ */ jsx17("button", { type: "button", disabled: busy, onClick: () => revoke(s.id), className: "elvix-btn elvix-btn-ghost", children: "Revoke" })
1732
3032
  ] }) }, s.id)) })
1733
3033
  ] });
1734
3034
  }
1735
3035
 
1736
3036
  // src/react/elvix-export.tsx
1737
- import { useState as useState13 } from "react";
1738
- import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
3037
+ import { useState as useState15 } from "react";
3038
+ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
1739
3039
  function ElvixExport({
1740
3040
  onResult,
1741
3041
  width,
@@ -1747,9 +3047,9 @@ function ElvixExport({
1747
3047
  }) {
1748
3048
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1749
3049
  const ctx = useElvixContext();
1750
- const [busy, setBusy] = useState13(false);
1751
- const [done, setDone] = useState13(false);
1752
- const [error, setError] = useState13(null);
3050
+ const [busy, setBusy] = useState15(false);
3051
+ const [done, setDone] = useState15(false);
3052
+ const [error, setError] = useState15(null);
1753
3053
  async function start() {
1754
3054
  if (!ctx.app) return;
1755
3055
  setBusy(true);
@@ -1764,16 +3064,16 @@ function ElvixExport({
1764
3064
  else setDone(true);
1765
3065
  onResult?.(result);
1766
3066
  }
1767
- return /* @__PURE__ */ jsxs14(ElvixCard, { title: "Export my data", ...sizeProps, children: [
1768
- /* @__PURE__ */ jsx15("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" }, children: "Request a zip of every record we hold for you in this app. Delivery by email; single-use download link valid for 24h." }),
1769
- done ? /* @__PURE__ */ jsx15("p", { className: "elvix-muted", children: "Request queued. Check your email." }) : /* @__PURE__ */ jsx15("button", { type: "button", onClick: start, disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Queuing\u2026" : "Request export" }),
1770
- error && /* @__PURE__ */ jsx15("p", { role: "alert", className: "elvix-error", children: error })
3067
+ return /* @__PURE__ */ jsxs17(ElvixCard, { title: "Export my data", ...sizeProps, children: [
3068
+ /* @__PURE__ */ jsx18("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" }, children: "Request a zip of every record we hold for you in this app. Delivery by email; single-use download link valid for 24h." }),
3069
+ done ? /* @__PURE__ */ jsx18("p", { className: "elvix-muted", children: "Request queued. Check your email." }) : /* @__PURE__ */ jsx18("button", { type: "button", onClick: start, disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Queuing\u2026" : "Request export" }),
3070
+ error && /* @__PURE__ */ jsx18("p", { role: "alert", className: "elvix-error", children: error })
1771
3071
  ] });
1772
3072
  }
1773
3073
 
1774
3074
  // src/react/elvix-deactivate.tsx
1775
- import { useState as useState14 } from "react";
1776
- import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
3075
+ import { useState as useState16 } from "react";
3076
+ import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
1777
3077
  function ElvixDeactivate({
1778
3078
  onResult,
1779
3079
  width,
@@ -1785,11 +3085,11 @@ function ElvixDeactivate({
1785
3085
  }) {
1786
3086
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1787
3087
  const ctx = useElvixContext();
1788
- const [pane, setPane] = useState14("warn");
1789
- const [challengeId, setChallengeId] = useState14(null);
1790
- const [code, setCode] = useState14("");
1791
- const [busy, setBusy] = useState14(false);
1792
- const [error, setError] = useState14(null);
3088
+ const [pane, setPane] = useState16("warn");
3089
+ const [challengeId, setChallengeId] = useState16(null);
3090
+ const [code, setCode] = useState16("");
3091
+ const [busy, setBusy] = useState16(false);
3092
+ const [error, setError] = useState16(null);
1793
3093
  async function startChallenge() {
1794
3094
  if (!ctx.app) return;
1795
3095
  setBusy(true);
@@ -1827,16 +3127,16 @@ function ElvixDeactivate({
1827
3127
  onResult?.(result);
1828
3128
  }
1829
3129
  if (pane === "done") {
1830
- return /* @__PURE__ */ jsx16(ElvixCard, { title: "Deactivated", ...sizeProps, children: /* @__PURE__ */ jsx16("p", { children: "Your access has been paused. Sign in again to restore it." }) });
3130
+ return /* @__PURE__ */ jsx19(ElvixCard, { title: "Deactivated", ...sizeProps, children: /* @__PURE__ */ jsx19("p", { children: "Your access has been paused. Sign in again to restore it." }) });
1831
3131
  }
1832
- return /* @__PURE__ */ jsxs15(ElvixCard, { title: "Deactivate account", ...sizeProps, children: [
1833
- pane === "warn" && /* @__PURE__ */ jsxs15(Fragment3, { children: [
1834
- /* @__PURE__ */ jsx16("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" }, children: "Pause your membership. You can restore it any time by signing in again. No data is deleted." }),
1835
- /* @__PURE__ */ jsx16("button", { type: "button", onClick: startChallenge, disabled: busy, className: "elvix-btn elvix-btn-danger", children: busy ? "Sending\u2026" : "Send code" })
3132
+ return /* @__PURE__ */ jsxs18(ElvixCard, { title: "Deactivate account", ...sizeProps, children: [
3133
+ pane === "warn" && /* @__PURE__ */ jsxs18(Fragment5, { children: [
3134
+ /* @__PURE__ */ jsx19("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" }, children: "Pause your membership. You can restore it any time by signing in again. No data is deleted." }),
3135
+ /* @__PURE__ */ jsx19("button", { type: "button", onClick: startChallenge, disabled: busy, className: "elvix-btn elvix-btn-danger", children: busy ? "Sending\u2026" : "Send code" })
1836
3136
  ] }),
1837
- pane === "otp" && /* @__PURE__ */ jsxs15("form", { onSubmit: confirm, className: "elvix-form", children: [
1838
- /* @__PURE__ */ jsx16("p", { className: "elvix-muted", children: "We sent a 6-digit code to your email." }),
1839
- /* @__PURE__ */ jsx16(
3137
+ pane === "otp" && /* @__PURE__ */ jsxs18("form", { onSubmit: confirm, className: "elvix-form", children: [
3138
+ /* @__PURE__ */ jsx19("p", { className: "elvix-muted", children: "We sent a 6-digit code to your email." }),
3139
+ /* @__PURE__ */ jsx19(
1840
3140
  "input",
1841
3141
  {
1842
3142
  type: "text",
@@ -1850,15 +3150,15 @@ function ElvixDeactivate({
1850
3150
  className: "elvix-input"
1851
3151
  }
1852
3152
  ),
1853
- /* @__PURE__ */ jsx16("button", { type: "submit", disabled: busy || code.length !== 6, className: "elvix-btn elvix-btn-danger", children: busy ? "Deactivating\u2026" : "Confirm" })
3153
+ /* @__PURE__ */ jsx19("button", { type: "submit", disabled: busy || code.length !== 6, className: "elvix-btn elvix-btn-danger", children: busy ? "Deactivating\u2026" : "Confirm" })
1854
3154
  ] }),
1855
- error && /* @__PURE__ */ jsx16("p", { role: "alert", className: "elvix-error", children: error })
3155
+ error && /* @__PURE__ */ jsx19("p", { role: "alert", className: "elvix-error", children: error })
1856
3156
  ] });
1857
3157
  }
1858
3158
 
1859
3159
  // src/react/elvix-leave.tsx
1860
- import { useState as useState15 } from "react";
1861
- import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
3160
+ import { useState as useState17 } from "react";
3161
+ import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
1862
3162
  function ElvixLeave({
1863
3163
  onResult,
1864
3164
  width,
@@ -1870,11 +3170,11 @@ function ElvixLeave({
1870
3170
  }) {
1871
3171
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1872
3172
  const ctx = useElvixContext();
1873
- const [pane, setPane] = useState15("warn");
1874
- const [challengeId, setChallengeId] = useState15(null);
1875
- const [code, setCode] = useState15("");
1876
- const [busy, setBusy] = useState15(false);
1877
- const [error, setError] = useState15(null);
3173
+ const [pane, setPane] = useState17("warn");
3174
+ const [challengeId, setChallengeId] = useState17(null);
3175
+ const [code, setCode] = useState17("");
3176
+ const [busy, setBusy] = useState17(false);
3177
+ const [error, setError] = useState17(null);
1878
3178
  async function startChallenge() {
1879
3179
  if (!ctx.app) return;
1880
3180
  setBusy(true);
@@ -1912,16 +3212,16 @@ function ElvixLeave({
1912
3212
  onResult?.(result);
1913
3213
  }
1914
3214
  if (pane === "done") {
1915
- return /* @__PURE__ */ jsx17(ElvixCard, { title: "You've left", ...sizeProps, children: /* @__PURE__ */ jsx17("p", { children: "You've left this app. Your data is archived; sign in again to rejoin." }) });
3215
+ return /* @__PURE__ */ jsx20(ElvixCard, { title: "You've left", ...sizeProps, children: /* @__PURE__ */ jsx20("p", { children: "You've left this app. Your data is archived; sign in again to rejoin." }) });
1916
3216
  }
1917
- return /* @__PURE__ */ jsxs16(ElvixCard, { title: "Leave this app", ...sizeProps, children: [
1918
- pane === "warn" && /* @__PURE__ */ jsxs16(Fragment4, { children: [
1919
- /* @__PURE__ */ jsx17("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" }, children: "Remove yourself from this app. Audit trail is preserved; you can sign back in any time to rejoin." }),
1920
- /* @__PURE__ */ jsx17("button", { type: "button", onClick: startChallenge, disabled: busy, className: "elvix-btn elvix-btn-danger", children: busy ? "Sending\u2026" : "Send code" })
3217
+ return /* @__PURE__ */ jsxs19(ElvixCard, { title: "Leave this app", ...sizeProps, children: [
3218
+ pane === "warn" && /* @__PURE__ */ jsxs19(Fragment6, { children: [
3219
+ /* @__PURE__ */ jsx20("p", { style: { fontSize: 13, color: "rgba(0,0,0,0.6)" }, children: "Remove yourself from this app. Audit trail is preserved; you can sign back in any time to rejoin." }),
3220
+ /* @__PURE__ */ jsx20("button", { type: "button", onClick: startChallenge, disabled: busy, className: "elvix-btn elvix-btn-danger", children: busy ? "Sending\u2026" : "Send code" })
1921
3221
  ] }),
1922
- pane === "otp" && /* @__PURE__ */ jsxs16("form", { onSubmit: confirm, className: "elvix-form", children: [
1923
- /* @__PURE__ */ jsx17("p", { className: "elvix-muted", children: "We sent a 6-digit code to your email." }),
1924
- /* @__PURE__ */ jsx17(
3222
+ pane === "otp" && /* @__PURE__ */ jsxs19("form", { onSubmit: confirm, className: "elvix-form", children: [
3223
+ /* @__PURE__ */ jsx20("p", { className: "elvix-muted", children: "We sent a 6-digit code to your email." }),
3224
+ /* @__PURE__ */ jsx20(
1925
3225
  "input",
1926
3226
  {
1927
3227
  type: "text",
@@ -1935,15 +3235,15 @@ function ElvixLeave({
1935
3235
  className: "elvix-input"
1936
3236
  }
1937
3237
  ),
1938
- /* @__PURE__ */ jsx17("button", { type: "submit", disabled: busy || code.length !== 6, className: "elvix-btn elvix-btn-danger", children: busy ? "Leaving\u2026" : "Confirm leave" })
3238
+ /* @__PURE__ */ jsx20("button", { type: "submit", disabled: busy || code.length !== 6, className: "elvix-btn elvix-btn-danger", children: busy ? "Leaving\u2026" : "Confirm leave" })
1939
3239
  ] }),
1940
- error && /* @__PURE__ */ jsx17("p", { role: "alert", className: "elvix-error", children: error })
3240
+ error && /* @__PURE__ */ jsx20("p", { role: "alert", className: "elvix-error", children: error })
1941
3241
  ] });
1942
3242
  }
1943
3243
 
1944
3244
  // src/react/elvix-address-book.tsx
1945
- import { useEffect as useEffect5, useState as useState16 } from "react";
1946
- import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
3245
+ import { useEffect as useEffect8, useState as useState18 } from "react";
3246
+ import { jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
1947
3247
  function ElvixAddressBook({
1948
3248
  onResult,
1949
3249
  width,
@@ -1955,11 +3255,11 @@ function ElvixAddressBook({
1955
3255
  }) {
1956
3256
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
1957
3257
  const ctx = useElvixContext();
1958
- const [rows, setRows] = useState16(null);
1959
- const [error, setError] = useState16(null);
1960
- const [busy, setBusy] = useState16(false);
1961
- const [adding, setAdding] = useState16(false);
1962
- const [form, setForm] = useState16({
3258
+ const [rows, setRows] = useState18(null);
3259
+ const [error, setError] = useState18(null);
3260
+ const [busy, setBusy] = useState18(false);
3261
+ const [adding, setAdding] = useState18(false);
3262
+ const [form, setForm] = useState18({
1963
3263
  label: "Home",
1964
3264
  line1: "",
1965
3265
  postalCode: "",
@@ -1975,7 +3275,7 @@ function ElvixAddressBook({
1975
3275
  else setError("load_failed");
1976
3276
  }).catch(() => setError("network"));
1977
3277
  }
1978
- useEffect5(() => {
3278
+ useEffect8(() => {
1979
3279
  reload();
1980
3280
  }, [ctx.app, ctx.baseUrl]);
1981
3281
  async function add(e) {
@@ -2008,14 +3308,14 @@ function ElvixAddressBook({
2008
3308
  if (result.ok) setRows((prev) => prev?.filter((a) => a.id !== id) ?? null);
2009
3309
  onResult?.(result);
2010
3310
  }
2011
- return /* @__PURE__ */ jsxs17(ElvixCard, { title: "Addresses", ...sizeProps, children: [
2012
- error && /* @__PURE__ */ jsx18("p", { role: "alert", className: "elvix-error", children: error }),
2013
- !rows && !error && /* @__PURE__ */ jsx18("p", { children: "Loading\u2026" }),
2014
- rows && rows.length === 0 && /* @__PURE__ */ jsx18("p", { className: "elvix-muted", children: "No addresses yet." }),
2015
- rows?.map((a) => /* @__PURE__ */ jsxs17("div", { style: { padding: "8px 0", borderBottom: "1px solid rgba(0,0,0,0.06)", display: "flex", justifyContent: "space-between", gap: 12 }, children: [
2016
- /* @__PURE__ */ jsxs17("div", { style: { fontSize: 13 }, children: [
2017
- /* @__PURE__ */ jsx18("div", { style: { fontWeight: 500 }, children: a.label }),
2018
- /* @__PURE__ */ jsxs17("div", { style: { color: "rgba(0,0,0,0.55)" }, children: [
3311
+ return /* @__PURE__ */ jsxs20(ElvixCard, { title: "Addresses", ...sizeProps, children: [
3312
+ error && /* @__PURE__ */ jsx21("p", { role: "alert", className: "elvix-error", children: error }),
3313
+ !rows && !error && /* @__PURE__ */ jsx21("p", { children: "Loading\u2026" }),
3314
+ rows && rows.length === 0 && /* @__PURE__ */ jsx21("p", { className: "elvix-muted", children: "No addresses yet." }),
3315
+ rows?.map((a) => /* @__PURE__ */ jsxs20("div", { style: { padding: "8px 0", borderBottom: "1px solid rgba(0,0,0,0.06)", display: "flex", justifyContent: "space-between", gap: 12 }, children: [
3316
+ /* @__PURE__ */ jsxs20("div", { style: { fontSize: 13 }, children: [
3317
+ /* @__PURE__ */ jsx21("div", { style: { fontWeight: 500 }, children: a.label }),
3318
+ /* @__PURE__ */ jsxs20("div", { style: { color: "rgba(0,0,0,0.55)" }, children: [
2019
3319
  a.line1,
2020
3320
  a.line2 ? `, ${a.line2}` : "",
2021
3321
  ", ",
@@ -2026,23 +3326,23 @@ function ElvixAddressBook({
2026
3326
  a.country
2027
3327
  ] })
2028
3328
  ] }),
2029
- /* @__PURE__ */ jsx18("button", { type: "button", disabled: busy, onClick: () => remove(a.id), className: "elvix-btn elvix-btn-ghost", children: "Remove" })
3329
+ /* @__PURE__ */ jsx21("button", { type: "button", disabled: busy, onClick: () => remove(a.id), className: "elvix-btn elvix-btn-ghost", children: "Remove" })
2030
3330
  ] }, a.id)),
2031
- !adding && /* @__PURE__ */ jsx18("button", { type: "button", onClick: () => setAdding(true), className: "elvix-btn elvix-btn-primary", style: { marginTop: 12 }, children: "Add address" }),
2032
- adding && /* @__PURE__ */ jsxs17("form", { onSubmit: add, className: "elvix-form", style: { marginTop: 12 }, children: [
2033
- /* @__PURE__ */ jsx18("input", { value: form.label, onChange: (e) => setForm({ ...form, label: e.target.value }), placeholder: "Label", className: "elvix-input" }),
2034
- /* @__PURE__ */ jsx18("input", { value: form.line1, onChange: (e) => setForm({ ...form, line1: e.target.value }), placeholder: "Street", required: true, className: "elvix-input" }),
2035
- /* @__PURE__ */ jsx18("input", { value: form.postalCode, onChange: (e) => setForm({ ...form, postalCode: e.target.value }), placeholder: "Postal code", required: true, className: "elvix-input" }),
2036
- /* @__PURE__ */ jsx18("input", { value: form.city, onChange: (e) => setForm({ ...form, city: e.target.value }), placeholder: "City", required: true, className: "elvix-input" }),
2037
- /* @__PURE__ */ jsx18("input", { value: form.country, onChange: (e) => setForm({ ...form, country: e.target.value.toUpperCase() }), placeholder: "Country (ISO-2)", maxLength: 2, required: true, className: "elvix-input" }),
2038
- /* @__PURE__ */ jsx18("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" })
3331
+ !adding && /* @__PURE__ */ jsx21("button", { type: "button", onClick: () => setAdding(true), className: "elvix-btn elvix-btn-primary", style: { marginTop: 12 }, children: "Add address" }),
3332
+ adding && /* @__PURE__ */ jsxs20("form", { onSubmit: add, className: "elvix-form", style: { marginTop: 12 }, children: [
3333
+ /* @__PURE__ */ jsx21("input", { value: form.label, onChange: (e) => setForm({ ...form, label: e.target.value }), placeholder: "Label", className: "elvix-input" }),
3334
+ /* @__PURE__ */ jsx21("input", { value: form.line1, onChange: (e) => setForm({ ...form, line1: e.target.value }), placeholder: "Street", required: true, className: "elvix-input" }),
3335
+ /* @__PURE__ */ jsx21("input", { value: form.postalCode, onChange: (e) => setForm({ ...form, postalCode: e.target.value }), placeholder: "Postal code", required: true, className: "elvix-input" }),
3336
+ /* @__PURE__ */ jsx21("input", { value: form.city, onChange: (e) => setForm({ ...form, city: e.target.value }), placeholder: "City", required: true, className: "elvix-input" }),
3337
+ /* @__PURE__ */ jsx21("input", { value: form.country, onChange: (e) => setForm({ ...form, country: e.target.value.toUpperCase() }), placeholder: "Country (ISO-2)", maxLength: 2, required: true, className: "elvix-input" }),
3338
+ /* @__PURE__ */ jsx21("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" })
2039
3339
  ] })
2040
3340
  ] });
2041
3341
  }
2042
3342
 
2043
3343
  // src/react/elvix-legal-entities.tsx
2044
- import { useEffect as useEffect6, useState as useState17 } from "react";
2045
- import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
3344
+ import { useEffect as useEffect9, useState as useState19 } from "react";
3345
+ import { jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
2046
3346
  function ElvixLegalEntities({
2047
3347
  onResult,
2048
3348
  width,
@@ -2054,11 +3354,11 @@ function ElvixLegalEntities({
2054
3354
  }) {
2055
3355
  const sizeProps = { width, height, minWidth, maxWidth, minHeight, maxHeight };
2056
3356
  const ctx = useElvixContext();
2057
- const [rows, setRows] = useState17(null);
2058
- const [error, setError] = useState17(null);
2059
- const [busy, setBusy] = useState17(false);
2060
- const [adding, setAdding] = useState17(false);
2061
- const [form, setForm] = useState17({
3357
+ const [rows, setRows] = useState19(null);
3358
+ const [error, setError] = useState19(null);
3359
+ const [busy, setBusy] = useState19(false);
3360
+ const [adding, setAdding] = useState19(false);
3361
+ const [form, setForm] = useState19({
2062
3362
  legalName: "",
2063
3363
  taxId: "",
2064
3364
  country: ""
@@ -2072,7 +3372,7 @@ function ElvixLegalEntities({
2072
3372
  else setError("load_failed");
2073
3373
  }).catch(() => setError("network"));
2074
3374
  }
2075
- useEffect6(() => {
3375
+ useEffect9(() => {
2076
3376
  reload();
2077
3377
  }, [ctx.app, ctx.baseUrl]);
2078
3378
  async function add(e) {
@@ -2105,27 +3405,27 @@ function ElvixLegalEntities({
2105
3405
  if (result.ok) setRows((prev) => prev?.filter((a) => a.id !== id) ?? null);
2106
3406
  onResult?.(result);
2107
3407
  }
2108
- return /* @__PURE__ */ jsxs18(ElvixCard, { title: "Legal entities", ...sizeProps, children: [
2109
- error && /* @__PURE__ */ jsx19("p", { role: "alert", className: "elvix-error", children: error }),
2110
- !rows && !error && /* @__PURE__ */ jsx19("p", { children: "Loading\u2026" }),
2111
- rows && rows.length === 0 && /* @__PURE__ */ jsx19("p", { className: "elvix-muted", children: "No legal entities yet." }),
2112
- rows?.map((e) => /* @__PURE__ */ jsxs18("div", { style: { padding: "8px 0", borderBottom: "1px solid rgba(0,0,0,0.06)", display: "flex", justifyContent: "space-between", gap: 12 }, children: [
2113
- /* @__PURE__ */ jsxs18("div", { style: { fontSize: 13 }, children: [
2114
- /* @__PURE__ */ jsx19("div", { style: { fontWeight: 500 }, children: e.legalName }),
2115
- /* @__PURE__ */ jsxs18("div", { style: { color: "rgba(0,0,0,0.55)" }, children: [
3408
+ return /* @__PURE__ */ jsxs21(ElvixCard, { title: "Legal entities", ...sizeProps, children: [
3409
+ error && /* @__PURE__ */ jsx22("p", { role: "alert", className: "elvix-error", children: error }),
3410
+ !rows && !error && /* @__PURE__ */ jsx22("p", { children: "Loading\u2026" }),
3411
+ rows && rows.length === 0 && /* @__PURE__ */ jsx22("p", { className: "elvix-muted", children: "No legal entities yet." }),
3412
+ rows?.map((e) => /* @__PURE__ */ jsxs21("div", { style: { padding: "8px 0", borderBottom: "1px solid rgba(0,0,0,0.06)", display: "flex", justifyContent: "space-between", gap: 12 }, children: [
3413
+ /* @__PURE__ */ jsxs21("div", { style: { fontSize: 13 }, children: [
3414
+ /* @__PURE__ */ jsx22("div", { style: { fontWeight: 500 }, children: e.legalName }),
3415
+ /* @__PURE__ */ jsxs21("div", { style: { color: "rgba(0,0,0,0.55)" }, children: [
2116
3416
  e.taxId,
2117
3417
  " \xB7 ",
2118
3418
  e.country
2119
3419
  ] })
2120
3420
  ] }),
2121
- /* @__PURE__ */ jsx19("button", { type: "button", disabled: busy, onClick: () => remove(e.id), className: "elvix-btn elvix-btn-ghost", children: "Remove" })
3421
+ /* @__PURE__ */ jsx22("button", { type: "button", disabled: busy, onClick: () => remove(e.id), className: "elvix-btn elvix-btn-ghost", children: "Remove" })
2122
3422
  ] }, e.id)),
2123
- !adding && /* @__PURE__ */ jsx19("button", { type: "button", onClick: () => setAdding(true), className: "elvix-btn elvix-btn-primary", style: { marginTop: 12 }, children: "Add entity" }),
2124
- adding && /* @__PURE__ */ jsxs18("form", { onSubmit: add, className: "elvix-form", style: { marginTop: 12 }, children: [
2125
- /* @__PURE__ */ jsx19("input", { value: form.legalName, onChange: (e) => setForm({ ...form, legalName: e.target.value }), placeholder: "Legal name", required: true, className: "elvix-input" }),
2126
- /* @__PURE__ */ jsx19("input", { value: form.taxId, onChange: (e) => setForm({ ...form, taxId: e.target.value }), placeholder: "Tax / VAT ID", required: true, className: "elvix-input" }),
2127
- /* @__PURE__ */ jsx19("input", { value: form.country, onChange: (e) => setForm({ ...form, country: e.target.value.toUpperCase() }), placeholder: "Country (ISO-2)", maxLength: 2, required: true, className: "elvix-input" }),
2128
- /* @__PURE__ */ jsx19("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" })
3423
+ !adding && /* @__PURE__ */ jsx22("button", { type: "button", onClick: () => setAdding(true), className: "elvix-btn elvix-btn-primary", style: { marginTop: 12 }, children: "Add entity" }),
3424
+ adding && /* @__PURE__ */ jsxs21("form", { onSubmit: add, className: "elvix-form", style: { marginTop: 12 }, children: [
3425
+ /* @__PURE__ */ jsx22("input", { value: form.legalName, onChange: (e) => setForm({ ...form, legalName: e.target.value }), placeholder: "Legal name", required: true, className: "elvix-input" }),
3426
+ /* @__PURE__ */ jsx22("input", { value: form.taxId, onChange: (e) => setForm({ ...form, taxId: e.target.value }), placeholder: "Tax / VAT ID", required: true, className: "elvix-input" }),
3427
+ /* @__PURE__ */ jsx22("input", { value: form.country, onChange: (e) => setForm({ ...form, country: e.target.value.toUpperCase() }), placeholder: "Country (ISO-2)", maxLength: 2, required: true, className: "elvix-input" }),
3428
+ /* @__PURE__ */ jsx22("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary", children: busy ? "Saving\u2026" : "Save" })
2129
3429
  ] })
2130
3430
  ] });
2131
3431
  }