@authon/js 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,11 +8,46 @@ var AuthonMfaRequiredError = class extends Error {
8
8
  }
9
9
  };
10
10
 
11
- // src/modal.ts
12
- import { DEFAULT_BRANDING } from "@authon/shared";
11
+ // ../shared/dist/index.js
12
+ var PROVIDER_DISPLAY_NAMES = {
13
+ google: "Google",
14
+ apple: "Apple",
15
+ kakao: "Kakao",
16
+ naver: "Naver",
17
+ facebook: "Facebook",
18
+ github: "GitHub",
19
+ discord: "Discord",
20
+ x: "X",
21
+ line: "LINE",
22
+ microsoft: "Microsoft"
23
+ };
24
+ var PROVIDER_COLORS = {
25
+ google: { bg: "#ffffff", text: "#1f1f1f" },
26
+ apple: { bg: "#000000", text: "#ffffff" },
27
+ kakao: { bg: "#FEE500", text: "#191919" },
28
+ naver: { bg: "#03C75A", text: "#ffffff" },
29
+ facebook: { bg: "#1877F2", text: "#ffffff" },
30
+ github: { bg: "#24292e", text: "#ffffff" },
31
+ discord: { bg: "#5865F2", text: "#ffffff" },
32
+ x: { bg: "#000000", text: "#ffffff" },
33
+ line: { bg: "#06C755", text: "#ffffff" },
34
+ microsoft: { bg: "#ffffff", text: "#1f1f1f" }
35
+ };
36
+ var DEFAULT_BRANDING = {
37
+ primaryColorStart: "#7c3aed",
38
+ primaryColorEnd: "#4f46e5",
39
+ lightBg: "#ffffff",
40
+ lightText: "#111827",
41
+ darkBg: "#0f172a",
42
+ darkText: "#f1f5f9",
43
+ borderRadius: 12,
44
+ showEmailPassword: true,
45
+ showDivider: true,
46
+ showSecuredBy: true,
47
+ locale: "en"
48
+ };
13
49
 
14
50
  // src/providers.ts
15
- import { PROVIDER_COLORS, PROVIDER_DISPLAY_NAMES } from "@authon/shared";
16
51
  var PROVIDER_ICONS = {
17
52
  google: `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
18
53
  apple: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/></svg>`,
@@ -37,6 +72,30 @@ function getProviderButtonConfig(provider) {
37
72
  }
38
73
 
39
74
  // src/modal.ts
75
+ function hexToRgba(hex, alpha) {
76
+ const h = hex.replace("#", "");
77
+ const r = parseInt(h.substring(0, 2), 16);
78
+ const g = parseInt(h.substring(2, 4), 16);
79
+ const b = parseInt(h.substring(4, 6), 16);
80
+ return `rgba(${r},${g},${b},${alpha})`;
81
+ }
82
+ var WALLET_OPTIONS = [
83
+ { id: "pexus", name: "Pexus", color: "#7c3aed" },
84
+ { id: "metamask", name: "MetaMask", color: "#f6851b" },
85
+ { id: "phantom", name: "Phantom", color: "#ab9ff2" }
86
+ ];
87
+ function walletIconSvg(id) {
88
+ switch (id) {
89
+ case "pexus":
90
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#7c3aed"/><path d="M7 8h4v8H7V8zm6 0h4v8h-4V8z" fill="#fff"/></svg>`;
91
+ case "metamask":
92
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#f6851b"/><path d="M17.2 4L12 8.5l1 .7L17.2 4zM6.8 4l5.1 5.3-1-0.6L6.8 4zM16 16.2l-1.4 2.1 3 .8.8-2.9h-2.4zM5.6 16.2l.9 2.8 3-.8-1.4-2h-2.5z" fill="#fff"/></svg>`;
93
+ case "phantom":
94
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#ab9ff2"/><circle cx="9" cy="11" r="1.5" fill="#fff"/><circle cx="15" cy="11" r="1.5" fill="#fff"/><path d="M6 12c0-3.3 2.7-6 6-6s6 2.7 6 6v2c0 1.1-.9 2-2 2H8c-1.1 0-2-.9-2-2v-2z" stroke="#fff" stroke-width="1.5" fill="none"/></svg>`;
95
+ default:
96
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#666"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="12">${id[0]?.toUpperCase() ?? "W"}</text></svg>`;
97
+ }
98
+ }
40
99
  var ModalRenderer = class {
41
100
  shadowRoot = null;
42
101
  hostElement = null;
@@ -44,12 +103,23 @@ var ModalRenderer = class {
44
103
  mode;
45
104
  theme;
46
105
  branding;
106
+ themeObserver = null;
107
+ mediaQueryListener = null;
47
108
  enabledProviders = [];
48
109
  currentView = "signIn";
49
110
  onProviderClick;
50
111
  onEmailSubmit;
51
112
  onClose;
113
+ onWeb3WalletSelect;
114
+ onPasswordlessSubmit;
115
+ onOtpVerify;
116
+ onPasskeyClick;
52
117
  escHandler = null;
118
+ // Overlay state
119
+ currentOverlay = "none";
120
+ selectedWallet = "";
121
+ overlayEmail = "";
122
+ overlayError = "";
53
123
  constructor(options) {
54
124
  this.mode = options.mode;
55
125
  this.theme = options.theme || "auto";
@@ -57,6 +127,14 @@ var ModalRenderer = class {
57
127
  this.onProviderClick = options.onProviderClick;
58
128
  this.onEmailSubmit = options.onEmailSubmit;
59
129
  this.onClose = options.onClose;
130
+ this.onWeb3WalletSelect = options.onWeb3WalletSelect || (() => {
131
+ });
132
+ this.onPasswordlessSubmit = options.onPasswordlessSubmit || (() => {
133
+ });
134
+ this.onOtpVerify = options.onOtpVerify || (() => {
135
+ });
136
+ this.onPasskeyClick = options.onPasskeyClick || (() => {
137
+ });
60
138
  if (options.mode === "embedded" && options.containerId) {
61
139
  this.containerElement = document.getElementById(options.containerId);
62
140
  }
@@ -69,13 +147,16 @@ var ModalRenderer = class {
69
147
  }
70
148
  open(view = "signIn") {
71
149
  if (this.shadowRoot && this.hostElement) {
150
+ this.hideOverlay();
72
151
  this.switchView(view);
73
152
  } else {
74
153
  this.currentView = view;
154
+ this.currentOverlay = "none";
75
155
  this.render(view);
76
156
  }
77
157
  }
78
158
  close() {
159
+ this.stopThemeObserver();
79
160
  if (this.escHandler) {
80
161
  document.removeEventListener("keydown", this.escHandler);
81
162
  this.escHandler = null;
@@ -88,6 +169,46 @@ var ModalRenderer = class {
88
169
  if (this.containerElement) {
89
170
  this.containerElement.innerHTML = "";
90
171
  }
172
+ this.currentOverlay = "none";
173
+ }
174
+ /** Update theme at runtime without destroying form state */
175
+ setTheme(theme) {
176
+ this.theme = theme;
177
+ this.updateThemeCSS();
178
+ if (theme === "auto") {
179
+ this.startThemeObserver();
180
+ } else {
181
+ this.stopThemeObserver();
182
+ }
183
+ }
184
+ updateThemeCSS() {
185
+ if (!this.shadowRoot) return;
186
+ const styleEl = this.shadowRoot.getElementById("authon-theme-style");
187
+ if (styleEl) {
188
+ styleEl.textContent = this.buildCSS();
189
+ }
190
+ }
191
+ startThemeObserver() {
192
+ this.stopThemeObserver();
193
+ if (typeof document === "undefined" || typeof window === "undefined") return;
194
+ this.themeObserver = new MutationObserver(() => this.updateThemeCSS());
195
+ this.themeObserver.observe(document.documentElement, {
196
+ attributes: true,
197
+ attributeFilter: ["data-theme", "class"]
198
+ });
199
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
200
+ this.mediaQueryListener = () => this.updateThemeCSS();
201
+ mq.addEventListener("change", this.mediaQueryListener);
202
+ }
203
+ stopThemeObserver() {
204
+ if (this.themeObserver) {
205
+ this.themeObserver.disconnect();
206
+ this.themeObserver = null;
207
+ }
208
+ if (this.mediaQueryListener) {
209
+ window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", this.mediaQueryListener);
210
+ this.mediaQueryListener = null;
211
+ }
91
212
  }
92
213
  showError(message) {
93
214
  if (!this.shadowRoot) return;
@@ -139,6 +260,47 @@ var ModalRenderer = class {
139
260
  if (!this.shadowRoot) return;
140
261
  this.shadowRoot.getElementById("authon-loading-overlay")?.remove();
141
262
  }
263
+ // ── Flow Overlay Public API ──
264
+ showOverlay(overlay) {
265
+ this.currentOverlay = overlay;
266
+ this.overlayError = "";
267
+ this.renderOverlay();
268
+ }
269
+ hideOverlay() {
270
+ this.currentOverlay = "none";
271
+ this.overlayError = "";
272
+ if (!this.shadowRoot) return;
273
+ this.shadowRoot.getElementById("flow-overlay")?.remove();
274
+ }
275
+ showWeb3Success(walletId, address) {
276
+ this.selectedWallet = walletId;
277
+ this.overlayError = "";
278
+ const truncated = address.length > 10 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;
279
+ this.currentOverlay = "web3-success";
280
+ this.renderOverlayWithData({ truncatedAddress: truncated, walletId });
281
+ }
282
+ showPasswordlessSent() {
283
+ this.overlayError = "";
284
+ this.currentOverlay = "passwordless-sent";
285
+ this.renderOverlay();
286
+ }
287
+ showOtpInput(email) {
288
+ this.overlayEmail = email;
289
+ this.overlayError = "";
290
+ this.currentOverlay = "otp-input";
291
+ this.renderOverlay();
292
+ }
293
+ showPasskeySuccess() {
294
+ this.overlayError = "";
295
+ this.currentOverlay = "passkey-success";
296
+ this.renderOverlay();
297
+ }
298
+ showOverlayError(message) {
299
+ this.overlayError = message;
300
+ if (this.currentOverlay !== "none") {
301
+ this.renderOverlay();
302
+ }
303
+ }
142
304
  // ── Smooth view switch (no flicker) ──
143
305
  switchView(view) {
144
306
  if (!this.shadowRoot || view === this.currentView) return;
@@ -169,13 +331,16 @@ var ModalRenderer = class {
169
331
  this.shadowRoot.innerHTML = this.buildShell(view);
170
332
  this.attachInnerEvents(view);
171
333
  this.attachShellEvents();
334
+ if (this.theme === "auto") {
335
+ this.startThemeObserver();
336
+ }
172
337
  }
173
338
  // ── HTML builders ──
174
339
  /** Shell = style + backdrop + modal-container (stable across view switches) */
175
340
  buildShell(view) {
176
341
  const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
177
342
  return `
178
- <style>${this.buildCSS()}</style>
343
+ <style id="authon-theme-style">${this.buildCSS()}</style>
179
344
  ${popupWrapper}
180
345
  <div class="modal-container" role="dialog" aria-modal="true">
181
346
  <div id="modal-inner" class="modal-inner">
@@ -210,6 +375,35 @@ var ModalRenderer = class {
210
375
  ${isSignUp ? '<p class="password-hint">Must contain uppercase, lowercase, and a number (min 8 chars)</p>' : ""}
211
376
  <button type="submit" class="submit-btn">${isSignUp ? "Sign up" : "Sign in"}</button>
212
377
  </form>` : "";
378
+ const hasMethodAbove = showProviders && this.enabledProviders.length > 0 || b.showEmailPassword !== false;
379
+ const hasMethodBelow = b.showWeb3 || b.showPasswordless || b.showPasskey;
380
+ const methodDivider = hasMethodAbove && hasMethodBelow ? `<div class="divider"><span>or</span></div>` : "";
381
+ const methodButtons = [];
382
+ if (b.showWeb3) {
383
+ methodButtons.push(`<button class="auth-method-btn web3-btn" id="web3-btn">
384
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
385
+ <path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/>
386
+ </svg>
387
+ <span>Connect Wallet</span>
388
+ </button>`);
389
+ }
390
+ if (b.showPasswordless) {
391
+ methodButtons.push(`<button class="auth-method-btn passwordless-btn" id="passwordless-btn">
392
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
393
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
394
+ </svg>
395
+ <span>Continue with Magic Link</span>
396
+ </button>`);
397
+ }
398
+ if (b.showPasskey) {
399
+ methodButtons.push(`<button class="auth-method-btn passkey-btn" id="passkey-btn">
400
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
401
+ <circle cx="10" cy="7" r="4"/><path d="M10.3 15H7a4 4 0 0 0-4 4v2"/><path d="M21.7 13.3 19 11"/><path d="m21 15-2.5-1.5"/><path d="m17 17 2.5-1.5"/><path d="M22 9v6a1 1 0 0 1-1 1h-.5"/><circle cx="18" cy="9" r="3"/>
402
+ </svg>
403
+ <span>Sign in with Passkey</span>
404
+ </button>`);
405
+ }
406
+ const authMethods = methodButtons.length > 0 ? `<div class="auth-methods">${methodButtons.join("")}</div>` : "";
213
407
  const footer = b.termsUrl || b.privacyUrl ? `<div class="footer">
214
408
  ${b.termsUrl ? `<a href="${b.termsUrl}" target="_blank">Terms of Service</a>` : ""}
215
409
  ${b.termsUrl && b.privacyUrl ? " \xB7 " : ""}
@@ -228,6 +422,8 @@ var ModalRenderer = class {
228
422
  ${showProviders ? `<div class="providers">${providerButtons}</div>` : ""}
229
423
  ${divider}
230
424
  ${emailForm}
425
+ ${methodDivider}
426
+ ${authMethods}
231
427
  <p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
232
428
  ${footer}
233
429
  ${b.showSecuredBy !== false ? `<div class="secured-by">Secured by <a href="https://authon.dev" target="_blank" rel="noopener noreferrer" class="secured-link">Authon</a></div>` : ""}
@@ -236,6 +432,11 @@ var ModalRenderer = class {
236
432
  isDark() {
237
433
  if (this.theme === "dark") return true;
238
434
  if (this.theme === "light") return false;
435
+ if (typeof document !== "undefined") {
436
+ const html = document.documentElement;
437
+ if (html.classList.contains("dark") || html.getAttribute("data-theme") === "dark") return true;
438
+ if (html.classList.contains("light") || html.getAttribute("data-theme") === "light") return false;
439
+ }
239
440
  return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
240
441
  }
241
442
  buildCSS() {
@@ -259,6 +460,10 @@ var ModalRenderer = class {
259
460
  --authon-border: ${borderColor};
260
461
  --authon-divider: ${dividerColor};
261
462
  --authon-input-bg: ${inputBg};
463
+ --authon-overlay-bg: ${hexToRgba(bg, 0.92)};
464
+ --authon-overlay-bg-solid: ${hexToRgba(bg, 0.97)};
465
+ --authon-backdrop-bg: rgba(0,0,0,${dark ? "0.7" : "0.5"});
466
+ --authon-shadow-opacity: ${dark ? "0.5" : "0.25"};
262
467
  --authon-radius: ${b.borderRadius ?? 12}px;
263
468
  --authon-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
264
469
  font-family: var(--authon-font);
@@ -267,7 +472,7 @@ var ModalRenderer = class {
267
472
  * { box-sizing: border-box; margin: 0; padding: 0; }
268
473
  .backdrop {
269
474
  position: fixed; inset: 0; z-index: 99998;
270
- background: rgba(0,0,0,${dark ? "0.7" : "0.5"}); backdrop-filter: blur(4px);
475
+ background: var(--authon-backdrop-bg); backdrop-filter: blur(4px);
271
476
  animation: fadeIn 0.2s ease;
272
477
  }
273
478
  .modal-container {
@@ -361,10 +566,154 @@ var ModalRenderer = class {
361
566
  }
362
567
  .secured-link { font-weight: 600; color: var(--authon-muted); text-decoration: none; }
363
568
  .secured-link:hover { text-decoration: underline; }
569
+
570
+ /* Auth method buttons */
571
+ .auth-methods { display: flex; flex-direction: column; gap: 8px; }
572
+ .auth-method-btn {
573
+ display: flex; align-items: center; justify-content: center; gap: 8px;
574
+ width: 100%; padding: 10px 16px;
575
+ border-radius: calc(var(--authon-radius) * 0.5);
576
+ font-size: 13px; font-weight: 500; cursor: pointer;
577
+ font-family: var(--authon-font); transition: opacity 0.15s, transform 0.1s;
578
+ }
579
+ .auth-method-btn:hover { opacity: 0.85; }
580
+ .auth-method-btn:active { transform: scale(0.98); }
581
+ /* Web3 -- purple */
582
+ .web3-btn {
583
+ background: ${dark ? "rgba(139,92,246,0.12)" : "rgba(139,92,246,0.08)"};
584
+ border: 1px solid ${dark ? "rgba(139,92,246,0.3)" : "rgba(139,92,246,0.25)"};
585
+ color: ${dark ? "#c4b5fd" : "#7c3aed"};
586
+ }
587
+ /* Passwordless -- cyan */
588
+ .passwordless-btn {
589
+ background: ${dark ? "rgba(6,182,212,0.12)" : "rgba(6,182,212,0.08)"};
590
+ border: 1px solid ${dark ? "rgba(6,182,212,0.3)" : "rgba(6,182,212,0.25)"};
591
+ color: ${dark ? "#67e8f9" : "#0891b2"};
592
+ }
593
+ /* Passkey -- amber */
594
+ .passkey-btn {
595
+ background: ${dark ? "rgba(245,158,11,0.12)" : "rgba(245,158,11,0.08)"};
596
+ border: 1px solid ${dark ? "rgba(245,158,11,0.3)" : "rgba(245,158,11,0.25)"};
597
+ color: ${dark ? "#fcd34d" : "#b45309"};
598
+ }
599
+
600
+ /* Flow overlay */
601
+ .flow-overlay {
602
+ position: absolute; inset: 0; z-index: 10;
603
+ background: var(--authon-overlay-bg-solid);
604
+ backdrop-filter: blur(2px);
605
+ border-radius: var(--authon-radius);
606
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
607
+ gap: 12px; padding: 24px;
608
+ animation: fadeIn 0.2s ease;
609
+ }
610
+ .flow-overlay .cancel-link {
611
+ font-size: 12px; color: var(--authon-dim); cursor: pointer; border: none;
612
+ background: none; font-family: var(--authon-font); margin-top: 4px;
613
+ }
614
+ .flow-overlay .cancel-link:hover { text-decoration: underline; }
615
+ .flow-overlay .overlay-title {
616
+ font-size: 14px; font-weight: 600; color: var(--authon-text); text-align: center;
617
+ }
618
+ .flow-overlay .overlay-subtitle {
619
+ font-size: 12px; color: var(--authon-muted); text-align: center;
620
+ }
621
+ .flow-overlay .overlay-error {
622
+ padding: 6px 12px; margin-top: 4px;
623
+ background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3);
624
+ border-radius: calc(var(--authon-radius) * 0.33);
625
+ font-size: 12px; color: #ef4444; text-align: center; width: 100%;
626
+ }
627
+
628
+ /* Wallet picker */
629
+ .wallet-picker { display: flex; flex-direction: column; gap: 8px; width: 100%; }
630
+ .wallet-btn {
631
+ display: flex; align-items: center; gap: 10px;
632
+ width: 100%; padding: 10px 14px;
633
+ background: ${dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"};
634
+ border: 1px solid ${dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};
635
+ border-radius: calc(var(--authon-radius) * 0.5);
636
+ font-size: 13px; font-weight: 500; color: var(--authon-text);
637
+ cursor: pointer; font-family: var(--authon-font);
638
+ transition: opacity 0.15s;
639
+ }
640
+ .wallet-btn:hover { opacity: 0.8; }
641
+ .wallet-btn .wallet-icon { display: flex; align-items: center; flex-shrink: 0; }
642
+ .wallet-btn .wallet-icon svg { border-radius: 6px; }
643
+
644
+ /* Passwordless email input in overlay */
645
+ .pwless-form { display: flex; flex-direction: column; gap: 10px; width: 100%; }
646
+ .pwless-submit {
647
+ width: 100%; padding: 10px;
648
+ background: linear-gradient(135deg, #06b6d4, #0891b2);
649
+ color: #fff; border: none; border-radius: calc(var(--authon-radius) * 0.5);
650
+ font-size: 13px; font-weight: 600; cursor: pointer;
651
+ font-family: var(--authon-font); transition: opacity 0.15s;
652
+ }
653
+ .pwless-submit:hover { opacity: 0.9; }
654
+ .pwless-submit:disabled { opacity: 0.6; cursor: not-allowed; }
655
+
656
+ /* OTP input */
657
+ .otp-container { display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; }
658
+ .otp-inputs { display: flex; gap: 8px; justify-content: center; }
659
+ .otp-digit {
660
+ width: 40px; height: 48px; text-align: center;
661
+ font-size: 20px; font-weight: 600; font-family: var(--authon-font);
662
+ background: var(--authon-input-bg); color: var(--authon-text);
663
+ border: 1px solid var(--authon-border);
664
+ border-radius: calc(var(--authon-radius) * 0.33);
665
+ outline: none; transition: border-color 0.15s;
666
+ }
667
+ .otp-digit:focus {
668
+ border-color: var(--authon-primary-start);
669
+ box-shadow: 0 0 0 3px rgba(124,58,237,0.15);
670
+ }
671
+
672
+ /* Success check animation */
673
+ .success-check {
674
+ width: 48px; height: 48px; border-radius: 50%;
675
+ display: flex; align-items: center; justify-content: center;
676
+ }
677
+ .success-check svg path {
678
+ stroke-dasharray: 20;
679
+ stroke-dashoffset: 20;
680
+ animation: check-draw 0.4s ease-out 0.1s forwards;
681
+ }
682
+
683
+ /* Spinner */
684
+ .flow-spinner {
685
+ animation: spin 0.8s linear infinite;
686
+ }
687
+
688
+ /* Passkey verifying icon */
689
+ .passkey-icon-pulse {
690
+ width: 48px; height: 48px; border-radius: 50%;
691
+ display: flex; align-items: center; justify-content: center;
692
+ background: rgba(245,158,11,0.15);
693
+ animation: pulse 1.5s ease-in-out infinite;
694
+ }
695
+
696
+ /* Wallet connecting icon */
697
+ .wallet-connecting-icon {
698
+ width: 48px; height: 48px; border-radius: 12px;
699
+ display: flex; align-items: center; justify-content: center;
700
+ animation: pulse 1.5s ease-in-out infinite;
701
+ }
702
+ .wallet-connecting-icon svg { border-radius: 6px; }
703
+
704
+ /* Address badge */
705
+ .address-badge {
706
+ display: inline-flex; align-items: center; gap: 6px;
707
+ padding: 2px 10px; border-radius: 6px;
708
+ background: ${dark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.03)"};
709
+ font-size: 11px; font-family: monospace; color: var(--authon-muted);
710
+ }
711
+ .address-badge .wallet-icon-sm svg { width: 16px; height: 16px; border-radius: 4px; }
712
+
364
713
  /* Loading overlay */
365
714
  #authon-loading-overlay {
366
715
  position: absolute; inset: 0; z-index: 10;
367
- background: ${dark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.92)"};
716
+ background: var(--authon-overlay-bg);
368
717
  backdrop-filter: blur(2px);
369
718
  border-radius: var(--authon-radius);
370
719
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;
@@ -396,11 +745,229 @@ var ModalRenderer = class {
396
745
  @keyframes blink { 0%,80%,100% { opacity: .2; } 40% { opacity: 1; } }
397
746
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
398
747
  @keyframes slideIn { from { opacity: 0; transform: translate(-50%, -48%); } to { opacity: 1; transform: translate(-50%, -50%); } }
748
+ @keyframes check-draw { to { stroke-dashoffset: 0; } }
749
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
399
750
  ${b.customCss || ""}
400
751
  `;
401
752
  }
753
+ // ── Flow Overlay Rendering ──
754
+ renderOverlay() {
755
+ this.renderOverlayWithData({});
756
+ }
757
+ renderOverlayWithData(data) {
758
+ if (!this.shadowRoot) return;
759
+ const container = this.shadowRoot.querySelector(".modal-container");
760
+ if (!container) return;
761
+ this.shadowRoot.getElementById("flow-overlay")?.remove();
762
+ if (this.currentOverlay === "none") return;
763
+ const overlay = document.createElement("div");
764
+ overlay.id = "flow-overlay";
765
+ overlay.className = "flow-overlay";
766
+ overlay.innerHTML = this.buildOverlayContent(data);
767
+ container.appendChild(overlay);
768
+ this.attachOverlayEvents(overlay);
769
+ }
770
+ buildOverlayContent(data) {
771
+ const dark = this.isDark();
772
+ const errorHtml = this.overlayError ? `<div class="overlay-error">${this.escapeHtml(this.overlayError)}</div>` : "";
773
+ switch (this.currentOverlay) {
774
+ case "web3-picker": {
775
+ const walletItems = WALLET_OPTIONS.map(
776
+ (w) => `<button class="wallet-btn" data-wallet="${w.id}">
777
+ <span class="wallet-icon">${walletIconSvg(w.id)}</span>
778
+ <span>${w.name}</span>
779
+ </button>`
780
+ ).join("");
781
+ return `
782
+ <div class="overlay-title" style="margin-bottom: 4px;">Select Wallet</div>
783
+ <div class="wallet-picker">${walletItems}</div>
784
+ ${errorHtml}
785
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
786
+ `;
787
+ }
788
+ case "web3-connecting": {
789
+ const wallet = WALLET_OPTIONS.find((w) => w.id === this.selectedWallet);
790
+ const walletName = wallet?.name ?? this.selectedWallet;
791
+ return `
792
+ <div class="wallet-connecting-icon">${walletIconSvg(this.selectedWallet)}</div>
793
+ <div style="display:flex;align-items:center;gap:8px;">
794
+ <svg class="flow-spinner" width="16" height="16" viewBox="0 0 16 16">
795
+ <circle cx="8" cy="8" r="6" fill="none" stroke="${wallet?.color ?? "#7c3aed"}" stroke-width="2" opacity="0.25"/>
796
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="${wallet?.color ?? "#7c3aed"}" stroke-width="2" stroke-linecap="round"/>
797
+ </svg>
798
+ <span class="overlay-subtitle">Connecting ${this.escapeHtml(walletName)}...</span>
799
+ </div>
800
+ ${errorHtml}
801
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
802
+ `;
803
+ }
804
+ case "web3-success": {
805
+ const wallet = WALLET_OPTIONS.find((w) => w.id === (data.walletId || this.selectedWallet));
806
+ const walletColor = wallet?.color ?? "#8b5cf6";
807
+ const truncAddr = data.truncatedAddress || "0x...";
808
+ return `
809
+ <div class="success-check" style="background:linear-gradient(135deg, ${walletColor}, ${walletColor})">
810
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
811
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
812
+ </svg>
813
+ </div>
814
+ <div class="overlay-title">Wallet Connected</div>
815
+ <div class="address-badge">
816
+ <span class="wallet-icon-sm">${walletIconSvg(data.walletId || this.selectedWallet)}</span>
817
+ <span>${this.escapeHtml(truncAddr)}</span>
818
+ </div>
819
+ `;
820
+ }
821
+ case "passwordless-input": {
822
+ return `
823
+ <div class="overlay-title">Enter your email</div>
824
+ <div class="pwless-form">
825
+ <input type="email" placeholder="you@example.com" class="input" id="pwless-email" autocomplete="email" />
826
+ <button class="pwless-submit" id="pwless-submit-btn">Send Magic Link</button>
827
+ </div>
828
+ ${errorHtml}
829
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
830
+ `;
831
+ }
832
+ case "passwordless-sending": {
833
+ return `
834
+ <svg class="flow-spinner" width="16" height="16" viewBox="0 0 16 16">
835
+ <circle cx="8" cy="8" r="6" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" opacity="0.25"/>
836
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" stroke-linecap="round"/>
837
+ </svg>
838
+ <span class="overlay-subtitle">Sending magic link...</span>
839
+ `;
840
+ }
841
+ case "passwordless-sent": {
842
+ return `
843
+ <div class="success-check" style="background:linear-gradient(135deg, var(--authon-primary-start, #7c3aed), var(--authon-primary-end, #4f46e5))">
844
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
845
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
846
+ </svg>
847
+ </div>
848
+ <div class="overlay-title">Magic link sent!</div>
849
+ <span class="overlay-subtitle">Check your email inbox</span>
850
+ `;
851
+ }
852
+ case "otp-input": {
853
+ const digitInputs = Array.from(
854
+ { length: 6 },
855
+ (_, i) => `<input type="text" inputmode="numeric" maxlength="1" class="otp-digit" data-idx="${i}" autocomplete="one-time-code" />`
856
+ ).join("");
857
+ return `
858
+ <div class="otp-container">
859
+ <div class="overlay-title">Enter verification code</div>
860
+ <span class="overlay-subtitle">6-digit code sent to ${this.escapeHtml(this.overlayEmail)}</span>
861
+ <div class="otp-inputs">${digitInputs}</div>
862
+ ${errorHtml}
863
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
864
+ </div>
865
+ `;
866
+ }
867
+ case "passkey-verifying": {
868
+ return `
869
+ <div class="passkey-icon-pulse">
870
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
871
+ <circle cx="10" cy="7" r="4"/><path d="M10.3 15H7a4 4 0 0 0-4 4v2"/>
872
+ <path d="M21.7 13.3 19 11"/><path d="m21 15-2.5-1.5"/><path d="m17 17 2.5-1.5"/>
873
+ <path d="M22 9v6a1 1 0 0 1-1 1h-.5"/><circle cx="18" cy="9" r="3"/>
874
+ </svg>
875
+ </div>
876
+ <span class="overlay-subtitle">Verifying identity...</span>
877
+ ${errorHtml}
878
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
879
+ `;
880
+ }
881
+ case "passkey-success": {
882
+ return `
883
+ <div class="success-check" style="background:linear-gradient(135deg, var(--authon-primary-start, #7c3aed), var(--authon-primary-end, #4f46e5))">
884
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
885
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
886
+ </svg>
887
+ </div>
888
+ <div class="overlay-title">Identity verified!</div>
889
+ `;
890
+ }
891
+ default:
892
+ return "";
893
+ }
894
+ }
895
+ attachOverlayEvents(overlay) {
896
+ if (!this.shadowRoot) return;
897
+ const cancelBtn = overlay.querySelector("#overlay-cancel");
898
+ if (cancelBtn) {
899
+ cancelBtn.addEventListener("click", () => this.hideOverlay());
900
+ }
901
+ overlay.querySelectorAll(".wallet-btn").forEach((btn) => {
902
+ btn.addEventListener("click", () => {
903
+ const walletId = btn.dataset.wallet;
904
+ if (walletId) {
905
+ this.selectedWallet = walletId;
906
+ this.onWeb3WalletSelect(walletId);
907
+ }
908
+ });
909
+ });
910
+ const pwlessSubmit = overlay.querySelector("#pwless-submit-btn");
911
+ const pwlessEmail = overlay.querySelector("#pwless-email");
912
+ if (pwlessSubmit && pwlessEmail) {
913
+ setTimeout(() => pwlessEmail.focus(), 50);
914
+ const submitHandler = () => {
915
+ const email = pwlessEmail.value.trim();
916
+ if (!email) return;
917
+ this.overlayEmail = email;
918
+ this.showOverlay("passwordless-sending");
919
+ this.onPasswordlessSubmit(email);
920
+ };
921
+ pwlessSubmit.addEventListener("click", submitHandler);
922
+ pwlessEmail.addEventListener("keydown", (e) => {
923
+ if (e.key === "Enter") {
924
+ e.preventDefault();
925
+ submitHandler();
926
+ }
927
+ });
928
+ }
929
+ const otpDigits = overlay.querySelectorAll(".otp-digit");
930
+ if (otpDigits.length === 6) {
931
+ setTimeout(() => otpDigits[0].focus(), 50);
932
+ otpDigits.forEach((digit, idx) => {
933
+ digit.addEventListener("input", () => {
934
+ const val = digit.value.replace(/\D/g, "");
935
+ digit.value = val.slice(0, 1);
936
+ if (val && idx < 5) {
937
+ otpDigits[idx + 1].focus();
938
+ }
939
+ const code = Array.from(otpDigits).map((d) => d.value).join("");
940
+ if (code.length === 6) {
941
+ this.onOtpVerify(this.overlayEmail, code);
942
+ }
943
+ });
944
+ digit.addEventListener("keydown", (e) => {
945
+ if (e.key === "Backspace" && !digit.value && idx > 0) {
946
+ otpDigits[idx - 1].focus();
947
+ otpDigits[idx - 1].value = "";
948
+ }
949
+ });
950
+ digit.addEventListener("paste", (e) => {
951
+ e.preventDefault();
952
+ const pasted = (e.clipboardData?.getData("text") ?? "").replace(/\D/g, "").slice(0, 6);
953
+ if (pasted.length === 0) return;
954
+ for (let i = 0; i < 6; i++) {
955
+ otpDigits[i].value = pasted[i] || "";
956
+ }
957
+ const lastIdx = Math.min(pasted.length, 5);
958
+ otpDigits[lastIdx].focus();
959
+ if (pasted.length === 6) {
960
+ this.onOtpVerify(this.overlayEmail, pasted);
961
+ }
962
+ });
963
+ });
964
+ }
965
+ }
966
+ escapeHtml(str) {
967
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
968
+ }
402
969
  // ── Event binding ──
403
- /** Attach events to shell elements (backdrop, ESC) called once */
970
+ /** Attach events to shell elements (backdrop, ESC) -- called once */
404
971
  attachShellEvents() {
405
972
  if (!this.shadowRoot) return;
406
973
  const backdrop = this.shadowRoot.getElementById("backdrop");
@@ -412,12 +979,18 @@ var ModalRenderer = class {
412
979
  }
413
980
  if (this.mode === "popup") {
414
981
  this.escHandler = (e) => {
415
- if (e.key === "Escape") this.onClose();
982
+ if (e.key === "Escape") {
983
+ if (this.currentOverlay !== "none") {
984
+ this.hideOverlay();
985
+ } else {
986
+ this.onClose();
987
+ }
988
+ }
416
989
  };
417
990
  document.addEventListener("keydown", this.escHandler);
418
991
  }
419
992
  }
420
- /** Attach events to inner content (buttons, form, switch link) called on each view */
993
+ /** Attach events to inner content (buttons, form, switch link) -- called on each view */
421
994
  attachInnerEvents(view) {
422
995
  if (!this.shadowRoot) return;
423
996
  this.shadowRoot.querySelectorAll(".provider-btn").forEach((btn) => {
@@ -451,6 +1024,18 @@ var ModalRenderer = class {
451
1024
  this.open(view === "signIn" ? "signUp" : "signIn");
452
1025
  });
453
1026
  }
1027
+ const web3Btn = this.shadowRoot.getElementById("web3-btn");
1028
+ if (web3Btn) {
1029
+ web3Btn.addEventListener("click", () => this.showOverlay("web3-picker"));
1030
+ }
1031
+ const pwlessBtn = this.shadowRoot.getElementById("passwordless-btn");
1032
+ if (pwlessBtn) {
1033
+ pwlessBtn.addEventListener("click", () => this.showOverlay("passwordless-input"));
1034
+ }
1035
+ const passkeyBtn = this.shadowRoot.getElementById("passkey-btn");
1036
+ if (passkeyBtn) {
1037
+ passkeyBtn.addEventListener("click", () => this.onPasskeyClick());
1038
+ }
454
1039
  }
455
1040
  };
456
1041
 
@@ -979,6 +1564,10 @@ var Authon = class {
979
1564
  await this.ensureInitialized();
980
1565
  this.getModal().open("signUp");
981
1566
  }
1567
+ /** Update theme at runtime without destroying form state */
1568
+ setTheme(theme) {
1569
+ this.getModal().setTheme(theme);
1570
+ }
982
1571
  async signInWithOAuth(provider, options) {
983
1572
  await this.ensureInitialized();
984
1573
  await this.startOAuthFlow(provider, options);
@@ -1321,7 +1910,53 @@ var Authon = class {
1321
1910
  this.emit("error", err instanceof Error ? err : new Error(msg));
1322
1911
  });
1323
1912
  },
1324
- onClose: () => this.modal?.close()
1913
+ onClose: () => this.modal?.close(),
1914
+ onWeb3WalletSelect: async (walletId) => {
1915
+ const chain = walletId === "phantom" ? "solana" : "evm";
1916
+ try {
1917
+ this.modal?.showOverlay?.("web3-connecting");
1918
+ const address = await this.getWalletAddress(walletId);
1919
+ const { message } = await this.web3GetNonce(address, chain, walletId);
1920
+ const signature = await this.requestWalletSignature(walletId, message);
1921
+ await this.web3Verify(message, signature, address, chain, walletId);
1922
+ this.modal?.showWeb3Success(walletId, address);
1923
+ setTimeout(() => this.modal?.close(), 2500);
1924
+ } catch (err) {
1925
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
1926
+ }
1927
+ },
1928
+ onPasswordlessSubmit: async (email) => {
1929
+ try {
1930
+ const method = this.branding?.passwordlessMethod ?? "magic_link";
1931
+ if (method === "email_otp" || method === "both") {
1932
+ await this.sendEmailOtp(email);
1933
+ this.modal?.showOtpInput(email);
1934
+ } else {
1935
+ await this.sendMagicLink(email);
1936
+ this.modal?.showPasswordlessSent();
1937
+ }
1938
+ } catch (err) {
1939
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
1940
+ }
1941
+ },
1942
+ onOtpVerify: async (email, code) => {
1943
+ try {
1944
+ await this.verifyPasswordless({ email, code });
1945
+ this.modal?.close();
1946
+ } catch (err) {
1947
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
1948
+ }
1949
+ },
1950
+ onPasskeyClick: async () => {
1951
+ try {
1952
+ this.modal?.showOverlay?.("passkey-verifying");
1953
+ await this.authenticateWithPasskey();
1954
+ this.modal?.showPasskeySuccess();
1955
+ setTimeout(() => this.modal?.close(), 2500);
1956
+ } catch (err) {
1957
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
1958
+ }
1959
+ }
1325
1960
  });
1326
1961
  }
1327
1962
  if (this.branding) this.modal.setBranding(this.branding);
@@ -1614,6 +2249,30 @@ var Authon = class {
1614
2249
  });
1615
2250
  if (!res.ok) throw new Error(await this.parseApiError(res, path));
1616
2251
  }
2252
+ // ── Wallet helpers ──
2253
+ async getWalletAddress(walletId) {
2254
+ if (walletId === "phantom") {
2255
+ const provider2 = window.solana;
2256
+ if (!provider2?.isPhantom) throw new Error("Phantom wallet not detected. Please install it from phantom.app");
2257
+ const resp = await provider2.connect();
2258
+ return resp.publicKey.toString();
2259
+ }
2260
+ const provider = window.ethereum;
2261
+ if (!provider) throw new Error(`${walletId} wallet not detected. Please install it.`);
2262
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
2263
+ return accounts[0];
2264
+ }
2265
+ async requestWalletSignature(walletId, message) {
2266
+ if (walletId === "phantom") {
2267
+ const provider2 = window.solana;
2268
+ const encoded = new TextEncoder().encode(message);
2269
+ const signed = await provider2.signMessage(encoded, "utf8");
2270
+ return Array.from(new Uint8Array(signed.signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
2271
+ }
2272
+ const provider = window.ethereum;
2273
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
2274
+ return provider.request({ method: "personal_sign", params: [message, accounts[0]] });
2275
+ }
1617
2276
  // ── WebAuthn helpers ──
1618
2277
  bufferToBase64url(buffer) {
1619
2278
  const bytes = new Uint8Array(buffer);