@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.cjs CHANGED
@@ -37,11 +37,46 @@ var AuthonMfaRequiredError = class extends Error {
37
37
  }
38
38
  };
39
39
 
40
- // src/modal.ts
41
- var import_shared2 = require("@authon/shared");
40
+ // ../shared/dist/index.js
41
+ var PROVIDER_DISPLAY_NAMES = {
42
+ google: "Google",
43
+ apple: "Apple",
44
+ kakao: "Kakao",
45
+ naver: "Naver",
46
+ facebook: "Facebook",
47
+ github: "GitHub",
48
+ discord: "Discord",
49
+ x: "X",
50
+ line: "LINE",
51
+ microsoft: "Microsoft"
52
+ };
53
+ var PROVIDER_COLORS = {
54
+ google: { bg: "#ffffff", text: "#1f1f1f" },
55
+ apple: { bg: "#000000", text: "#ffffff" },
56
+ kakao: { bg: "#FEE500", text: "#191919" },
57
+ naver: { bg: "#03C75A", text: "#ffffff" },
58
+ facebook: { bg: "#1877F2", text: "#ffffff" },
59
+ github: { bg: "#24292e", text: "#ffffff" },
60
+ discord: { bg: "#5865F2", text: "#ffffff" },
61
+ x: { bg: "#000000", text: "#ffffff" },
62
+ line: { bg: "#06C755", text: "#ffffff" },
63
+ microsoft: { bg: "#ffffff", text: "#1f1f1f" }
64
+ };
65
+ var DEFAULT_BRANDING = {
66
+ primaryColorStart: "#7c3aed",
67
+ primaryColorEnd: "#4f46e5",
68
+ lightBg: "#ffffff",
69
+ lightText: "#111827",
70
+ darkBg: "#0f172a",
71
+ darkText: "#f1f5f9",
72
+ borderRadius: 12,
73
+ showEmailPassword: true,
74
+ showDivider: true,
75
+ showSecuredBy: true,
76
+ locale: "en"
77
+ };
42
78
 
43
79
  // src/providers.ts
44
- var import_shared = require("@authon/shared");
45
80
  var PROVIDER_ICONS = {
46
81
  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>`,
47
82
  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>`,
@@ -55,10 +90,10 @@ var PROVIDER_ICONS = {
55
90
  microsoft: `<svg viewBox="0 0 24 24" width="20" height="20"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`
56
91
  };
57
92
  function getProviderButtonConfig(provider) {
58
- const colors = import_shared.PROVIDER_COLORS[provider];
93
+ const colors = PROVIDER_COLORS[provider];
59
94
  return {
60
95
  provider,
61
- label: `Continue with ${import_shared.PROVIDER_DISPLAY_NAMES[provider]}`,
96
+ label: `Continue with ${PROVIDER_DISPLAY_NAMES[provider]}`,
62
97
  bgColor: colors.bg,
63
98
  textColor: colors.text,
64
99
  iconSvg: PROVIDER_ICONS[provider]
@@ -66,6 +101,30 @@ function getProviderButtonConfig(provider) {
66
101
  }
67
102
 
68
103
  // src/modal.ts
104
+ function hexToRgba(hex, alpha) {
105
+ const h = hex.replace("#", "");
106
+ const r = parseInt(h.substring(0, 2), 16);
107
+ const g = parseInt(h.substring(2, 4), 16);
108
+ const b = parseInt(h.substring(4, 6), 16);
109
+ return `rgba(${r},${g},${b},${alpha})`;
110
+ }
111
+ var WALLET_OPTIONS = [
112
+ { id: "pexus", name: "Pexus", color: "#7c3aed" },
113
+ { id: "metamask", name: "MetaMask", color: "#f6851b" },
114
+ { id: "phantom", name: "Phantom", color: "#ab9ff2" }
115
+ ];
116
+ function walletIconSvg(id) {
117
+ switch (id) {
118
+ case "pexus":
119
+ 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>`;
120
+ case "metamask":
121
+ 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>`;
122
+ case "phantom":
123
+ 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>`;
124
+ default:
125
+ 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>`;
126
+ }
127
+ }
69
128
  var ModalRenderer = class {
70
129
  shadowRoot = null;
71
130
  hostElement = null;
@@ -73,19 +132,38 @@ var ModalRenderer = class {
73
132
  mode;
74
133
  theme;
75
134
  branding;
135
+ themeObserver = null;
136
+ mediaQueryListener = null;
76
137
  enabledProviders = [];
77
138
  currentView = "signIn";
78
139
  onProviderClick;
79
140
  onEmailSubmit;
80
141
  onClose;
142
+ onWeb3WalletSelect;
143
+ onPasswordlessSubmit;
144
+ onOtpVerify;
145
+ onPasskeyClick;
81
146
  escHandler = null;
147
+ // Overlay state
148
+ currentOverlay = "none";
149
+ selectedWallet = "";
150
+ overlayEmail = "";
151
+ overlayError = "";
82
152
  constructor(options) {
83
153
  this.mode = options.mode;
84
154
  this.theme = options.theme || "auto";
85
- this.branding = { ...import_shared2.DEFAULT_BRANDING, ...options.branding };
155
+ this.branding = { ...DEFAULT_BRANDING, ...options.branding };
86
156
  this.onProviderClick = options.onProviderClick;
87
157
  this.onEmailSubmit = options.onEmailSubmit;
88
158
  this.onClose = options.onClose;
159
+ this.onWeb3WalletSelect = options.onWeb3WalletSelect || (() => {
160
+ });
161
+ this.onPasswordlessSubmit = options.onPasswordlessSubmit || (() => {
162
+ });
163
+ this.onOtpVerify = options.onOtpVerify || (() => {
164
+ });
165
+ this.onPasskeyClick = options.onPasskeyClick || (() => {
166
+ });
89
167
  if (options.mode === "embedded" && options.containerId) {
90
168
  this.containerElement = document.getElementById(options.containerId);
91
169
  }
@@ -94,17 +172,20 @@ var ModalRenderer = class {
94
172
  this.enabledProviders = providers;
95
173
  }
96
174
  setBranding(branding) {
97
- this.branding = { ...import_shared2.DEFAULT_BRANDING, ...branding };
175
+ this.branding = { ...DEFAULT_BRANDING, ...branding };
98
176
  }
99
177
  open(view = "signIn") {
100
178
  if (this.shadowRoot && this.hostElement) {
179
+ this.hideOverlay();
101
180
  this.switchView(view);
102
181
  } else {
103
182
  this.currentView = view;
183
+ this.currentOverlay = "none";
104
184
  this.render(view);
105
185
  }
106
186
  }
107
187
  close() {
188
+ this.stopThemeObserver();
108
189
  if (this.escHandler) {
109
190
  document.removeEventListener("keydown", this.escHandler);
110
191
  this.escHandler = null;
@@ -117,6 +198,46 @@ var ModalRenderer = class {
117
198
  if (this.containerElement) {
118
199
  this.containerElement.innerHTML = "";
119
200
  }
201
+ this.currentOverlay = "none";
202
+ }
203
+ /** Update theme at runtime without destroying form state */
204
+ setTheme(theme) {
205
+ this.theme = theme;
206
+ this.updateThemeCSS();
207
+ if (theme === "auto") {
208
+ this.startThemeObserver();
209
+ } else {
210
+ this.stopThemeObserver();
211
+ }
212
+ }
213
+ updateThemeCSS() {
214
+ if (!this.shadowRoot) return;
215
+ const styleEl = this.shadowRoot.getElementById("authon-theme-style");
216
+ if (styleEl) {
217
+ styleEl.textContent = this.buildCSS();
218
+ }
219
+ }
220
+ startThemeObserver() {
221
+ this.stopThemeObserver();
222
+ if (typeof document === "undefined" || typeof window === "undefined") return;
223
+ this.themeObserver = new MutationObserver(() => this.updateThemeCSS());
224
+ this.themeObserver.observe(document.documentElement, {
225
+ attributes: true,
226
+ attributeFilter: ["data-theme", "class"]
227
+ });
228
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
229
+ this.mediaQueryListener = () => this.updateThemeCSS();
230
+ mq.addEventListener("change", this.mediaQueryListener);
231
+ }
232
+ stopThemeObserver() {
233
+ if (this.themeObserver) {
234
+ this.themeObserver.disconnect();
235
+ this.themeObserver = null;
236
+ }
237
+ if (this.mediaQueryListener) {
238
+ window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", this.mediaQueryListener);
239
+ this.mediaQueryListener = null;
240
+ }
120
241
  }
121
242
  showError(message) {
122
243
  if (!this.shadowRoot) return;
@@ -168,6 +289,47 @@ var ModalRenderer = class {
168
289
  if (!this.shadowRoot) return;
169
290
  this.shadowRoot.getElementById("authon-loading-overlay")?.remove();
170
291
  }
292
+ // ── Flow Overlay Public API ──
293
+ showOverlay(overlay) {
294
+ this.currentOverlay = overlay;
295
+ this.overlayError = "";
296
+ this.renderOverlay();
297
+ }
298
+ hideOverlay() {
299
+ this.currentOverlay = "none";
300
+ this.overlayError = "";
301
+ if (!this.shadowRoot) return;
302
+ this.shadowRoot.getElementById("flow-overlay")?.remove();
303
+ }
304
+ showWeb3Success(walletId, address) {
305
+ this.selectedWallet = walletId;
306
+ this.overlayError = "";
307
+ const truncated = address.length > 10 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;
308
+ this.currentOverlay = "web3-success";
309
+ this.renderOverlayWithData({ truncatedAddress: truncated, walletId });
310
+ }
311
+ showPasswordlessSent() {
312
+ this.overlayError = "";
313
+ this.currentOverlay = "passwordless-sent";
314
+ this.renderOverlay();
315
+ }
316
+ showOtpInput(email) {
317
+ this.overlayEmail = email;
318
+ this.overlayError = "";
319
+ this.currentOverlay = "otp-input";
320
+ this.renderOverlay();
321
+ }
322
+ showPasskeySuccess() {
323
+ this.overlayError = "";
324
+ this.currentOverlay = "passkey-success";
325
+ this.renderOverlay();
326
+ }
327
+ showOverlayError(message) {
328
+ this.overlayError = message;
329
+ if (this.currentOverlay !== "none") {
330
+ this.renderOverlay();
331
+ }
332
+ }
171
333
  // ── Smooth view switch (no flicker) ──
172
334
  switchView(view) {
173
335
  if (!this.shadowRoot || view === this.currentView) return;
@@ -198,13 +360,16 @@ var ModalRenderer = class {
198
360
  this.shadowRoot.innerHTML = this.buildShell(view);
199
361
  this.attachInnerEvents(view);
200
362
  this.attachShellEvents();
363
+ if (this.theme === "auto") {
364
+ this.startThemeObserver();
365
+ }
201
366
  }
202
367
  // ── HTML builders ──
203
368
  /** Shell = style + backdrop + modal-container (stable across view switches) */
204
369
  buildShell(view) {
205
370
  const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
206
371
  return `
207
- <style>${this.buildCSS()}</style>
372
+ <style id="authon-theme-style">${this.buildCSS()}</style>
208
373
  ${popupWrapper}
209
374
  <div class="modal-container" role="dialog" aria-modal="true">
210
375
  <div id="modal-inner" class="modal-inner">
@@ -239,6 +404,35 @@ var ModalRenderer = class {
239
404
  ${isSignUp ? '<p class="password-hint">Must contain uppercase, lowercase, and a number (min 8 chars)</p>' : ""}
240
405
  <button type="submit" class="submit-btn">${isSignUp ? "Sign up" : "Sign in"}</button>
241
406
  </form>` : "";
407
+ const hasMethodAbove = showProviders && this.enabledProviders.length > 0 || b.showEmailPassword !== false;
408
+ const hasMethodBelow = b.showWeb3 || b.showPasswordless || b.showPasskey;
409
+ const methodDivider = hasMethodAbove && hasMethodBelow ? `<div class="divider"><span>or</span></div>` : "";
410
+ const methodButtons = [];
411
+ if (b.showWeb3) {
412
+ methodButtons.push(`<button class="auth-method-btn web3-btn" id="web3-btn">
413
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
414
+ <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"/>
415
+ </svg>
416
+ <span>Connect Wallet</span>
417
+ </button>`);
418
+ }
419
+ if (b.showPasswordless) {
420
+ methodButtons.push(`<button class="auth-method-btn passwordless-btn" id="passwordless-btn">
421
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
422
+ <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"/>
423
+ </svg>
424
+ <span>Continue with Magic Link</span>
425
+ </button>`);
426
+ }
427
+ if (b.showPasskey) {
428
+ methodButtons.push(`<button class="auth-method-btn passkey-btn" id="passkey-btn">
429
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
430
+ <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"/>
431
+ </svg>
432
+ <span>Sign in with Passkey</span>
433
+ </button>`);
434
+ }
435
+ const authMethods = methodButtons.length > 0 ? `<div class="auth-methods">${methodButtons.join("")}</div>` : "";
242
436
  const footer = b.termsUrl || b.privacyUrl ? `<div class="footer">
243
437
  ${b.termsUrl ? `<a href="${b.termsUrl}" target="_blank">Terms of Service</a>` : ""}
244
438
  ${b.termsUrl && b.privacyUrl ? " \xB7 " : ""}
@@ -257,6 +451,8 @@ var ModalRenderer = class {
257
451
  ${showProviders ? `<div class="providers">${providerButtons}</div>` : ""}
258
452
  ${divider}
259
453
  ${emailForm}
454
+ ${methodDivider}
455
+ ${authMethods}
260
456
  <p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
261
457
  ${footer}
262
458
  ${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>` : ""}
@@ -265,6 +461,11 @@ var ModalRenderer = class {
265
461
  isDark() {
266
462
  if (this.theme === "dark") return true;
267
463
  if (this.theme === "light") return false;
464
+ if (typeof document !== "undefined") {
465
+ const html = document.documentElement;
466
+ if (html.classList.contains("dark") || html.getAttribute("data-theme") === "dark") return true;
467
+ if (html.classList.contains("light") || html.getAttribute("data-theme") === "light") return false;
468
+ }
268
469
  return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
269
470
  }
270
471
  buildCSS() {
@@ -288,6 +489,10 @@ var ModalRenderer = class {
288
489
  --authon-border: ${borderColor};
289
490
  --authon-divider: ${dividerColor};
290
491
  --authon-input-bg: ${inputBg};
492
+ --authon-overlay-bg: ${hexToRgba(bg, 0.92)};
493
+ --authon-overlay-bg-solid: ${hexToRgba(bg, 0.97)};
494
+ --authon-backdrop-bg: rgba(0,0,0,${dark ? "0.7" : "0.5"});
495
+ --authon-shadow-opacity: ${dark ? "0.5" : "0.25"};
291
496
  --authon-radius: ${b.borderRadius ?? 12}px;
292
497
  --authon-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
293
498
  font-family: var(--authon-font);
@@ -296,7 +501,7 @@ var ModalRenderer = class {
296
501
  * { box-sizing: border-box; margin: 0; padding: 0; }
297
502
  .backdrop {
298
503
  position: fixed; inset: 0; z-index: 99998;
299
- background: rgba(0,0,0,${dark ? "0.7" : "0.5"}); backdrop-filter: blur(4px);
504
+ background: var(--authon-backdrop-bg); backdrop-filter: blur(4px);
300
505
  animation: fadeIn 0.2s ease;
301
506
  }
302
507
  .modal-container {
@@ -390,10 +595,154 @@ var ModalRenderer = class {
390
595
  }
391
596
  .secured-link { font-weight: 600; color: var(--authon-muted); text-decoration: none; }
392
597
  .secured-link:hover { text-decoration: underline; }
598
+
599
+ /* Auth method buttons */
600
+ .auth-methods { display: flex; flex-direction: column; gap: 8px; }
601
+ .auth-method-btn {
602
+ display: flex; align-items: center; justify-content: center; gap: 8px;
603
+ width: 100%; padding: 10px 16px;
604
+ border-radius: calc(var(--authon-radius) * 0.5);
605
+ font-size: 13px; font-weight: 500; cursor: pointer;
606
+ font-family: var(--authon-font); transition: opacity 0.15s, transform 0.1s;
607
+ }
608
+ .auth-method-btn:hover { opacity: 0.85; }
609
+ .auth-method-btn:active { transform: scale(0.98); }
610
+ /* Web3 -- purple */
611
+ .web3-btn {
612
+ background: ${dark ? "rgba(139,92,246,0.12)" : "rgba(139,92,246,0.08)"};
613
+ border: 1px solid ${dark ? "rgba(139,92,246,0.3)" : "rgba(139,92,246,0.25)"};
614
+ color: ${dark ? "#c4b5fd" : "#7c3aed"};
615
+ }
616
+ /* Passwordless -- cyan */
617
+ .passwordless-btn {
618
+ background: ${dark ? "rgba(6,182,212,0.12)" : "rgba(6,182,212,0.08)"};
619
+ border: 1px solid ${dark ? "rgba(6,182,212,0.3)" : "rgba(6,182,212,0.25)"};
620
+ color: ${dark ? "#67e8f9" : "#0891b2"};
621
+ }
622
+ /* Passkey -- amber */
623
+ .passkey-btn {
624
+ background: ${dark ? "rgba(245,158,11,0.12)" : "rgba(245,158,11,0.08)"};
625
+ border: 1px solid ${dark ? "rgba(245,158,11,0.3)" : "rgba(245,158,11,0.25)"};
626
+ color: ${dark ? "#fcd34d" : "#b45309"};
627
+ }
628
+
629
+ /* Flow overlay */
630
+ .flow-overlay {
631
+ position: absolute; inset: 0; z-index: 10;
632
+ background: var(--authon-overlay-bg-solid);
633
+ backdrop-filter: blur(2px);
634
+ border-radius: var(--authon-radius);
635
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
636
+ gap: 12px; padding: 24px;
637
+ animation: fadeIn 0.2s ease;
638
+ }
639
+ .flow-overlay .cancel-link {
640
+ font-size: 12px; color: var(--authon-dim); cursor: pointer; border: none;
641
+ background: none; font-family: var(--authon-font); margin-top: 4px;
642
+ }
643
+ .flow-overlay .cancel-link:hover { text-decoration: underline; }
644
+ .flow-overlay .overlay-title {
645
+ font-size: 14px; font-weight: 600; color: var(--authon-text); text-align: center;
646
+ }
647
+ .flow-overlay .overlay-subtitle {
648
+ font-size: 12px; color: var(--authon-muted); text-align: center;
649
+ }
650
+ .flow-overlay .overlay-error {
651
+ padding: 6px 12px; margin-top: 4px;
652
+ background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3);
653
+ border-radius: calc(var(--authon-radius) * 0.33);
654
+ font-size: 12px; color: #ef4444; text-align: center; width: 100%;
655
+ }
656
+
657
+ /* Wallet picker */
658
+ .wallet-picker { display: flex; flex-direction: column; gap: 8px; width: 100%; }
659
+ .wallet-btn {
660
+ display: flex; align-items: center; gap: 10px;
661
+ width: 100%; padding: 10px 14px;
662
+ background: ${dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"};
663
+ border: 1px solid ${dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};
664
+ border-radius: calc(var(--authon-radius) * 0.5);
665
+ font-size: 13px; font-weight: 500; color: var(--authon-text);
666
+ cursor: pointer; font-family: var(--authon-font);
667
+ transition: opacity 0.15s;
668
+ }
669
+ .wallet-btn:hover { opacity: 0.8; }
670
+ .wallet-btn .wallet-icon { display: flex; align-items: center; flex-shrink: 0; }
671
+ .wallet-btn .wallet-icon svg { border-radius: 6px; }
672
+
673
+ /* Passwordless email input in overlay */
674
+ .pwless-form { display: flex; flex-direction: column; gap: 10px; width: 100%; }
675
+ .pwless-submit {
676
+ width: 100%; padding: 10px;
677
+ background: linear-gradient(135deg, #06b6d4, #0891b2);
678
+ color: #fff; border: none; border-radius: calc(var(--authon-radius) * 0.5);
679
+ font-size: 13px; font-weight: 600; cursor: pointer;
680
+ font-family: var(--authon-font); transition: opacity 0.15s;
681
+ }
682
+ .pwless-submit:hover { opacity: 0.9; }
683
+ .pwless-submit:disabled { opacity: 0.6; cursor: not-allowed; }
684
+
685
+ /* OTP input */
686
+ .otp-container { display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; }
687
+ .otp-inputs { display: flex; gap: 8px; justify-content: center; }
688
+ .otp-digit {
689
+ width: 40px; height: 48px; text-align: center;
690
+ font-size: 20px; font-weight: 600; font-family: var(--authon-font);
691
+ background: var(--authon-input-bg); color: var(--authon-text);
692
+ border: 1px solid var(--authon-border);
693
+ border-radius: calc(var(--authon-radius) * 0.33);
694
+ outline: none; transition: border-color 0.15s;
695
+ }
696
+ .otp-digit:focus {
697
+ border-color: var(--authon-primary-start);
698
+ box-shadow: 0 0 0 3px rgba(124,58,237,0.15);
699
+ }
700
+
701
+ /* Success check animation */
702
+ .success-check {
703
+ width: 48px; height: 48px; border-radius: 50%;
704
+ display: flex; align-items: center; justify-content: center;
705
+ }
706
+ .success-check svg path {
707
+ stroke-dasharray: 20;
708
+ stroke-dashoffset: 20;
709
+ animation: check-draw 0.4s ease-out 0.1s forwards;
710
+ }
711
+
712
+ /* Spinner */
713
+ .flow-spinner {
714
+ animation: spin 0.8s linear infinite;
715
+ }
716
+
717
+ /* Passkey verifying icon */
718
+ .passkey-icon-pulse {
719
+ width: 48px; height: 48px; border-radius: 50%;
720
+ display: flex; align-items: center; justify-content: center;
721
+ background: rgba(245,158,11,0.15);
722
+ animation: pulse 1.5s ease-in-out infinite;
723
+ }
724
+
725
+ /* Wallet connecting icon */
726
+ .wallet-connecting-icon {
727
+ width: 48px; height: 48px; border-radius: 12px;
728
+ display: flex; align-items: center; justify-content: center;
729
+ animation: pulse 1.5s ease-in-out infinite;
730
+ }
731
+ .wallet-connecting-icon svg { border-radius: 6px; }
732
+
733
+ /* Address badge */
734
+ .address-badge {
735
+ display: inline-flex; align-items: center; gap: 6px;
736
+ padding: 2px 10px; border-radius: 6px;
737
+ background: ${dark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.03)"};
738
+ font-size: 11px; font-family: monospace; color: var(--authon-muted);
739
+ }
740
+ .address-badge .wallet-icon-sm svg { width: 16px; height: 16px; border-radius: 4px; }
741
+
393
742
  /* Loading overlay */
394
743
  #authon-loading-overlay {
395
744
  position: absolute; inset: 0; z-index: 10;
396
- background: ${dark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.92)"};
745
+ background: var(--authon-overlay-bg);
397
746
  backdrop-filter: blur(2px);
398
747
  border-radius: var(--authon-radius);
399
748
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;
@@ -425,11 +774,229 @@ var ModalRenderer = class {
425
774
  @keyframes blink { 0%,80%,100% { opacity: .2; } 40% { opacity: 1; } }
426
775
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
427
776
  @keyframes slideIn { from { opacity: 0; transform: translate(-50%, -48%); } to { opacity: 1; transform: translate(-50%, -50%); } }
777
+ @keyframes check-draw { to { stroke-dashoffset: 0; } }
778
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
428
779
  ${b.customCss || ""}
429
780
  `;
430
781
  }
782
+ // ── Flow Overlay Rendering ──
783
+ renderOverlay() {
784
+ this.renderOverlayWithData({});
785
+ }
786
+ renderOverlayWithData(data) {
787
+ if (!this.shadowRoot) return;
788
+ const container = this.shadowRoot.querySelector(".modal-container");
789
+ if (!container) return;
790
+ this.shadowRoot.getElementById("flow-overlay")?.remove();
791
+ if (this.currentOverlay === "none") return;
792
+ const overlay = document.createElement("div");
793
+ overlay.id = "flow-overlay";
794
+ overlay.className = "flow-overlay";
795
+ overlay.innerHTML = this.buildOverlayContent(data);
796
+ container.appendChild(overlay);
797
+ this.attachOverlayEvents(overlay);
798
+ }
799
+ buildOverlayContent(data) {
800
+ const dark = this.isDark();
801
+ const errorHtml = this.overlayError ? `<div class="overlay-error">${this.escapeHtml(this.overlayError)}</div>` : "";
802
+ switch (this.currentOverlay) {
803
+ case "web3-picker": {
804
+ const walletItems = WALLET_OPTIONS.map(
805
+ (w) => `<button class="wallet-btn" data-wallet="${w.id}">
806
+ <span class="wallet-icon">${walletIconSvg(w.id)}</span>
807
+ <span>${w.name}</span>
808
+ </button>`
809
+ ).join("");
810
+ return `
811
+ <div class="overlay-title" style="margin-bottom: 4px;">Select Wallet</div>
812
+ <div class="wallet-picker">${walletItems}</div>
813
+ ${errorHtml}
814
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
815
+ `;
816
+ }
817
+ case "web3-connecting": {
818
+ const wallet = WALLET_OPTIONS.find((w) => w.id === this.selectedWallet);
819
+ const walletName = wallet?.name ?? this.selectedWallet;
820
+ return `
821
+ <div class="wallet-connecting-icon">${walletIconSvg(this.selectedWallet)}</div>
822
+ <div style="display:flex;align-items:center;gap:8px;">
823
+ <svg class="flow-spinner" width="16" height="16" viewBox="0 0 16 16">
824
+ <circle cx="8" cy="8" r="6" fill="none" stroke="${wallet?.color ?? "#7c3aed"}" stroke-width="2" opacity="0.25"/>
825
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="${wallet?.color ?? "#7c3aed"}" stroke-width="2" stroke-linecap="round"/>
826
+ </svg>
827
+ <span class="overlay-subtitle">Connecting ${this.escapeHtml(walletName)}...</span>
828
+ </div>
829
+ ${errorHtml}
830
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
831
+ `;
832
+ }
833
+ case "web3-success": {
834
+ const wallet = WALLET_OPTIONS.find((w) => w.id === (data.walletId || this.selectedWallet));
835
+ const walletColor = wallet?.color ?? "#8b5cf6";
836
+ const truncAddr = data.truncatedAddress || "0x...";
837
+ return `
838
+ <div class="success-check" style="background:linear-gradient(135deg, ${walletColor}, ${walletColor})">
839
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
840
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
841
+ </svg>
842
+ </div>
843
+ <div class="overlay-title">Wallet Connected</div>
844
+ <div class="address-badge">
845
+ <span class="wallet-icon-sm">${walletIconSvg(data.walletId || this.selectedWallet)}</span>
846
+ <span>${this.escapeHtml(truncAddr)}</span>
847
+ </div>
848
+ `;
849
+ }
850
+ case "passwordless-input": {
851
+ return `
852
+ <div class="overlay-title">Enter your email</div>
853
+ <div class="pwless-form">
854
+ <input type="email" placeholder="you@example.com" class="input" id="pwless-email" autocomplete="email" />
855
+ <button class="pwless-submit" id="pwless-submit-btn">Send Magic Link</button>
856
+ </div>
857
+ ${errorHtml}
858
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
859
+ `;
860
+ }
861
+ case "passwordless-sending": {
862
+ return `
863
+ <svg class="flow-spinner" width="16" height="16" viewBox="0 0 16 16">
864
+ <circle cx="8" cy="8" r="6" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" opacity="0.25"/>
865
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" stroke-linecap="round"/>
866
+ </svg>
867
+ <span class="overlay-subtitle">Sending magic link...</span>
868
+ `;
869
+ }
870
+ case "passwordless-sent": {
871
+ return `
872
+ <div class="success-check" style="background:linear-gradient(135deg, var(--authon-primary-start, #7c3aed), var(--authon-primary-end, #4f46e5))">
873
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
874
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
875
+ </svg>
876
+ </div>
877
+ <div class="overlay-title">Magic link sent!</div>
878
+ <span class="overlay-subtitle">Check your email inbox</span>
879
+ `;
880
+ }
881
+ case "otp-input": {
882
+ const digitInputs = Array.from(
883
+ { length: 6 },
884
+ (_, i) => `<input type="text" inputmode="numeric" maxlength="1" class="otp-digit" data-idx="${i}" autocomplete="one-time-code" />`
885
+ ).join("");
886
+ return `
887
+ <div class="otp-container">
888
+ <div class="overlay-title">Enter verification code</div>
889
+ <span class="overlay-subtitle">6-digit code sent to ${this.escapeHtml(this.overlayEmail)}</span>
890
+ <div class="otp-inputs">${digitInputs}</div>
891
+ ${errorHtml}
892
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
893
+ </div>
894
+ `;
895
+ }
896
+ case "passkey-verifying": {
897
+ return `
898
+ <div class="passkey-icon-pulse">
899
+ <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">
900
+ <circle cx="10" cy="7" r="4"/><path d="M10.3 15H7a4 4 0 0 0-4 4v2"/>
901
+ <path d="M21.7 13.3 19 11"/><path d="m21 15-2.5-1.5"/><path d="m17 17 2.5-1.5"/>
902
+ <path d="M22 9v6a1 1 0 0 1-1 1h-.5"/><circle cx="18" cy="9" r="3"/>
903
+ </svg>
904
+ </div>
905
+ <span class="overlay-subtitle">Verifying identity...</span>
906
+ ${errorHtml}
907
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
908
+ `;
909
+ }
910
+ case "passkey-success": {
911
+ return `
912
+ <div class="success-check" style="background:linear-gradient(135deg, var(--authon-primary-start, #7c3aed), var(--authon-primary-end, #4f46e5))">
913
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
914
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
915
+ </svg>
916
+ </div>
917
+ <div class="overlay-title">Identity verified!</div>
918
+ `;
919
+ }
920
+ default:
921
+ return "";
922
+ }
923
+ }
924
+ attachOverlayEvents(overlay) {
925
+ if (!this.shadowRoot) return;
926
+ const cancelBtn = overlay.querySelector("#overlay-cancel");
927
+ if (cancelBtn) {
928
+ cancelBtn.addEventListener("click", () => this.hideOverlay());
929
+ }
930
+ overlay.querySelectorAll(".wallet-btn").forEach((btn) => {
931
+ btn.addEventListener("click", () => {
932
+ const walletId = btn.dataset.wallet;
933
+ if (walletId) {
934
+ this.selectedWallet = walletId;
935
+ this.onWeb3WalletSelect(walletId);
936
+ }
937
+ });
938
+ });
939
+ const pwlessSubmit = overlay.querySelector("#pwless-submit-btn");
940
+ const pwlessEmail = overlay.querySelector("#pwless-email");
941
+ if (pwlessSubmit && pwlessEmail) {
942
+ setTimeout(() => pwlessEmail.focus(), 50);
943
+ const submitHandler = () => {
944
+ const email = pwlessEmail.value.trim();
945
+ if (!email) return;
946
+ this.overlayEmail = email;
947
+ this.showOverlay("passwordless-sending");
948
+ this.onPasswordlessSubmit(email);
949
+ };
950
+ pwlessSubmit.addEventListener("click", submitHandler);
951
+ pwlessEmail.addEventListener("keydown", (e) => {
952
+ if (e.key === "Enter") {
953
+ e.preventDefault();
954
+ submitHandler();
955
+ }
956
+ });
957
+ }
958
+ const otpDigits = overlay.querySelectorAll(".otp-digit");
959
+ if (otpDigits.length === 6) {
960
+ setTimeout(() => otpDigits[0].focus(), 50);
961
+ otpDigits.forEach((digit, idx) => {
962
+ digit.addEventListener("input", () => {
963
+ const val = digit.value.replace(/\D/g, "");
964
+ digit.value = val.slice(0, 1);
965
+ if (val && idx < 5) {
966
+ otpDigits[idx + 1].focus();
967
+ }
968
+ const code = Array.from(otpDigits).map((d) => d.value).join("");
969
+ if (code.length === 6) {
970
+ this.onOtpVerify(this.overlayEmail, code);
971
+ }
972
+ });
973
+ digit.addEventListener("keydown", (e) => {
974
+ if (e.key === "Backspace" && !digit.value && idx > 0) {
975
+ otpDigits[idx - 1].focus();
976
+ otpDigits[idx - 1].value = "";
977
+ }
978
+ });
979
+ digit.addEventListener("paste", (e) => {
980
+ e.preventDefault();
981
+ const pasted = (e.clipboardData?.getData("text") ?? "").replace(/\D/g, "").slice(0, 6);
982
+ if (pasted.length === 0) return;
983
+ for (let i = 0; i < 6; i++) {
984
+ otpDigits[i].value = pasted[i] || "";
985
+ }
986
+ const lastIdx = Math.min(pasted.length, 5);
987
+ otpDigits[lastIdx].focus();
988
+ if (pasted.length === 6) {
989
+ this.onOtpVerify(this.overlayEmail, pasted);
990
+ }
991
+ });
992
+ });
993
+ }
994
+ }
995
+ escapeHtml(str) {
996
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
997
+ }
431
998
  // ── Event binding ──
432
- /** Attach events to shell elements (backdrop, ESC) called once */
999
+ /** Attach events to shell elements (backdrop, ESC) -- called once */
433
1000
  attachShellEvents() {
434
1001
  if (!this.shadowRoot) return;
435
1002
  const backdrop = this.shadowRoot.getElementById("backdrop");
@@ -441,12 +1008,18 @@ var ModalRenderer = class {
441
1008
  }
442
1009
  if (this.mode === "popup") {
443
1010
  this.escHandler = (e) => {
444
- if (e.key === "Escape") this.onClose();
1011
+ if (e.key === "Escape") {
1012
+ if (this.currentOverlay !== "none") {
1013
+ this.hideOverlay();
1014
+ } else {
1015
+ this.onClose();
1016
+ }
1017
+ }
445
1018
  };
446
1019
  document.addEventListener("keydown", this.escHandler);
447
1020
  }
448
1021
  }
449
- /** Attach events to inner content (buttons, form, switch link) called on each view */
1022
+ /** Attach events to inner content (buttons, form, switch link) -- called on each view */
450
1023
  attachInnerEvents(view) {
451
1024
  if (!this.shadowRoot) return;
452
1025
  this.shadowRoot.querySelectorAll(".provider-btn").forEach((btn) => {
@@ -480,6 +1053,18 @@ var ModalRenderer = class {
480
1053
  this.open(view === "signIn" ? "signUp" : "signIn");
481
1054
  });
482
1055
  }
1056
+ const web3Btn = this.shadowRoot.getElementById("web3-btn");
1057
+ if (web3Btn) {
1058
+ web3Btn.addEventListener("click", () => this.showOverlay("web3-picker"));
1059
+ }
1060
+ const pwlessBtn = this.shadowRoot.getElementById("passwordless-btn");
1061
+ if (pwlessBtn) {
1062
+ pwlessBtn.addEventListener("click", () => this.showOverlay("passwordless-input"));
1063
+ }
1064
+ const passkeyBtn = this.shadowRoot.getElementById("passkey-btn");
1065
+ if (passkeyBtn) {
1066
+ passkeyBtn.addEventListener("click", () => this.onPasskeyClick());
1067
+ }
483
1068
  }
484
1069
  };
485
1070
 
@@ -1008,6 +1593,10 @@ var Authon = class {
1008
1593
  await this.ensureInitialized();
1009
1594
  this.getModal().open("signUp");
1010
1595
  }
1596
+ /** Update theme at runtime without destroying form state */
1597
+ setTheme(theme) {
1598
+ this.getModal().setTheme(theme);
1599
+ }
1011
1600
  async signInWithOAuth(provider, options) {
1012
1601
  await this.ensureInitialized();
1013
1602
  await this.startOAuthFlow(provider, options);
@@ -1350,7 +1939,53 @@ var Authon = class {
1350
1939
  this.emit("error", err instanceof Error ? err : new Error(msg));
1351
1940
  });
1352
1941
  },
1353
- onClose: () => this.modal?.close()
1942
+ onClose: () => this.modal?.close(),
1943
+ onWeb3WalletSelect: async (walletId) => {
1944
+ const chain = walletId === "phantom" ? "solana" : "evm";
1945
+ try {
1946
+ this.modal?.showOverlay?.("web3-connecting");
1947
+ const address = await this.getWalletAddress(walletId);
1948
+ const { message } = await this.web3GetNonce(address, chain, walletId);
1949
+ const signature = await this.requestWalletSignature(walletId, message);
1950
+ await this.web3Verify(message, signature, address, chain, walletId);
1951
+ this.modal?.showWeb3Success(walletId, address);
1952
+ setTimeout(() => this.modal?.close(), 2500);
1953
+ } catch (err) {
1954
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
1955
+ }
1956
+ },
1957
+ onPasswordlessSubmit: async (email) => {
1958
+ try {
1959
+ const method = this.branding?.passwordlessMethod ?? "magic_link";
1960
+ if (method === "email_otp" || method === "both") {
1961
+ await this.sendEmailOtp(email);
1962
+ this.modal?.showOtpInput(email);
1963
+ } else {
1964
+ await this.sendMagicLink(email);
1965
+ this.modal?.showPasswordlessSent();
1966
+ }
1967
+ } catch (err) {
1968
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
1969
+ }
1970
+ },
1971
+ onOtpVerify: async (email, code) => {
1972
+ try {
1973
+ await this.verifyPasswordless({ email, code });
1974
+ this.modal?.close();
1975
+ } catch (err) {
1976
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
1977
+ }
1978
+ },
1979
+ onPasskeyClick: async () => {
1980
+ try {
1981
+ this.modal?.showOverlay?.("passkey-verifying");
1982
+ await this.authenticateWithPasskey();
1983
+ this.modal?.showPasskeySuccess();
1984
+ setTimeout(() => this.modal?.close(), 2500);
1985
+ } catch (err) {
1986
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
1987
+ }
1988
+ }
1354
1989
  });
1355
1990
  }
1356
1991
  if (this.branding) this.modal.setBranding(this.branding);
@@ -1643,6 +2278,30 @@ var Authon = class {
1643
2278
  });
1644
2279
  if (!res.ok) throw new Error(await this.parseApiError(res, path));
1645
2280
  }
2281
+ // ── Wallet helpers ──
2282
+ async getWalletAddress(walletId) {
2283
+ if (walletId === "phantom") {
2284
+ const provider2 = window.solana;
2285
+ if (!provider2?.isPhantom) throw new Error("Phantom wallet not detected. Please install it from phantom.app");
2286
+ const resp = await provider2.connect();
2287
+ return resp.publicKey.toString();
2288
+ }
2289
+ const provider = window.ethereum;
2290
+ if (!provider) throw new Error(`${walletId} wallet not detected. Please install it.`);
2291
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
2292
+ return accounts[0];
2293
+ }
2294
+ async requestWalletSignature(walletId, message) {
2295
+ if (walletId === "phantom") {
2296
+ const provider2 = window.solana;
2297
+ const encoded = new TextEncoder().encode(message);
2298
+ const signed = await provider2.signMessage(encoded, "utf8");
2299
+ return Array.from(new Uint8Array(signed.signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
2300
+ }
2301
+ const provider = window.ethereum;
2302
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
2303
+ return provider.request({ method: "personal_sign", params: [message, accounts[0]] });
2304
+ }
1646
2305
  // ── WebAuthn helpers ──
1647
2306
  bufferToBase64url(buffer) {
1648
2307
  const bytes = new Uint8Array(buffer);