@arcblock/did-connect-core 4.1.52 → 4.1.54

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.
@@ -100,7 +100,7 @@ var lightTokens = {
100
100
  var DESIGN_TOKENS_CSS = (
101
101
  /* css */
102
102
  `
103
- :root {
103
+ :root, :host {
104
104
  /* Backgrounds \u2014 layered depth */
105
105
  --bg-root: ${tokens.bg.root};
106
106
  --bg-surface: ${tokens.bg.surface};
@@ -159,7 +159,7 @@ var DESIGN_TOKENS_CSS = (
159
159
  var DESIGN_TOKENS_LIGHT_CSS = (
160
160
  /* css */
161
161
  `
162
- [data-theme="light"] {
162
+ [data-theme="light"], :host([data-theme="light"]) {
163
163
  --bg-root: ${lightTokens.bg.root};
164
164
  --bg-surface: ${lightTokens.bg.surface};
165
165
  --bg-card: ${lightTokens.bg.card};
@@ -190,8 +190,8 @@ var DESIGN_RESET_CSS = (
190
190
  /* css */
191
191
  `
192
192
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
193
- html { color-scheme: dark; }
194
- html[data-theme="light"] { color-scheme: light; }
193
+ html, :host { color-scheme: dark; }
194
+ html[data-theme="light"], :host([data-theme="light"]) { color-scheme: light; }
195
195
  body {
196
196
  font-family: ${tokens.font.family};
197
197
  background: var(--bg-root);
@@ -378,18 +378,39 @@ if (typeof customElements !== "undefined" && !customElements.get("did-address"))
378
378
  // dist/ui/i18n.js
379
379
  var messages = {
380
380
  en: {
381
- signIn: "Sign in",
382
- signInTo: "Sign in to {appName}",
381
+ emailExplanation: "We will send you a verification code. No password needed.",
382
+ otherMethods: "Other ways to sign in",
383
+ passkeyCreateAccount: "Create an account with Passkey",
384
+ lastUsed: "Last used",
385
+ passkeyExplanation: "Create a new account using your fingerprint, face, or device unlock. Your device will ask you to save a Passkey. Already have an account? Go back to sign in.",
386
+ continueWith: "Continue with {provider}",
387
+ oauthWaiting: "Continue in the {provider} window",
388
+ oauthIncomplete: "Sign-in is not complete. Reopen the window or choose another method.",
389
+ oauthReopen: "Reopen window",
390
+ oauthUnconfirmed: "We could not confirm sign-in. Reload this page to check your session before trying again.",
391
+ oauthCheckSession: "Reload and check sign-in",
392
+ oauthCancelled: "Stopped waiting for sign-in. If the provider window is still open, close it before retrying.",
393
+ oauthDenied: "Authorization was declined. Try again or choose another sign-in method.",
394
+ oauthBlocked: "Your browser blocked the sign-in window. Reopen it or continue on this page.",
395
+ oauthTimedOut: "Sign-in timed out. Reopen the window to start again.",
396
+ oauthFailed: "We could not complete sign-in. Try again or choose another method.",
397
+ oauthFullPage: "Continue on this page",
398
+ emailEdit: "Change email",
399
+ emailCooldown: "Resend in {seconds}s",
400
+ walletRetry: "Wallet did not open? Try again or scan the QR code.",
401
+ tryAgain: "Try again",
402
+ signIn: "Sign in or create an account",
403
+ signInTo: "Sign in or join {appName}",
383
404
  passkeyButton: "Continue with Passkey",
384
405
  passkeySignIn: "Sign in with Passkey",
385
406
  passkeyCreate: "Create Passkey",
386
- passkeyFirstTime: "First time? Create a passkey",
407
+ passkeyFirstTime: "First time? Create an account",
387
408
  passkeyBackToSignIn: "Back to sign in",
388
409
  passkeyWaiting: "Waiting for passkey\u2026",
389
410
  passkeyCreating: "Creating your passkey\u2026",
390
- passkeyNoLocal: "No passkey found on this device.",
411
+ passkeyNoLocal: "Passkey verification was not completed. Retry, use another device, or choose another method.",
391
412
  passkeyUseAnotherDevice: "Use another device",
392
- passkeyCreateInstead: "Create a new passkey instead",
413
+ passkeyCreateInstead: "First time? Create an account",
393
414
  didWalletButton: "Continue with DID Wallet",
394
415
  didWalletButtonShort: "DID Wallet",
395
416
  scanWithWallet: "Scan with your DID Wallet",
@@ -422,26 +443,47 @@ var messages = {
422
443
  emailTitle: "Continue with Email",
423
444
  emailSubmit: "Send Code",
424
445
  emailSending: "Sending code...",
425
- emailSent: "We sent a 6-digit code to {email}",
426
- emailCodePlaceholder: "6-digit code",
446
+ emailSent: "We sent an 8-digit code to {email}",
447
+ emailCodePlaceholder: "8-digit code",
427
448
  emailVerify: "Verify",
428
449
  emailResend: "Resend code",
429
450
  emailInvalid: "Please enter a valid email address.",
430
- emailCodeInvalid: "Please enter the 6-digit code."
451
+ emailCodeInvalid: "Please enter the 8-digit code."
431
452
  },
432
453
  zh: {
433
- signIn: "\u767B\u5F55",
434
- signInTo: "\u767B\u5F55\u5230 {appName}",
454
+ emailExplanation: "\u6211\u4EEC\u4F1A\u53D1\u9001\u9A8C\u8BC1\u7801\uFF0C\u65E0\u9700\u5BC6\u7801\u3002",
455
+ otherMethods: "\u5176\u4ED6\u767B\u5F55\u65B9\u5F0F",
456
+ passkeyCreateAccount: "\u4F7F\u7528 Passkey \u521B\u5EFA\u8D26\u6237",
457
+ lastUsed: "\u4E0A\u6B21\u4F7F\u7528",
458
+ passkeyExplanation: "\u4F7F\u7528\u6307\u7EB9\u3001\u9762\u5BB9\u6216\u8BBE\u5907\u89E3\u9501\u65B9\u5F0F\u521B\u5EFA\u65B0\u8D26\u6237\uFF0C\u65E0\u9700\u5BC6\u7801\u3002\u8BBE\u5907\u4F1A\u63D0\u793A\u4FDD\u5B58 Passkey\u3002\u5DF2\u6709\u8D26\u6237\uFF1F\u8BF7\u8FD4\u56DE\u767B\u5F55\u3002",
459
+ continueWith: "\u4F7F\u7528 {provider} \u7EE7\u7EED",
460
+ oauthWaiting: "\u8BF7\u5728 {provider} \u7A97\u53E3\u4E2D\u7EE7\u7EED",
461
+ oauthIncomplete: "\u767B\u5F55\u5C1A\u672A\u5B8C\u6210\u3002\u60A8\u53EF\u4EE5\u91CD\u65B0\u6253\u5F00\u7A97\u53E3\u6216\u9009\u62E9\u5176\u4ED6\u65B9\u5F0F\u3002",
462
+ oauthReopen: "\u91CD\u65B0\u6253\u5F00\u7A97\u53E3",
463
+ oauthUnconfirmed: "\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u767B\u5F55\u7ED3\u679C\u3002\u8BF7\u5148\u91CD\u65B0\u52A0\u8F7D\u9875\u9762\u68C0\u67E5\u767B\u5F55\u72B6\u6001\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u91CD\u8BD5\u3002",
464
+ oauthCheckSession: "\u91CD\u65B0\u52A0\u8F7D\u5E76\u68C0\u67E5\u767B\u5F55\u72B6\u6001",
465
+ oauthCancelled: "\u5DF2\u505C\u6B62\u7B49\u5F85\u767B\u5F55\u3002\u82E5\u6388\u6743\u7A97\u53E3\u4ECD\u7136\u6253\u5F00\uFF0C\u8BF7\u5173\u95ED\u540E\u518D\u91CD\u8BD5\u3002",
466
+ oauthDenied: "\u60A8\u672A\u540C\u610F\u6388\u6743\u3002\u53EF\u4EE5\u91CD\u8BD5\u6216\u9009\u62E9\u5176\u4ED6\u767B\u5F55\u65B9\u5F0F\u3002",
467
+ oauthBlocked: "\u6D4F\u89C8\u5668\u62E6\u622A\u4E86\u767B\u5F55\u7A97\u53E3\u3002\u8BF7\u91CD\u65B0\u6253\u5F00\uFF0C\u6216\u5728\u5F53\u524D\u9875\u9762\u7EE7\u7EED\u3002",
468
+ oauthTimedOut: "\u767B\u5F55\u7B49\u5F85\u5DF2\u8D85\u65F6\u3002\u8BF7\u91CD\u65B0\u6253\u5F00\u7A97\u53E3\u518D\u8BD5\u3002",
469
+ oauthFailed: "\u672A\u80FD\u5B8C\u6210\u767B\u5F55\u3002\u8BF7\u91CD\u8BD5\u6216\u9009\u62E9\u5176\u4ED6\u65B9\u5F0F\u3002",
470
+ oauthFullPage: "\u5728\u5F53\u524D\u9875\u9762\u7EE7\u7EED",
471
+ emailEdit: "\u4FEE\u6539\u90AE\u7BB1",
472
+ emailCooldown: "{seconds} \u79D2\u540E\u91CD\u53D1",
473
+ walletRetry: "\u94B1\u5305\u672A\u6253\u5F00\uFF1F\u8BF7\u91CD\u8BD5\u6216\u626B\u63CF\u4E8C\u7EF4\u7801\u3002",
474
+ tryAgain: "\u91CD\u8BD5",
475
+ signIn: "\u767B\u5F55\u6216\u521B\u5EFA\u8D26\u6237",
476
+ signInTo: "\u767B\u5F55\u6216\u52A0\u5165 {appName}",
435
477
  passkeyButton: "\u4F7F\u7528 Passkey \u7EE7\u7EED",
436
478
  passkeySignIn: "\u4F7F\u7528 Passkey \u767B\u5F55",
437
479
  passkeyCreate: "\u521B\u5EFA Passkey",
438
- passkeyFirstTime: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u521B\u5EFA Passkey",
480
+ passkeyFirstTime: "\u9996\u6B21\u4F7F\u7528\uFF1F\u521B\u5EFA\u8D26\u6237",
439
481
  passkeyBackToSignIn: "\u8FD4\u56DE\u767B\u5F55",
440
482
  passkeyWaiting: "\u6B63\u5728\u7B49\u5F85 Passkey\u2026",
441
483
  passkeyCreating: "\u6B63\u5728\u521B\u5EFA Passkey\u2026",
442
- passkeyNoLocal: "\u6B64\u8BBE\u5907\u4E0A\u6CA1\u6709\u627E\u5230 Passkey\u3002",
484
+ passkeyNoLocal: "\u8FD9\u6B21\u672A\u5B8C\u6210 Passkey \u9A8C\u8BC1\u3002\u8BF7\u91CD\u8BD5\u3001\u4F7F\u7528\u5176\u4ED6\u8BBE\u5907\uFF0C\u6216\u9009\u62E9\u5176\u4ED6\u767B\u5F55\u65B9\u5F0F\u3002",
443
485
  passkeyUseAnotherDevice: "\u7528\u5176\u4ED6\u8BBE\u5907\u767B\u5F55",
444
- passkeyCreateInstead: "\u6539\u4E3A\u521B\u5EFA\u65B0\u7684 Passkey",
486
+ passkeyCreateInstead: "\u9996\u6B21\u4F7F\u7528\uFF1F\u521B\u5EFA\u8D26\u6237",
445
487
  didWalletButton: "\u4F7F\u7528 DID Wallet \u7EE7\u7EED",
446
488
  didWalletButtonShort: "DID Wallet",
447
489
  scanWithWallet: "\u8BF7\u4F7F\u7528 DID Wallet \u626B\u7801",
@@ -475,25 +517,46 @@ var messages = {
475
517
  emailSubmit: "\u53D1\u9001\u9A8C\u8BC1\u7801",
476
518
  emailSending: "\u6B63\u5728\u53D1\u9001\u9A8C\u8BC1\u7801...",
477
519
  emailSent: "\u9A8C\u8BC1\u7801\u5DF2\u53D1\u9001\u81F3 {email}",
478
- emailCodePlaceholder: "6 \u4F4D\u9A8C\u8BC1\u7801",
520
+ emailCodePlaceholder: "8 \u4F4D\u9A8C\u8BC1\u7801",
479
521
  emailVerify: "\u9A8C\u8BC1",
480
522
  emailResend: "\u91CD\u65B0\u53D1\u9001",
481
523
  emailInvalid: "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740\u3002",
482
- emailCodeInvalid: "\u8BF7\u8F93\u5165 6 \u4F4D\u9A8C\u8BC1\u7801\u3002"
524
+ emailCodeInvalid: "\u8BF7\u8F93\u5165 8 \u4F4D\u9A8C\u8BC1\u7801\u3002"
483
525
  },
484
526
  "zh-TW": {
485
- signIn: "\u767B\u5165",
486
- signInTo: "\u767B\u5165\u81F3 {appName}",
527
+ emailExplanation: "\u6211\u5011\u6703\u50B3\u9001\u9A57\u8B49\u78BC\uFF0C\u7121\u9700\u5BC6\u78BC\u3002",
528
+ otherMethods: "\u5176\u4ED6\u767B\u5165\u65B9\u5F0F",
529
+ passkeyCreateAccount: "\u4F7F\u7528 Passkey \u5EFA\u7ACB\u5E33\u6236",
530
+ lastUsed: "\u4E0A\u6B21\u4F7F\u7528",
531
+ passkeyExplanation: "\u4F7F\u7528\u6307\u7D0B\u3001\u9762\u5BB9\u6216\u88DD\u7F6E\u89E3\u9396\u65B9\u5F0F\u5EFA\u7ACB\u65B0\u5E33\u6236\uFF0C\u7121\u9700\u5BC6\u78BC\u3002\u88DD\u7F6E\u6703\u63D0\u793A\u5132\u5B58 Passkey\u3002\u5DF2\u6709\u5E33\u6236\uFF1F\u8ACB\u8FD4\u56DE\u767B\u5165\u3002",
532
+ continueWith: "\u4F7F\u7528 {provider} \u7E7C\u7E8C",
533
+ oauthWaiting: "\u8ACB\u5728 {provider} \u8996\u7A97\u4E2D\u7E7C\u7E8C",
534
+ oauthIncomplete: "\u767B\u5165\u5C1A\u672A\u5B8C\u6210\u3002\u60A8\u53EF\u4EE5\u91CD\u65B0\u958B\u555F\u8996\u7A97\u6216\u9078\u64C7\u5176\u4ED6\u65B9\u5F0F\u3002",
535
+ oauthReopen: "\u91CD\u65B0\u958B\u555F\u8996\u7A97",
536
+ oauthUnconfirmed: "\u66AB\u6642\u7121\u6CD5\u78BA\u8A8D\u767B\u5165\u7D50\u679C\u3002\u8ACB\u5148\u91CD\u65B0\u8F09\u5165\u9801\u9762\u6AA2\u67E5\u767B\u5165\u72C0\u614B\uFF0C\u518D\u6C7A\u5B9A\u662F\u5426\u91CD\u8A66\u3002",
537
+ oauthCheckSession: "\u91CD\u65B0\u8F09\u5165\u4E26\u6AA2\u67E5\u767B\u5165\u72C0\u614B",
538
+ oauthCancelled: "\u5DF2\u505C\u6B62\u7B49\u5F85\u767B\u5165\u3002\u82E5\u6388\u6B0A\u8996\u7A97\u4ECD\u7136\u958B\u555F\uFF0C\u8ACB\u95DC\u9589\u5F8C\u518D\u91CD\u8A66\u3002",
539
+ oauthDenied: "\u60A8\u672A\u540C\u610F\u6388\u6B0A\u3002\u53EF\u4EE5\u91CD\u8A66\u6216\u9078\u64C7\u5176\u4ED6\u767B\u5165\u65B9\u5F0F\u3002",
540
+ oauthBlocked: "\u700F\u89BD\u5668\u5C01\u9396\u4E86\u767B\u5165\u8996\u7A97\u3002\u8ACB\u91CD\u65B0\u958B\u555F\uFF0C\u6216\u5728\u76EE\u524D\u9801\u9762\u7E7C\u7E8C\u3002",
541
+ oauthTimedOut: "\u767B\u5165\u7B49\u5F85\u5DF2\u903E\u6642\u3002\u8ACB\u91CD\u65B0\u958B\u555F\u8996\u7A97\u518D\u8A66\u3002",
542
+ oauthFailed: "\u672A\u80FD\u5B8C\u6210\u767B\u5165\u3002\u8ACB\u91CD\u8A66\u6216\u9078\u64C7\u5176\u4ED6\u65B9\u5F0F\u3002",
543
+ oauthFullPage: "\u5728\u76EE\u524D\u9801\u9762\u7E7C\u7E8C",
544
+ emailEdit: "\u4FEE\u6539\u96FB\u5B50\u90F5\u4EF6",
545
+ emailCooldown: "{seconds} \u79D2\u5F8C\u91CD\u65B0\u50B3\u9001",
546
+ walletRetry: "\u9322\u5305\u672A\u958B\u555F\uFF1F\u8ACB\u91CD\u8A66\u6216\u6383\u78BC\u3002",
547
+ tryAgain: "\u91CD\u8A66",
548
+ signIn: "\u767B\u5165\u6216\u5EFA\u7ACB\u5E33\u6236",
549
+ signInTo: "\u767B\u5165\u6216\u52A0\u5165 {appName}",
487
550
  passkeyButton: "\u4F7F\u7528 Passkey \u7E7C\u7E8C",
488
551
  passkeySignIn: "\u4F7F\u7528 Passkey \u767B\u5165",
489
552
  passkeyCreate: "\u5EFA\u7ACB Passkey",
490
- passkeyFirstTime: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB Passkey",
553
+ passkeyFirstTime: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB\u5E33\u6236",
491
554
  passkeyBackToSignIn: "\u8FD4\u56DE\u767B\u5165",
492
555
  passkeyWaiting: "\u6B63\u5728\u7B49\u5F85 Passkey\u2026",
493
556
  passkeyCreating: "\u6B63\u5728\u5EFA\u7ACB Passkey\u2026",
494
- passkeyNoLocal: "\u6B64\u88DD\u7F6E\u4E0A\u6C92\u6709\u627E\u5230 Passkey\u3002",
557
+ passkeyNoLocal: "\u9019\u6B21\u672A\u5B8C\u6210 Passkey \u9A57\u8B49\u3002\u8ACB\u91CD\u8A66\u3001\u4F7F\u7528\u5176\u4ED6\u88DD\u7F6E\uFF0C\u6216\u9078\u64C7\u5176\u4ED6\u767B\u5165\u65B9\u5F0F\u3002",
495
558
  passkeyUseAnotherDevice: "\u7528\u5176\u4ED6\u88DD\u7F6E\u767B\u5165",
496
- passkeyCreateInstead: "\u6539\u70BA\u5EFA\u7ACB\u65B0\u7684 Passkey",
559
+ passkeyCreateInstead: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB\u5E33\u6236",
497
560
  didWalletButton: "\u4F7F\u7528 DID Wallet \u7E7C\u7E8C",
498
561
  didWalletButtonShort: "DID Wallet",
499
562
  scanWithWallet: "\u8ACB\u4F7F\u7528 DID Wallet \u6383\u78BC",
@@ -527,25 +590,46 @@ var messages = {
527
590
  emailSubmit: "\u50B3\u9001\u9A57\u8B49\u78BC",
528
591
  emailSending: "\u6B63\u5728\u50B3\u9001\u9A57\u8B49\u78BC...",
529
592
  emailSent: "\u9A57\u8B49\u78BC\u5DF2\u50B3\u9001\u81F3 {email}",
530
- emailCodePlaceholder: "6 \u4F4D\u6578\u9A57\u8B49\u78BC",
593
+ emailCodePlaceholder: "8 \u4F4D\u6578\u9A57\u8B49\u78BC",
531
594
  emailVerify: "\u9A57\u8B49",
532
595
  emailResend: "\u91CD\u65B0\u50B3\u9001",
533
596
  emailInvalid: "\u8ACB\u8F38\u5165\u6709\u6548\u7684\u96FB\u5B50\u90F5\u4EF6\u5730\u5740\u3002",
534
- emailCodeInvalid: "\u8ACB\u8F38\u5165 6 \u4F4D\u6578\u9A57\u8B49\u78BC\u3002"
597
+ emailCodeInvalid: "\u8ACB\u8F38\u5165 8 \u4F4D\u6578\u9A57\u8B49\u78BC\u3002"
535
598
  },
536
599
  ja: {
537
- signIn: "\u30ED\u30B0\u30A4\u30F3",
538
- signInTo: "{appName} \u306B\u30ED\u30B0\u30A4\u30F3",
600
+ emailExplanation: "\u78BA\u8A8D\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u307E\u3059\u3002\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u4E0D\u8981\u3067\u3059\u3002",
601
+ otherMethods: "\u4ED6\u306E\u30ED\u30B0\u30A4\u30F3\u65B9\u6CD5",
602
+ passkeyCreateAccount: "Passkey \u3067\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
603
+ lastUsed: "\u524D\u56DE\u4F7F\u7528",
604
+ passkeyExplanation: "\u6307\u7D0B\u3001\u9854\u8A8D\u8A3C\u3001\u307E\u305F\u306F\u30C7\u30D0\u30A4\u30B9\u306E\u30ED\u30C3\u30AF\u89E3\u9664\u3067\u65B0\u3057\u3044\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210\u3057\u307E\u3059\u3002\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u4E0D\u8981\u3067\u3059\u3002\u30C7\u30D0\u30A4\u30B9\u304C Passkey \u306E\u4FDD\u5B58\u3092\u6C42\u3081\u307E\u3059\u3002\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u304A\u6301\u3061\u3067\u3059\u304B\uFF1F\u30ED\u30B0\u30A4\u30F3\u306B\u623B\u3063\u3066\u304F\u3060\u3055\u3044\u3002",
605
+ continueWith: "{provider} \u3067\u7D9A\u3051\u308B",
606
+ oauthWaiting: "{provider} \u306E\u30A6\u30A3\u30F3\u30C9\u30A6\u3067\u7D9A\u3051\u3066\u304F\u3060\u3055\u3044",
607
+ oauthIncomplete: "\u30ED\u30B0\u30A4\u30F3\u306F\u307E\u3060\u5B8C\u4E86\u3057\u3066\u3044\u307E\u305B\u3093\u3002\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304D\u76F4\u3059\u304B\u3001\u5225\u306E\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002",
608
+ oauthReopen: "\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304D\u76F4\u3059",
609
+ oauthUnconfirmed: "\u30ED\u30B0\u30A4\u30F3\u7D50\u679C\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u306E\u524D\u306B\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
610
+ oauthCheckSession: "\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u30ED\u30B0\u30A4\u30F3\u3092\u78BA\u8A8D",
611
+ oauthCancelled: "\u30ED\u30B0\u30A4\u30F3\u306E\u5F85\u6A5F\u3092\u7D42\u4E86\u3057\u307E\u3057\u305F\u3002\u8A8D\u8A3C\u30A6\u30A3\u30F3\u30C9\u30A6\u304C\u958B\u3044\u3066\u3044\u308B\u5834\u5408\u306F\u3001\u9589\u3058\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
612
+ oauthDenied: "\u8A8D\u8A3C\u304C\u8A31\u53EF\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u3059\u308B\u304B\u3001\u5225\u306E\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002",
613
+ oauthBlocked: "\u30D6\u30E9\u30A6\u30B6\u304C\u30ED\u30B0\u30A4\u30F3\u753B\u9762\u3092\u30D6\u30ED\u30C3\u30AF\u3057\u307E\u3057\u305F\u3002\u958B\u304D\u76F4\u3059\u304B\u3001\u3053\u306E\u30DA\u30FC\u30B8\u3067\u7D9A\u3051\u3066\u304F\u3060\u3055\u3044\u3002",
614
+ oauthTimedOut: "\u30ED\u30B0\u30A4\u30F3\u304C\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u307E\u3057\u305F\u3002\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304D\u76F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
615
+ oauthFailed: "\u30ED\u30B0\u30A4\u30F3\u3092\u5B8C\u4E86\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u3059\u308B\u304B\u3001\u5225\u306E\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002",
616
+ oauthFullPage: "\u3053\u306E\u30DA\u30FC\u30B8\u3067\u7D9A\u3051\u308B",
617
+ emailEdit: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5909\u66F4",
618
+ emailCooldown: "{seconds} \u79D2\u5F8C\u306B\u518D\u9001\u4FE1",
619
+ walletRetry: "\u30A6\u30A9\u30EC\u30C3\u30C8\u304C\u958B\u304B\u306A\u3044\u5834\u5408\u306F\u3001\u518D\u8A66\u884C\u3059\u308B\u304B QR \u30B3\u30FC\u30C9\u3092\u30B9\u30AD\u30E3\u30F3\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
620
+ tryAgain: "\u518D\u8A66\u884C",
621
+ signIn: "\u30ED\u30B0\u30A4\u30F3\u307E\u305F\u306F\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210",
622
+ signInTo: "{appName} \u306B\u30ED\u30B0\u30A4\u30F3\u307E\u305F\u306F\u53C2\u52A0",
539
623
  passkeyButton: "Passkey \u3067\u7D9A\u3051\u308B",
540
624
  passkeySignIn: "Passkey \u3067\u30ED\u30B0\u30A4\u30F3",
541
625
  passkeyCreate: "Passkey \u3092\u4F5C\u6210",
542
- passkeyFirstTime: "\u521D\u3081\u3066\u3067\u3059\u304B\uFF1FPasskey \u3092\u4F5C\u6210",
626
+ passkeyFirstTime: "\u521D\u3081\u3066\u3067\u3059\u304B\uFF1F\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
543
627
  passkeyBackToSignIn: "\u30ED\u30B0\u30A4\u30F3\u306B\u623B\u308B",
544
628
  passkeyWaiting: "Passkey \u3092\u5F85\u3063\u3066\u3044\u307E\u3059\u2026",
545
629
  passkeyCreating: "Passkey \u3092\u4F5C\u6210\u3057\u3066\u3044\u307E\u3059\u2026",
546
- passkeyNoLocal: "\u3053\u306E\u30C7\u30D0\u30A4\u30B9\u306B Passkey \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
630
+ passkeyNoLocal: "\u4ECA\u56DE\u306F Passkey \u306E\u78BA\u8A8D\u304C\u5B8C\u4E86\u3057\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u3059\u308B\u304B\u3001\u5225\u306E\u30C7\u30D0\u30A4\u30B9\u3092\u4F7F\u3046\u304B\u3001\u5225\u306E\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002",
547
631
  passkeyUseAnotherDevice: "\u5225\u306E\u30C7\u30D0\u30A4\u30B9\u3092\u4F7F\u7528",
548
- passkeyCreateInstead: "\u4EE3\u308F\u308A\u306B\u65B0\u3057\u3044 Passkey \u3092\u4F5C\u6210",
632
+ passkeyCreateInstead: "\u521D\u3081\u3066\u3067\u3059\u304B\uFF1F\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
549
633
  didWalletButton: "DID Wallet \u3067\u7D9A\u3051\u308B",
550
634
  didWalletButtonShort: "DID Wallet",
551
635
  scanWithWallet: "DID Wallet \u3067\u30B9\u30AD\u30E3\u30F3\u3057\u3066\u304F\u3060\u3055\u3044",
@@ -578,12 +662,12 @@ var messages = {
578
662
  emailTitle: "\u30E1\u30FC\u30EB\u3067\u7D9A\u3051\u308B",
579
663
  emailSubmit: "\u30B3\u30FC\u30C9\u3092\u9001\u4FE1",
580
664
  emailSending: "\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u3066\u3044\u307E\u3059...",
581
- emailSent: "{email} \u306B 6 \u6841\u306E\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u307E\u3057\u305F",
582
- emailCodePlaceholder: "6 \u6841\u306E\u30B3\u30FC\u30C9",
665
+ emailSent: "{email} \u306B 8 \u6841\u306E\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u307E\u3057\u305F",
666
+ emailCodePlaceholder: "8 \u6841\u306E\u30B3\u30FC\u30C9",
583
667
  emailVerify: "\u78BA\u8A8D",
584
668
  emailResend: "\u30B3\u30FC\u30C9\u3092\u518D\u9001\u4FE1",
585
669
  emailInvalid: "\u6709\u52B9\u306A\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
586
- emailCodeInvalid: "6 \u6841\u306E\u30B3\u30FC\u30C9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
670
+ emailCodeInvalid: "8 \u6841\u306E\u30B3\u30FC\u30C9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
587
671
  }
588
672
  };
589
673
  var SUPPORTED_LOCALES = [
@@ -592,7 +676,7 @@ var SUPPORTED_LOCALES = [
592
676
  ];
593
677
  function t(key, locale, params) {
594
678
  const lang = locale && messages[locale] ? locale : "en";
595
- let msg = messages[lang][key] ?? key;
679
+ let msg = messages[lang][key] ?? messages.en[key] ?? key;
596
680
  if (params) {
597
681
  for (const [k, v] of Object.entries(params)) {
598
682
  msg = msg.replace(new RegExp(`\\{${k}\\}`, "g"), v);
@@ -2258,8 +2342,17 @@ var qrcode_default = qrcode;
2258
2342
  var stringToBytes = qrcode.stringToBytes;
2259
2343
 
2260
2344
  // dist/ui/qr-renderer.js
2345
+ var QR_PLATE_DARK = "#e8e8e8";
2346
+ var QR_PLATE_LIGHT = "#ffffff";
2347
+ function plateColor(container) {
2348
+ const root = container.getRootNode();
2349
+ const themeEl = root instanceof ShadowRoot ? root.host : document.documentElement;
2350
+ const theme = themeEl instanceof Element ? themeEl.getAttribute("data-theme") : null;
2351
+ return theme === "light" ? QR_PLATE_LIGHT : QR_PLATE_DARK;
2352
+ }
2261
2353
  function renderQR(container, url, size = 200) {
2262
2354
  clearQR(container);
2355
+ container.dataset.qrUrl = url;
2263
2356
  const canvas = document.createElement("canvas");
2264
2357
  const ctx = canvas.getContext?.("2d") ?? null;
2265
2358
  if (!ctx) {
@@ -2283,7 +2376,7 @@ function renderQR(container, url, size = 200) {
2283
2376
  canvas.style.width = `${size}px`;
2284
2377
  canvas.style.height = `${size}px`;
2285
2378
  canvas.style.imageRendering = "pixelated";
2286
- const bgColor = "#e8e8e8";
2379
+ const bgColor = plateColor(container);
2287
2380
  ctx.fillStyle = bgColor;
2288
2381
  ctx.fillRect(0, 0, actualSize, actualSize);
2289
2382
  ctx.fillStyle = "#000000";
@@ -2295,12 +2388,18 @@ function renderQR(container, url, size = 200) {
2295
2388
  }
2296
2389
  }
2297
2390
  const wrapper = document.createElement("div");
2298
- wrapper.style.cssText = "display:inline-block;padding:10px;background:#e8e8e8;border-radius:8px;line-height:0";
2391
+ wrapper.className = "qr-plate";
2392
+ wrapper.style.display = "inline-block";
2393
+ wrapper.style.padding = "10px";
2394
+ wrapper.style.backgroundColor = bgColor;
2395
+ wrapper.style.borderRadius = "8px";
2396
+ wrapper.style.lineHeight = "0";
2299
2397
  wrapper.appendChild(canvas);
2300
2398
  container.appendChild(wrapper);
2301
2399
  }
2302
2400
  function clearQR(container) {
2303
2401
  container.innerHTML = "";
2402
+ delete container.dataset.qrUrl;
2304
2403
  }
2305
2404
 
2306
2405
  // dist/ui/styles.js
@@ -2317,6 +2416,14 @@ body {
2317
2416
  justify-content: center;
2318
2417
  background-image: radial-gradient(ellipse at 50% 0%, rgba(108,71,255,0.08) 0%, transparent 60%);
2319
2418
  }
2419
+ /* The body rule does not apply inside a shadow tree. The host page must
2420
+ give the LoginPage container a height for this centering to show. */
2421
+ :host {
2422
+ display: flex;
2423
+ align-items: center;
2424
+ justify-content: center;
2425
+ min-height: 100%;
2426
+ }
2320
2427
 
2321
2428
  /* Card \u2014 fixed width prevents shrinking when views switch */
2322
2429
  .card {
@@ -2614,19 +2721,25 @@ h1 {
2614
2721
  /* Email section (methods view) */
2615
2722
  .email-section { margin-top: 8px; }
2616
2723
 
2617
- /* Back button */
2724
+ /* Back button \u2014 same top row as Cancel and the locale/theme tools.
2725
+ A float sat inside the 48px content padding, so Back landed below the
2726
+ absolute locale pill. */
2618
2727
  .back-btn {
2728
+ position: absolute;
2729
+ top: 16px;
2730
+ left: 40px;
2619
2731
  display: inline-flex;
2620
2732
  align-items: center;
2621
2733
  gap: 4px;
2734
+ height: 28px;
2622
2735
  background: none;
2623
2736
  border: none;
2624
2737
  color: var(--text-secondary);
2625
2738
  font-size: 14px;
2739
+ line-height: 1;
2626
2740
  cursor: pointer;
2627
2741
  padding: 0;
2628
- margin-bottom: 16px;
2629
- float: left;
2742
+ margin: 0;
2630
2743
  }
2631
2744
  .back-btn:hover { color: var(--text); }
2632
2745
  .back-btn svg { width: 18px; height: 18px; }
@@ -2651,6 +2764,7 @@ h1 {
2651
2764
  line-height: 1;
2652
2765
  text-decoration: none;
2653
2766
  cursor: pointer;
2767
+ height: 28px;
2654
2768
  padding: 2px 4px 2px 0;
2655
2769
  transition: color 0.15s ease;
2656
2770
  }
@@ -2663,7 +2777,6 @@ h1 {
2663
2777
  font-weight: 600;
2664
2778
  color: var(--text-white);
2665
2779
  margin-bottom: 8px;
2666
- clear: both;
2667
2780
  padding-top: 8px;
2668
2781
  }
2669
2782
 
@@ -2837,23 +2950,32 @@ h1 {
2837
2950
  [data-theme="light"] body {
2838
2951
  background-image: radial-gradient(ellipse at 50% 0%, rgba(108,71,255,0.05) 0%, transparent 60%);
2839
2952
  }
2840
- [data-theme="light"] .card {
2953
+ [data-theme="light"] .card,
2954
+ :host([data-theme="light"]) .card {
2841
2955
  box-shadow: var(--shadow-lg), 0 0 80px -20px rgba(108,71,255,0.08);
2842
2956
  }
2843
- [data-theme="light"] .btn:hover {
2957
+ [data-theme="light"] .btn:hover,
2958
+ :host([data-theme="light"]) .btn:hover {
2844
2959
  box-shadow: 0 2px 8px rgba(108,71,255,0.15);
2845
2960
  }
2846
2961
 
2847
- /* Language switcher */
2848
- .locale-switcher {
2962
+ /* Language sits on the same top row as Back / Cancel. */
2963
+ .card-tools {
2849
2964
  position: absolute;
2850
2965
  top: 16px;
2851
2966
  right: 16px;
2967
+ z-index: 2;
2968
+ display: flex;
2969
+ align-items: flex-start;
2970
+ }
2971
+ .locale-switcher {
2972
+ position: relative;
2852
2973
  }
2853
2974
  .locale-btn {
2854
2975
  display: inline-flex;
2855
2976
  align-items: center;
2856
2977
  gap: 4px;
2978
+ height: 28px;
2857
2979
  padding: 4px 10px;
2858
2980
  font-size: 12px;
2859
2981
  color: var(--text-secondary);
@@ -2872,6 +2994,7 @@ h1 {
2872
2994
  display: inline-flex;
2873
2995
  align-items: center;
2874
2996
  gap: 4px;
2997
+ height: 28px;
2875
2998
  padding: 4px 10px;
2876
2999
  font-size: 12px;
2877
3000
  color: var(--text-secondary);
@@ -2885,22 +3008,89 @@ h1 {
2885
3008
  border-color: var(--border-strong);
2886
3009
  }
2887
3010
  .locale-trigger svg { width: 14px; height: 14px; flex-shrink: 0; }
3011
+ /* One compact menu surface; rows are choices, not nested pill buttons. */
2888
3012
  .locale-menu {
2889
- display: none;
2890
- flex-direction: column;
2891
- align-items: stretch;
2892
- gap: 4px;
2893
- margin-top: 4px;
3013
+ display: none; position: absolute; top: calc(100% + 6px); right: 0; z-index: 4;
3014
+ min-width: 144px; width: max-content; max-width: min(240px,calc(100vw - 64px));
3015
+ padding: 4px; background: var(--bg-card); border: 1px solid var(--border);
3016
+ border-radius: 10px; box-shadow: var(--shadow-lg);
2894
3017
  }
2895
- .locale-switcher.open .locale-menu {
2896
- display: flex;
2897
- }
2898
- .locale-btn[aria-checked="true"] {
2899
- color: var(--text);
2900
- border-color: var(--border-strong);
3018
+ .locale-switcher.open .locale-menu { display: flex; flex-direction: column; }
3019
+ .locale-menu .locale-btn {
3020
+ width: 100%; min-height: 36px; height: auto; padding: 7px 10px; gap: 8px;
3021
+ border: 0; border-radius: 6px; background: transparent; text-align: left;
3022
+ font-size: 13px; line-height: 1.4; overflow-wrap: anywhere;
2901
3023
  }
2902
- .locale-check { font-size: 11px; line-height: 1; }
3024
+ .locale-menu .locale-btn:hover, .locale-menu .locale-btn:focus-visible { background: var(--bg-hover); color: var(--text); }
3025
+ .locale-menu .locale-btn[aria-checked="true"] { color: var(--text); font-weight: 600; }
3026
+ .locale-check { width: 14px; flex: 0 0 14px; font-size: 12px; line-height: 1; }
3027
+ @media (pointer: coarse) { .locale-menu .locale-btn { min-height: 44px; } }
3028
+ .has-last-used { flex-wrap: wrap; }
3029
+ .last-used { display: inline-block; font-size: 11px; font-weight: 400; line-height: 1.4;
3030
+ padding: 2px 6px; border: 1px solid currentColor; border-radius: 4px; opacity: .72; white-space: nowrap; }
2903
3031
  .card { position: relative; }
3032
+
3033
+ /* Long localized method labels remain readable on narrow screens. */
3034
+ .methods-grid { grid-template-columns: 1fr; }
3035
+ .method-btn { min-height: 48px; white-space: normal; }
3036
+ .passkey-alt { min-height: 44px; font-weight: 600; }
3037
+ .oauth-recovery { font-size: 14px; line-height: 1.5; margin: 12px 0; padding: 12px;
3038
+ border: 1px solid var(--border); border-radius: var(--radius); color: var(--text-secondary); text-align: left; }
3039
+ .oauth-recovery p { margin: 0 0 8px; overflow-wrap: anywhere; }
3040
+ .oauth-recovery-actions { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: center; }
3041
+ .oauth-recovery-actions button, .oauth-recovery-actions a { font: inherit; font-size: 13px; min-height: 44px;
3042
+ display: inline-flex; align-items: center; padding: 4px 0; color: var(--text); border: 0; background: none; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
3043
+ .oauth-recovery-actions[hidden], .oauth-recovery-actions [hidden] { display: none; }
3044
+ .oauth-recovery-actions :focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
3045
+
3046
+ @media (max-width: 480px) { .card { padding: 24px 20px; width: 100%; box-sizing: border-box; } h1 { font-size: 24px; } }
3047
+
3048
+ .passkey-create-entry { display: flex; justify-content: center; align-items: center; width: 100%; box-sizing: border-box; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-surface); color: var(--text); padding: 10px 14px; }
3049
+ .passkey-create-entry:hover { background: var(--bg-hover); text-decoration: none; }
3050
+ .status:empty { min-height: 0; padding: 0; margin: 0; }
3051
+ .passkey-section .subtitle { margin: 8px 0 16px; font-size: 14px; line-height: 1.5; }
3052
+ @media (max-width: 480px) { .card { padding: 20px 18px; } .brand-header { margin-bottom: 20px; } }
3053
+
3054
+ .card-modal .card-tools { left: 20px; right: 80px; top: 12px; }
3055
+ .card-modal .locale-switcher { min-width: 0; max-width: 100%; }
3056
+ .card-modal .locale-trigger { max-width: 100%; height: 36px; }
3057
+ .locale-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3058
+ .card-modal .locale-menu { left: 0; right: auto; }
3059
+
3060
+ .card { padding: 52px 24px 24px; box-sizing: border-box; }
3061
+ .card-modal { width: 100%; }
3062
+ .card-modal { padding-top: 68px; border: 0; border-radius: 0; }
3063
+ /* The dialog frame owns the shadow in both palettes. */
3064
+ .card.card-modal, :host([data-theme="light"]) .card.card-modal { box-shadow: none; }
3065
+ /* A translateY entrance expands an auto scroller by 4px until it finishes. */
3066
+ .card-modal[data-view] { animation: none; }
3067
+ /* Sub-step navigation shares the close button's center line in both modes. */
3068
+ .back-btn { top: 8px; left: 24px; height: 44px; max-width: calc(100% - 160px); line-height: 1.4; text-align: left; }
3069
+ .back-btn:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; border-radius: 4px; }
3070
+ .card-modal .back-btn { position: absolute; top: 8px; left: 24px; margin: 0; }
3071
+ .card-modal:has(.back-btn) .card-tools { left: auto; right: 64px; max-width: calc(100% - 160px); }
3072
+ .passkey-creating > .home-link { display: none; }
3073
+ @media (max-width:480px) { .back-btn, .card-modal .back-btn { left: 18px; } }
3074
+ .card-modal h1 { display: block; overflow: visible; -webkit-line-clamp: unset; line-clamp: unset; line-height: 1.4; padding-block: 2px; }
3075
+ .card-modal h1[tabindex="-1"]:focus { outline: none; }
3076
+ .card-modal h1, .card-modal h2, .card-modal .subtitle, .card-modal .status { overflow-wrap: anywhere; }
3077
+ .card-modal .btn { height: auto; min-height: 44px; line-height: 1.4; padding-block: 10px; white-space: normal; }
3078
+ .card-modal button, .card-modal a { overflow-wrap: anywhere; }
3079
+ .social-first .divider { margin: 14px 0; }
3080
+ .social-first .app-logo { width: 32px; height: 32px; margin-bottom: 12px; }
3081
+ .social-first .login-footer { margin-top: 16px; padding-top: 12px; }
3082
+ .other-methods { margin-top: 8px; color: var(--text-secondary); font-size: 14px; }
3083
+ .other-methods summary { min-height: 44px; padding: 12px 0; cursor: pointer; }
3084
+ .other-methods summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
3085
+ @media (max-width:480px) { .card { padding: 52px 18px 24px; } }
3086
+ @media (max-width:480px) { .card-modal { padding-top: 68px; } .card-modal .card-tools { left: 18px; } }
3087
+ @media (prefers-reduced-motion: reduce) { .card[data-view], .qr-view { animation: none; } }
3088
+
3089
+ .passkey-creating > .methods-grid, .passkey-creating > .divider, .passkey-creating > .other-methods { display: none; }
3090
+
3091
+ .input-label { display: block; text-align: left; margin: 8px 0; color: var(--text); font-size: 14px; font-weight: 500; }
3092
+ .input-label[hidden], .email-help[hidden] { display: none; }
3093
+ .email-help { text-align: left; font-size: 13px; line-height: 1.5; color: var(--text-secondary); margin: 0 0 12px; }
2904
3094
  `;
2905
3095
 
2906
3096
  // dist/ui/webauthn-browser.js
@@ -2962,7 +3152,7 @@ async function runPasskeyAuth(options) {
2962
3152
  const txtCreating = texts.creating || t("passkeyCreating", locale);
2963
3153
  const txtVerifying = texts.verifying || t("verifying", locale);
2964
3154
  async function fetchAuthChallenge() {
2965
- const res = await fetch(`${prefix}/auth`, { method: "GET" });
3155
+ const res = await fetch(`${prefix}/auth`, { method: "GET", signal: options.signal });
2966
3156
  if (!res.ok)
2967
3157
  throw new Error(t("authFailed", locale));
2968
3158
  return res.json();
@@ -2974,7 +3164,7 @@ async function runPasskeyAuth(options) {
2974
3164
  if (invitationId)
2975
3165
  params.push(`invitationId=${encodeURIComponent(invitationId)}`);
2976
3166
  const qs = params.length ? `?${params.join("&")}` : "";
2977
- const res = await fetch(`${prefix}/register${qs}`, { method: "GET" });
3167
+ const res = await fetch(`${prefix}/register${qs}`, { method: "GET", signal: options.signal });
2978
3168
  if (!res.ok)
2979
3169
  throw new Error(t("registrationFailed", locale));
2980
3170
  return res.json();
@@ -2988,6 +3178,7 @@ async function runPasskeyAuth(options) {
2988
3178
  async function verifyAuth(challengeId, credential) {
2989
3179
  const res = await fetch(`${prefix}/auth`, {
2990
3180
  method: "POST",
3181
+ signal: options.signal,
2991
3182
  headers: { "Content-Type": "application/json" },
2992
3183
  body: JSON.stringify({ challengeId, credential })
2993
3184
  });
@@ -3000,6 +3191,7 @@ async function runPasskeyAuth(options) {
3000
3191
  async function verifyRegister(challengeId, credential, name) {
3001
3192
  const res = await fetch(`${prefix}/register`, {
3002
3193
  method: "POST",
3194
+ signal: options.signal,
3003
3195
  headers: { "Content-Type": "application/json" },
3004
3196
  body: JSON.stringify({ challengeId, credential, name: name || void 0 })
3005
3197
  });
@@ -3016,6 +3208,7 @@ async function runPasskeyAuth(options) {
3016
3208
  const { challengeId } = challengeResult;
3017
3209
  const authentication = challengeResult.authentication || challengeResult;
3018
3210
  const authOpts = {
3211
+ signal: options.signal,
3019
3212
  publicKey: {
3020
3213
  ...authentication,
3021
3214
  challenge: b64urlDecode(authentication.challenge),
@@ -3033,6 +3226,10 @@ async function runPasskeyAuth(options) {
3033
3226
  return { success: false, error: "no_passkey" };
3034
3227
  }
3035
3228
  }
3229
+ if (!raw && authOnly)
3230
+ return { success: false, error: "no_passkey" };
3231
+ if (options.signal?.aborted)
3232
+ return { success: false, error: "cancelled" };
3036
3233
  if (raw) {
3037
3234
  const credential = serializeAuthentication(raw);
3038
3235
  onStatus(txtVerifying, false);
@@ -3046,15 +3243,16 @@ async function runPasskeyAuth(options) {
3046
3243
  return { success: false, error: errMsg, code: errCode };
3047
3244
  }
3048
3245
  }
3049
- const challengeData = await fetchRegisterChallenge(options.name || "");
3050
- if (challengeData.registrationAllowed === false) {
3051
- onStatus(t("regClosed", locale), true);
3052
- return { success: false, error: "registration_closed" };
3053
- }
3054
- onStatus(txtCreating, false);
3055
3246
  try {
3247
+ const challengeData = await fetchRegisterChallenge(options.name || "");
3248
+ if (challengeData.registrationAllowed === false) {
3249
+ onStatus(t("regClosed", locale), true);
3250
+ return { success: false, error: "registration_closed" };
3251
+ }
3252
+ onStatus(txtCreating, false);
3056
3253
  const registration = challengeData.registration || challengeData;
3057
3254
  const regOpts = {
3255
+ signal: options.signal,
3058
3256
  publicKey: {
3059
3257
  ...registration,
3060
3258
  challenge: b64urlDecode(registration.challenge),
@@ -3069,12 +3267,14 @@ async function runPasskeyAuth(options) {
3069
3267
  }
3070
3268
  };
3071
3269
  const raw = await navigator.credentials.create(regOpts);
3270
+ if (!raw || options.signal?.aborted)
3271
+ return { success: false, error: t("passkeyNoLocal", locale) };
3072
3272
  const credential = serializeRegistration(raw);
3073
3273
  onStatus(txtVerifying, false);
3074
3274
  await verifyRegister(challengeData.challengeId, credential, options.name || "");
3075
3275
  return { success: true };
3076
3276
  } catch (regErr) {
3077
- const errMsg = regErr instanceof Error ? regErr.message : t("passkeySetupFailed", locale);
3277
+ const errMsg = regErr instanceof Error && (regErr.name === "NotAllowedError" || regErr.name === "AbortError") ? t("passkeyNoLocal", locale) : regErr instanceof Error ? regErr.message : t("passkeySetupFailed", locale);
3078
3278
  const errCode = regErr instanceof Error ? regErr.code : void 0;
3079
3279
  onStatus(errMsg, true);
3080
3280
  return { success: false, error: errMsg, code: errCode };
@@ -3113,15 +3313,42 @@ var LoginPage = class {
3113
3313
  tokenSession = null;
3114
3314
  styleEl = null;
3115
3315
  mounted = false;
3316
+ completed = false;
3317
+ generation = 0;
3318
+ activeMethod;
3319
+ activeProvider;
3320
+ oauthCleanup;
3321
+ emailTimer;
3322
+ emailDraft = "";
3323
+ abort;
3324
+ emit(name) {
3325
+ try {
3326
+ this.options.onEvent?.({ name, method: this.activeMethod, mode: this.options.mode ?? "page" });
3327
+ } catch {
3328
+ }
3329
+ }
3330
+ select(method) {
3331
+ this.generation++;
3332
+ this.oauthCleanup?.();
3333
+ this.abort?.abort();
3334
+ this.abort = new AbortController();
3335
+ this.activeMethod = method;
3336
+ this.activeProvider = void 0;
3337
+ this.emit("method_selected");
3338
+ }
3116
3339
  currentView = "methods";
3117
3340
  cardEl = null;
3118
3341
  mediaQuery = null;
3119
3342
  mediaListener = null;
3120
3343
  localeOutsideClick = null;
3344
+ /** Open shadow root when isolate is set. Reused across destroy/mount. */
3345
+ shadow = null;
3346
+ isolate;
3121
3347
  constructor(container, config, http, options) {
3122
3348
  this.container = container;
3123
3349
  this.config = config;
3124
3350
  this.http = http;
3351
+ this.isolate = options?.isolate === true;
3125
3352
  this.options = {
3126
3353
  methods: ["passkey", "did-connect"],
3127
3354
  locale: "en",
@@ -3142,32 +3369,50 @@ var LoginPage = class {
3142
3369
  if (this.mounted)
3143
3370
  return;
3144
3371
  this.mounted = true;
3372
+ this.completed = false;
3373
+ this.emit("view");
3145
3374
  this.styleEl = document.createElement("style");
3146
3375
  this.styleEl.textContent = LOGIN_PAGE_STYLES;
3147
- this.container.appendChild(this.styleEl);
3376
+ this.mountRoot().appendChild(this.styleEl);
3377
+ this.adoptStoredTheme();
3148
3378
  this.applyTheme();
3149
3379
  const available = this.filterMethods(this.options.methods);
3150
3380
  this.renderCard(available);
3151
3381
  }
3152
3382
  /** Destroy the login UI, clean up timers and listeners. Idempotent. */
3153
3383
  destroy() {
3384
+ this.generation++;
3385
+ this.oauthCleanup?.();
3386
+ this.abort?.abort();
3387
+ clearInterval(this.emailTimer);
3154
3388
  this.detachLocaleMenu();
3155
3389
  if (this.tokenSession) {
3156
3390
  this.tokenSession.destroy();
3157
3391
  this.tokenSession = null;
3158
3392
  }
3159
- if (this.mediaQuery && this.mediaListener) {
3160
- this.mediaQuery.removeEventListener("change", this.mediaListener);
3161
- this.mediaQuery = null;
3162
- this.mediaListener = null;
3393
+ this.detachMedia();
3394
+ if (this.isolate) {
3395
+ const shadow = this.shadow ?? this.container.shadowRoot;
3396
+ if (shadow)
3397
+ shadow.innerHTML = "";
3398
+ this.container.removeAttribute("data-theme");
3399
+ } else {
3400
+ document.documentElement.removeAttribute("data-theme");
3401
+ this.container.innerHTML = "";
3163
3402
  }
3164
- document.documentElement.removeAttribute("data-theme");
3165
- this.container.innerHTML = "";
3166
3403
  this.styleEl = null;
3167
3404
  this.cardEl = null;
3168
3405
  this.currentView = "methods";
3169
3406
  this.mounted = false;
3170
3407
  }
3408
+ /**
3409
+ * Apply a light or dark theme without remounting.
3410
+ * The host writes `web-mode` itself. Isolate updates the shadow host only.
3411
+ */
3412
+ setTheme(theme) {
3413
+ this.options.theme = theme;
3414
+ this.applyTheme();
3415
+ }
3171
3416
  /** Update options and re-render. */
3172
3417
  update(newOptions) {
3173
3418
  Object.assign(this.options, newOptions);
@@ -3177,24 +3422,82 @@ var LoginPage = class {
3177
3422
  }
3178
3423
  }
3179
3424
  // ─── Private: theme ────────────────────────────────────────────────
3425
+ /** Light DOM themes <html>. Isolate themes the shadow host only. */
3426
+ themeTarget() {
3427
+ return this.isolate ? this.container : document.documentElement;
3428
+ }
3429
+ /** Shadow root created once. attachShadow throws if called again. */
3430
+ mountRoot() {
3431
+ if (!this.isolate)
3432
+ return this.container;
3433
+ if (this.shadow)
3434
+ return this.shadow;
3435
+ this.shadow = this.container.shadowRoot ?? this.container.attachShadow({ mode: "open" });
3436
+ return this.shadow;
3437
+ }
3438
+ detachMedia() {
3439
+ if (this.mediaQuery && this.mediaListener) {
3440
+ this.mediaQuery.removeEventListener("change", this.mediaListener);
3441
+ }
3442
+ this.mediaQuery = null;
3443
+ this.mediaListener = null;
3444
+ }
3445
+ /**
3446
+ * Same order as theme/init.js: `?theme=` is a preview and wins;
3447
+ * otherwise `localStorage["web-mode"]`. Neither write happens here.
3448
+ */
3449
+ adoptStoredTheme() {
3450
+ if (this.options.mode === "dialog")
3451
+ return;
3452
+ if (typeof window === "undefined")
3453
+ return;
3454
+ const urlTheme = new URLSearchParams(window.location.search).get("theme");
3455
+ if (urlTheme === "light" || urlTheme === "dark" || urlTheme === "auto") {
3456
+ this.options.theme = urlTheme;
3457
+ return;
3458
+ }
3459
+ try {
3460
+ const stored = localStorage.getItem("web-mode");
3461
+ if (stored === "light" || stored === "dark" || stored === "auto")
3462
+ this.options.theme = stored;
3463
+ } catch {
3464
+ }
3465
+ }
3466
+ /** light sets data-theme. dark removes it. auto follows the OS. */
3180
3467
  applyTheme() {
3468
+ this.detachMedia();
3181
3469
  const theme = this.options.theme ?? "dark";
3470
+ const target = this.themeTarget();
3182
3471
  if (theme === "light") {
3183
- document.documentElement.dataset.theme = "light";
3472
+ target.dataset.theme = "light";
3184
3473
  } else if (theme === "auto" && typeof window.matchMedia === "function") {
3185
3474
  const mq = window.matchMedia("(prefers-color-scheme: light)");
3186
3475
  this.mediaQuery = mq;
3187
3476
  const apply = (light) => {
3188
- if (light) {
3189
- document.documentElement.dataset.theme = "light";
3190
- } else {
3191
- document.documentElement.removeAttribute("data-theme");
3192
- }
3477
+ if (light)
3478
+ target.dataset.theme = "light";
3479
+ else
3480
+ target.removeAttribute("data-theme");
3481
+ this.repaintQRPlate();
3193
3482
  };
3194
3483
  apply(mq.matches);
3195
3484
  this.mediaListener = (e) => apply(e.matches);
3196
3485
  mq.addEventListener("change", this.mediaListener);
3486
+ return;
3487
+ } else {
3488
+ target.removeAttribute("data-theme");
3197
3489
  }
3490
+ this.repaintQRPlate();
3491
+ }
3492
+ /** Redraw a visible QR so its plate matches the theme. Leaves a timeout overlay alone. */
3493
+ repaintQRPlate() {
3494
+ const qr = this.mountRoot().querySelector("#did-connect-qr");
3495
+ if (!(qr instanceof HTMLElement))
3496
+ return;
3497
+ const url = qr.dataset.qrUrl;
3498
+ if (!url || !qr.querySelector("canvas") || qr.querySelector(".qr-timeout-overlay"))
3499
+ return;
3500
+ renderQR(qr, url);
3198
3501
  }
3199
3502
  // ─── Private: rendering ──────────────────────────────────────────────
3200
3503
  filterMethods(methods) {
@@ -3207,11 +3510,20 @@ var LoginPage = class {
3207
3510
  }
3208
3511
  renderCard(methods) {
3209
3512
  const card = document.createElement("div");
3210
- card.className = "card";
3513
+ card.className = this.options.mode === "dialog" ? "card card-modal" : "card";
3211
3514
  this.cardEl = card;
3212
- this.container.appendChild(card);
3515
+ this.mountRoot().appendChild(card);
3213
3516
  this.switchView("methods", methods);
3214
3517
  }
3518
+ /** Locale sits on the card's top-right row, level with Back / Cancel. */
3519
+ renderCardTools(card) {
3520
+ if (!(this.options.showLocaleSelector ?? this.options.mode !== "dialog"))
3521
+ return;
3522
+ const tools = document.createElement("div");
3523
+ tools.className = "card-tools";
3524
+ this.renderLocaleSwitcher(tools);
3525
+ card.appendChild(tools);
3526
+ }
3215
3527
  renderLocaleSwitcher(card) {
3216
3528
  const passed = this.options.locales;
3217
3529
  if (passed) {
@@ -3250,7 +3562,7 @@ var LoginPage = class {
3250
3562
  trigger.className = "locale-trigger";
3251
3563
  trigger.setAttribute("aria-haspopup", "menu");
3252
3564
  trigger.setAttribute("aria-expanded", "false");
3253
- trigger.innerHTML = `${ICON_GLOBE} ${escapeHtml(current.label)}`;
3565
+ trigger.innerHTML = `${ICON_GLOBE} <span class="locale-label">${escapeHtml(current.label)}</span>`;
3254
3566
  trigger.title = current.label;
3255
3567
  const menu = document.createElement("div");
3256
3568
  menu.className = "locale-menu";
@@ -3264,6 +3576,8 @@ var LoginPage = class {
3264
3576
  trigger.addEventListener("click", (event) => {
3265
3577
  event.stopPropagation();
3266
3578
  setOpen(menu.hidden);
3579
+ if (!menu.hidden)
3580
+ menu.querySelector('[aria-checked="true"]')?.focus();
3267
3581
  });
3268
3582
  for (const item of locales) {
3269
3583
  const checked = item.code === current.code;
@@ -3273,12 +3587,13 @@ var LoginPage = class {
3273
3587
  btn.setAttribute("role", "menuitemradio");
3274
3588
  btn.setAttribute("aria-checked", checked ? "true" : "false");
3275
3589
  btn.dataset.locale = item.code;
3590
+ btn.tabIndex = -1;
3276
3591
  btn.title = item.label;
3277
- if (checked) {
3592
+ {
3278
3593
  const mark = document.createElement("span");
3279
3594
  mark.className = "locale-check";
3280
3595
  mark.setAttribute("aria-hidden", "true");
3281
- mark.textContent = "\u2713";
3596
+ mark.textContent = checked ? "\u2713" : "";
3282
3597
  btn.appendChild(mark);
3283
3598
  }
3284
3599
  const label = document.createElement("span");
@@ -3290,9 +3605,31 @@ var LoginPage = class {
3290
3605
  });
3291
3606
  menu.appendChild(btn);
3292
3607
  }
3293
- const onOutsideClick = (event) => {
3294
- if (!wrapper.contains(event.target))
3608
+ wrapper.addEventListener("keydown", (event) => {
3609
+ const items = [...menu.querySelectorAll(".locale-btn")];
3610
+ const active = this.shadow?.activeElement ?? document.activeElement;
3611
+ const index = items.indexOf(active);
3612
+ if (event.key === "Escape") {
3613
+ event.preventDefault();
3614
+ event.stopPropagation();
3615
+ setOpen(false);
3616
+ trigger.focus();
3617
+ } else if (event.key === "Tab")
3295
3618
  setOpen(false);
3619
+ else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
3620
+ event.preventDefault();
3621
+ setOpen(true);
3622
+ const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : index < 0 ? event.key === "ArrowUp" ? items.length - 1 : 0 : (index + (event.key === "ArrowUp" ? -1 : 1) + items.length) % items.length;
3623
+ items[next]?.focus();
3624
+ }
3625
+ });
3626
+ const onOutsideClick = (event) => {
3627
+ const path = event.composedPath();
3628
+ if (path.includes(wrapper))
3629
+ return;
3630
+ if (this.isolate && path.includes(this.container))
3631
+ return;
3632
+ setOpen(false);
3296
3633
  };
3297
3634
  document.addEventListener("click", onOutsideClick);
3298
3635
  this.localeOutsideClick = onOutsideClick;
@@ -3316,10 +3653,14 @@ var LoginPage = class {
3316
3653
  this.tokenSession = null;
3317
3654
  }
3318
3655
  }
3656
+ this.generation++;
3657
+ this.oauthCleanup?.();
3658
+ this.abort?.abort();
3659
+ clearInterval(this.emailTimer);
3319
3660
  this.currentView = view;
3320
3661
  this.cardEl.innerHTML = "";
3321
3662
  this.cardEl.dataset.view = view;
3322
- this.renderLocaleSwitcher(this.cardEl);
3663
+ this.renderCardTools(this.cardEl);
3323
3664
  switch (view) {
3324
3665
  case "methods":
3325
3666
  this.renderMethodsView(this.cardEl, locale, methods ?? this.filterMethods(this.options.methods));
@@ -3332,8 +3673,24 @@ var LoginPage = class {
3332
3673
  break;
3333
3674
  }
3334
3675
  }
3676
+ /** Keep recognition next to the action, without a second page subtitle. */
3677
+ markLastUsed(button, method, locale) {
3678
+ if (!this.options.preferenceKey)
3679
+ return;
3680
+ try {
3681
+ if (localStorage.getItem(`did-login-method:${this.options.preferenceKey}`) !== method)
3682
+ return;
3683
+ const hint = document.createElement("span");
3684
+ hint.className = "last-used";
3685
+ hint.textContent = t("lastUsed", locale);
3686
+ button.classList.add("has-last-used");
3687
+ button.appendChild(hint);
3688
+ } catch {
3689
+ }
3690
+ }
3335
3691
  renderMethodsView(card, locale, methods) {
3336
- this.renderHomeLink(card, locale);
3692
+ if (this.options.mode !== "dialog")
3693
+ this.renderHomeLink(card, locale);
3337
3694
  this.renderBrandHeader(card, locale);
3338
3695
  const requestedPasskey = this.options.methods.includes("passkey");
3339
3696
  if (requestedPasskey && !methods.includes("passkey")) {
@@ -3342,6 +3699,12 @@ var LoginPage = class {
3342
3699
  warn.textContent = t("passkeyUnsupported", locale);
3343
3700
  card.appendChild(warn);
3344
3701
  }
3702
+ const socialFirst = this.options.preferredLayout !== "passkey-first" && methods.includes("oauth") && this.options.oauthProviders?.some((p) => p.enabled && ["google", "apple"].includes(p.id));
3703
+ card.classList.toggle("social-first", !!socialFirst);
3704
+ if (socialFirst) {
3705
+ this.renderSecondaryGrid(card, locale, ["oauth"], "primary");
3706
+ this.renderDivider(card, locale);
3707
+ }
3345
3708
  const hasPrimary = methods.includes("passkey");
3346
3709
  if (hasPrimary) {
3347
3710
  this.renderPasskeySection(card, locale);
@@ -3351,7 +3714,20 @@ var LoginPage = class {
3351
3714
  this.renderDivider(card, locale);
3352
3715
  }
3353
3716
  if (secondary.length > 0) {
3354
- this.renderSecondaryGrid(card, locale, methods);
3717
+ if (socialFirst) {
3718
+ if (methods.includes("email"))
3719
+ this.renderSecondaryGrid(card, locale, ["email"]);
3720
+ const details = document.createElement("details");
3721
+ details.className = "other-methods";
3722
+ const summary = document.createElement("summary");
3723
+ summary.textContent = t("otherMethods", locale);
3724
+ details.appendChild(summary);
3725
+ this.renderSecondaryGrid(details, locale, methods.filter((m) => m !== "email"), "secondary");
3726
+ if (details.querySelector("button"))
3727
+ card.appendChild(details);
3728
+ } else {
3729
+ this.renderSecondaryGrid(card, locale, methods);
3730
+ }
3355
3731
  }
3356
3732
  if (methods.length === 0 && !requestedPasskey) {
3357
3733
  const msg = document.createElement("p");
@@ -3378,10 +3754,14 @@ var LoginPage = class {
3378
3754
  walletBtn.id = "did-wallet-open-btn";
3379
3755
  walletBtn.textContent = t("openInWallet", locale);
3380
3756
  walletBtn.disabled = true;
3381
- card.appendChild(walletBtn);
3757
+ if (isMobile())
3758
+ card.insertBefore(walletBtn, qrContainer);
3759
+ else
3760
+ card.appendChild(walletBtn);
3382
3761
  const statusEl = document.createElement("div");
3383
3762
  statusEl.id = "did-connect-status";
3384
3763
  statusEl.className = "status";
3764
+ statusEl.setAttribute("role", "status");
3385
3765
  card.appendChild(statusEl);
3386
3766
  this.renderTrustFooter(card, locale);
3387
3767
  this.startDIDConnectInView(statusEl, qrContainer, walletBtn, locale);
@@ -3393,9 +3773,26 @@ var LoginPage = class {
3393
3773
  title.className = "view-title";
3394
3774
  title.textContent = t("emailTitle", locale);
3395
3775
  card.appendChild(title);
3776
+ const emailLabel = document.createElement("label");
3777
+ emailLabel.className = "input-label";
3778
+ emailLabel.htmlFor = "login-email-address";
3779
+ emailLabel.textContent = t("emailPlaceholder", locale);
3780
+ card.appendChild(emailLabel);
3781
+ const emailHelp = document.createElement("p");
3782
+ emailHelp.className = "email-help";
3783
+ emailHelp.textContent = t("emailExplanation", locale);
3784
+ card.appendChild(emailHelp);
3396
3785
  const emailInput = document.createElement("input");
3397
3786
  emailInput.className = "input";
3787
+ emailInput.id = "login-email-address";
3398
3788
  emailInput.type = "email";
3789
+ emailInput.required = true;
3790
+ emailInput.autocomplete = "email";
3791
+ emailInput.value = this.emailDraft;
3792
+ emailInput.setAttribute("aria-label", t("emailPlaceholder", locale));
3793
+ emailInput.addEventListener("input", () => {
3794
+ this.emailDraft = emailInput.value;
3795
+ });
3399
3796
  emailInput.placeholder = t("emailPlaceholder", locale);
3400
3797
  card.appendChild(emailInput);
3401
3798
  const sendBtn = document.createElement("button");
@@ -3404,15 +3801,23 @@ var LoginPage = class {
3404
3801
  card.appendChild(sendBtn);
3405
3802
  const status = document.createElement("div");
3406
3803
  status.className = "status";
3804
+ status.setAttribute("role", "status");
3407
3805
  card.appendChild(status);
3408
3806
  const codeSection = document.createElement("div");
3409
3807
  codeSection.className = "email-code-section";
3410
3808
  codeSection.style.display = "none";
3809
+ const codeLabel = document.createElement("label");
3810
+ codeLabel.className = "input-label";
3811
+ codeLabel.htmlFor = "login-email-code";
3812
+ codeLabel.textContent = t("emailCodePlaceholder", locale);
3813
+ codeSection.appendChild(codeLabel);
3411
3814
  const codeInput = document.createElement("input");
3815
+ codeInput.id = "login-email-code";
3412
3816
  codeInput.className = "input";
3413
3817
  codeInput.type = "text";
3414
3818
  codeInput.inputMode = "numeric";
3415
3819
  codeInput.maxLength = 8;
3820
+ codeInput.setAttribute("aria-label", t("emailCodePlaceholder", locale));
3416
3821
  codeInput.autocomplete = "one-time-code";
3417
3822
  codeInput.placeholder = t("emailCodePlaceholder", locale);
3418
3823
  codeSection.appendChild(codeInput);
@@ -3427,6 +3832,27 @@ var LoginPage = class {
3427
3832
  codeSection.appendChild(resendBtn);
3428
3833
  card.insertBefore(codeSection, status);
3429
3834
  this.renderTrustFooter(card, locale);
3835
+ const epoch = this.generation;
3836
+ const isCurrent = () => this.mounted && epoch === this.generation;
3837
+ const editBtn = document.createElement("button");
3838
+ editBtn.className = "passkey-alt";
3839
+ editBtn.textContent = t("emailEdit", locale);
3840
+ codeSection.appendChild(editBtn);
3841
+ editBtn.addEventListener("click", () => this.switchView("email"));
3842
+ const cooldown = () => {
3843
+ let remaining = 60;
3844
+ clearInterval(this.emailTimer);
3845
+ resendBtn.disabled = true;
3846
+ resendBtn.textContent = t("emailCooldown", locale, { seconds: String(remaining) });
3847
+ this.emailTimer = setInterval(() => {
3848
+ remaining--;
3849
+ resendBtn.textContent = remaining > 0 ? t("emailCooldown", locale, { seconds: String(remaining) }) : t("emailResend", locale);
3850
+ if (remaining <= 0) {
3851
+ clearInterval(this.emailTimer);
3852
+ resendBtn.disabled = false;
3853
+ }
3854
+ }, 1e3);
3855
+ };
3430
3856
  let currentEmail = "";
3431
3857
  let currentCodeId;
3432
3858
  const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
@@ -3434,7 +3860,7 @@ var LoginPage = class {
3434
3860
  if (sendBtn.disabled)
3435
3861
  return;
3436
3862
  const email = emailInput.value.trim();
3437
- if (!email?.includes("@")) {
3863
+ if (!emailInput.checkValidity()) {
3438
3864
  status.textContent = t("emailInvalid", locale);
3439
3865
  status.className = "status error";
3440
3866
  return;
@@ -3442,11 +3868,17 @@ var LoginPage = class {
3442
3868
  sendBtn.disabled = true;
3443
3869
  status.textContent = t("emailSending", locale);
3444
3870
  status.className = "status";
3871
+ status.setAttribute("role", "status");
3445
3872
  try {
3446
3873
  const sendResult = await this.http.post(`${servicePrefix}/api/email/sendCode`, { email, locale });
3874
+ if (!isCurrent())
3875
+ return;
3876
+ cooldown();
3447
3877
  currentEmail = email;
3448
3878
  currentCodeId = sendResult?.id;
3449
3879
  emailInput.style.display = "none";
3880
+ emailLabel.hidden = true;
3881
+ emailHelp.hidden = true;
3450
3882
  sendBtn.style.display = "none";
3451
3883
  codeSection.style.display = "";
3452
3884
  title.textContent = t("emailVerify", locale);
@@ -3454,6 +3886,8 @@ var LoginPage = class {
3454
3886
  status.className = "status";
3455
3887
  codeInput.focus();
3456
3888
  } catch (err) {
3889
+ if (!isCurrent())
3890
+ return;
3457
3891
  const msg = err instanceof Error ? err.message : "Failed to send email";
3458
3892
  status.textContent = msg;
3459
3893
  status.className = "status error";
@@ -3464,7 +3898,7 @@ var LoginPage = class {
3464
3898
  if (verifyBtn.disabled)
3465
3899
  return;
3466
3900
  const code = codeInput.value.trim();
3467
- if (!code || code.length !== 8) {
3901
+ if (!/^\d{8}$/.test(code)) {
3468
3902
  status.textContent = t("emailCodeInvalid", locale);
3469
3903
  status.className = "status error";
3470
3904
  return;
@@ -3474,10 +3908,14 @@ var LoginPage = class {
3474
3908
  status.className = "status";
3475
3909
  try {
3476
3910
  await this.http.post(`${servicePrefix}/api/email/login`, { code, id: currentCodeId });
3911
+ if (!isCurrent())
3912
+ return;
3477
3913
  status.textContent = t("success", locale);
3478
3914
  status.className = "status";
3479
3915
  this.handleSuccess();
3480
3916
  } catch (err) {
3917
+ if (!isCurrent())
3918
+ return;
3481
3919
  if (err instanceof Error && "code" in err && err.code === "FORBIDDEN") {
3482
3920
  this.showFullError(friendlyError(err, locale));
3483
3921
  } else {
@@ -3500,27 +3938,35 @@ var LoginPage = class {
3500
3938
  verifyCode();
3501
3939
  });
3502
3940
  resendBtn.addEventListener("click", async () => {
3941
+ if (resendBtn.disabled)
3942
+ return;
3503
3943
  resendBtn.disabled = true;
3504
3944
  status.textContent = t("emailSending", locale);
3505
3945
  status.className = "status";
3506
3946
  try {
3507
3947
  const resendResult = await this.http.post(`${servicePrefix}/api/email/sendCode`, { email: currentEmail, locale });
3948
+ if (!isCurrent())
3949
+ return;
3950
+ cooldown();
3508
3951
  currentCodeId = resendResult?.id;
3509
3952
  status.textContent = t("emailSent", locale, { email: currentEmail });
3510
3953
  status.className = "status";
3511
3954
  } catch (err) {
3955
+ if (!isCurrent())
3956
+ return;
3957
+ resendBtn.disabled = false;
3512
3958
  const msg = err instanceof Error ? err.message : "Failed to resend";
3513
3959
  status.textContent = msg;
3514
3960
  status.className = "status error";
3515
3961
  }
3516
- resendBtn.disabled = false;
3517
3962
  });
3518
3963
  }
3519
- renderBackButton(parent, locale) {
3964
+ renderBackButton(parent, locale, onBack = () => this.switchView("methods")) {
3520
3965
  const btn = document.createElement("button");
3966
+ btn.type = "button";
3521
3967
  btn.className = "back-btn";
3522
3968
  btn.innerHTML = `${ICON_BACK_ARROW} ${escapeHtml(t("back", locale))}`;
3523
- btn.addEventListener("click", () => this.switchView("methods"));
3969
+ btn.addEventListener("click", onBack);
3524
3970
  parent.appendChild(btn);
3525
3971
  }
3526
3972
  /**
@@ -3535,6 +3981,8 @@ var LoginPage = class {
3535
3981
  * sub-view back buttons that mean "return to the method list".
3536
3982
  */
3537
3983
  renderHomeLink(parent, locale) {
3984
+ if (this.options.dismissible === false)
3985
+ return;
3538
3986
  const { homeUrl, onCancel } = this.options;
3539
3987
  if (!onCancel && !homeUrl)
3540
3988
  return;
@@ -3544,7 +3992,10 @@ var LoginPage = class {
3544
3992
  btn.type = "button";
3545
3993
  btn.className = "home-link";
3546
3994
  btn.innerHTML = label;
3547
- btn.addEventListener("click", () => this.options.onCancel?.());
3995
+ btn.addEventListener("click", () => {
3996
+ this.emit("cancel");
3997
+ this.options.onCancel?.();
3998
+ });
3548
3999
  parent.appendChild(btn);
3549
4000
  } else {
3550
4001
  const link = document.createElement("a");
@@ -3572,7 +4023,16 @@ var LoginPage = class {
3572
4023
  } else {
3573
4024
  h1.textContent = t("signIn", locale);
3574
4025
  }
4026
+ if (this.options.purpose)
4027
+ h1.textContent = this.options.purpose.title;
4028
+ h1.id = "login-title";
3575
4029
  header.appendChild(h1);
4030
+ if (this.options.purpose?.description) {
4031
+ const description = document.createElement("p");
4032
+ description.className = "subtitle";
4033
+ description.textContent = this.options.purpose.description;
4034
+ header.appendChild(description);
4035
+ }
3576
4036
  if (this.options.masterAppName) {
3577
4037
  const sub = document.createElement("p");
3578
4038
  sub.className = "federated-hint";
@@ -3613,6 +4073,12 @@ var LoginPage = class {
3613
4073
  }
3614
4074
  footer.appendChild(links);
3615
4075
  }
4076
+ if (this.currentView !== "methods" && this.options.mode !== "dialog") {
4077
+ this.renderHomeLink(footer, locale);
4078
+ const cancel = footer.querySelector(".home-link");
4079
+ if (cancel)
4080
+ cancel.style.cssText = "position:static;display:inline-flex;margin-top:12px";
4081
+ }
3616
4082
  parent.appendChild(footer);
3617
4083
  }
3618
4084
  renderDivider(parent, locale) {
@@ -3673,14 +4139,24 @@ var LoginPage = class {
3673
4139
  retryBtn.addEventListener("click", () => doSignIn());
3674
4140
  const createLink = document.createElement("button");
3675
4141
  createLink.type = "button";
3676
- createLink.className = "passkey-alt";
4142
+ createLink.className = "passkey-alt passkey-create-entry";
3677
4143
  createLink.textContent = t("passkeyCreateInstead", locale);
3678
4144
  section.appendChild(createLink);
3679
4145
  createLink.addEventListener("click", () => renderCreateState());
3680
4146
  };
3681
4147
  const renderCreateState = () => {
4148
+ this.cardEl?.classList.add("passkey-creating");
4149
+ this.abort?.abort();
4150
+ this.generation++;
3682
4151
  section.innerHTML = "";
3683
4152
  clearStatus();
4153
+ const heading = this.cardEl?.querySelector("h1");
4154
+ if (heading)
4155
+ heading.textContent = t("passkeyCreateAccount", locale);
4156
+ const explanation = document.createElement("p");
4157
+ explanation.className = "subtitle";
4158
+ explanation.textContent = t("passkeyExplanation", locale);
4159
+ section.appendChild(explanation);
3684
4160
  const nameRow = document.createElement("div");
3685
4161
  nameRow.className = "name-row visible";
3686
4162
  nameRow.id = "passkey-name-row";
@@ -3688,6 +4164,7 @@ var LoginPage = class {
3688
4164
  nameInput.className = "input";
3689
4165
  nameInput.id = "passkey-name-input";
3690
4166
  nameInput.type = "text";
4167
+ nameInput.setAttribute("aria-label", t("namePlaceholder", locale));
3691
4168
  nameInput.placeholder = t("namePlaceholder", locale);
3692
4169
  nameInput.autocomplete = "name";
3693
4170
  nameRow.appendChild(nameInput);
@@ -3697,17 +4174,17 @@ var LoginPage = class {
3697
4174
  createBtn.id = "passkey-btn";
3698
4175
  createBtn.innerHTML = `${ICON_PASSKEY} ${escapeHtml(t("passkeyCreate", locale))}`;
3699
4176
  section.appendChild(createBtn);
3700
- const backLink = document.createElement("button");
3701
- backLink.type = "button";
3702
- backLink.className = "passkey-alt";
3703
- backLink.textContent = t("passkeyBackToSignIn", locale);
3704
- section.appendChild(backLink);
4177
+ parent.querySelector(".back-btn")?.remove();
4178
+ this.renderBackButton(parent, locale, () => renderSignInState());
3705
4179
  const doCreate = async () => {
3706
4180
  if (createBtn.disabled)
3707
4181
  return;
3708
4182
  createBtn.disabled = true;
4183
+ this.select("passkey");
4184
+ const epoch = this.generation;
3709
4185
  const result = await runPasskeyAuth({
3710
4186
  registerOnly: true,
4187
+ signal: this.abort?.signal,
3711
4188
  name: nameInput.value.trim(),
3712
4189
  onStatus: setStatus,
3713
4190
  texts: statusTexts,
@@ -3715,6 +4192,8 @@ var LoginPage = class {
3715
4192
  prefix: this.options.passkeyPrefix,
3716
4193
  invitationId: this.options.invitationId
3717
4194
  });
4195
+ if (!this.mounted || epoch !== this.generation || this.abort?.signal.aborted)
4196
+ return;
3718
4197
  if (handleResult(result))
3719
4198
  return;
3720
4199
  if (result.error)
@@ -3726,21 +4205,27 @@ var LoginPage = class {
3726
4205
  if (e.key === "Enter")
3727
4206
  doCreate();
3728
4207
  });
3729
- backLink.addEventListener("click", () => renderSignInState());
3730
- nameInput.focus();
4208
+ createBtn.focus();
3731
4209
  };
3732
4210
  const doSignIn = async () => {
3733
4211
  const btn = section.querySelector("#passkey-btn");
4212
+ if (btn?.disabled)
4213
+ return;
3734
4214
  if (btn)
3735
4215
  btn.disabled = true;
4216
+ this.select("passkey");
4217
+ const epoch = this.generation;
3736
4218
  const result = await runPasskeyAuth({
3737
4219
  authOnly: true,
4220
+ signal: this.abort?.signal,
3738
4221
  onStatus: setStatus,
3739
4222
  texts: statusTexts,
3740
4223
  locale,
3741
4224
  prefix: this.options.passkeyPrefix,
3742
4225
  invitationId: this.options.invitationId
3743
4226
  });
4227
+ if (!this.mounted || epoch !== this.generation || this.abort?.signal.aborted)
4228
+ return;
3744
4229
  if (handleResult(result))
3745
4230
  return;
3746
4231
  if (result.error && result.error !== "no_passkey") {
@@ -3749,20 +4234,29 @@ var LoginPage = class {
3749
4234
  btn.disabled = false;
3750
4235
  return;
3751
4236
  }
4237
+ this.emit("incomplete");
3752
4238
  renderCrossDeviceState();
3753
4239
  };
3754
4240
  const renderSignInState = () => {
4241
+ this.cardEl?.classList.remove("passkey-creating");
4242
+ parent.querySelector(".back-btn")?.remove();
4243
+ this.abort?.abort();
4244
+ this.generation++;
4245
+ const heading = this.cardEl?.querySelector("h1");
4246
+ if (heading)
4247
+ heading.textContent = this.options.purpose?.title ?? (this.options.appName ? t("signInTo", locale, { appName: this.options.appName }) : t("signIn", locale));
3755
4248
  section.innerHTML = "";
3756
4249
  clearStatus();
3757
4250
  const signInBtn = document.createElement("button");
3758
- signInBtn.className = "btn";
4251
+ signInBtn.className = this.cardEl?.classList.contains("social-first") ? "btn btn-secondary" : "btn";
3759
4252
  signInBtn.id = "passkey-btn";
3760
4253
  signInBtn.innerHTML = `${ICON_PASSKEY} ${escapeHtml(t("passkeySignIn", locale))}`;
4254
+ this.markLastUsed(signInBtn, "passkey", locale);
3761
4255
  section.appendChild(signInBtn);
3762
4256
  signInBtn.addEventListener("click", () => doSignIn());
3763
4257
  const createLink = document.createElement("button");
3764
4258
  createLink.type = "button";
3765
- createLink.className = "passkey-alt";
4259
+ createLink.className = "passkey-alt passkey-create-entry";
3766
4260
  createLink.textContent = t("passkeyFirstTime", locale);
3767
4261
  section.appendChild(createLink);
3768
4262
  createLink.addEventListener("click", () => renderCreateState());
@@ -3777,127 +4271,242 @@ var LoginPage = class {
3777
4271
  // methods now render through renderSecondaryGrid)
3778
4272
  // ─── Private: OAuth popup flow ──────────────────────────────────────
3779
4273
  startOAuthPopup(providerId, btn) {
4274
+ this.oauthCleanup?.();
4275
+ const epoch = this.generation;
4276
+ const locale = this.options.locale;
3780
4277
  const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
3781
4278
  const oauthBase = this.options.masterOAuthOrigin ? `${this.options.masterOAuthOrigin}${servicePrefix}` : servicePrefix;
3782
4279
  const oauthOrigin = new URL(oauthBase, window.location.href).origin;
3783
- const loginUrl = `${oauthBase}/api/oauth/${encodeURIComponent(providerId)}/login?returnUrl=${encodeURIComponent(window.location.href)}`;
3784
- const width = 500;
3785
- const height = 620;
3786
- const left = window.screenX + (window.innerWidth - width) / 2;
3787
- const top = window.screenY + (window.innerHeight - height) / 2;
3788
- const popup = window.open(loginUrl, "oauth-login:popup", `left=${left},top=${top},width=${width},height=${height},resizable,scrollbars=yes,status=1,popup`);
3789
- if (!popup) {
3790
- window.location.href = loginUrl;
3791
- return;
4280
+ let returnUrl = window.location.href;
4281
+ if (this.options.mode !== "dialog" && window.location.pathname === `${servicePrefix}/login`) {
4282
+ const requested = new URLSearchParams(window.location.search).get("return_to");
4283
+ try {
4284
+ const target = new URL(requested || "/", window.location.origin);
4285
+ returnUrl = target.origin === window.location.origin && !/^\/[/\\]/.test(target.pathname) ? target.href : `${window.location.origin}/`;
4286
+ } catch {
4287
+ returnUrl = `${window.location.origin}/`;
4288
+ }
3792
4289
  }
4290
+ const loginUrl = `${oauthBase}/api/oauth/${encodeURIComponent(providerId)}/login?returnUrl=${encodeURIComponent(returnUrl)}`;
4291
+ const provider = this.options.oauthProviders?.find((p) => p.id === providerId)?.name ?? providerId;
3793
4292
  const originalHTML = btn.innerHTML;
3794
- btn.innerHTML = `<span class="oauth-spinner"></span> Authorizing...`;
3795
- btn.disabled = true;
3796
- btn.classList.add("oauth-btn-loading");
3797
- const restore = () => {
4293
+ let popup = null;
4294
+ let settled = false;
4295
+ let exchanging = false;
4296
+ let probing = false;
4297
+ let pollTimer;
4298
+ let sessionTimer;
4299
+ let abandonTimer;
4300
+ const current = () => !settled && this.mounted && epoch === this.generation;
4301
+ const locked = [];
4302
+ const lockMethods = () => {
4303
+ for (const button of this.cardEl?.querySelectorAll(".method-btn, .passkey-create-entry, #passkey-btn, .locale-trigger, .locale-btn") ?? []) {
4304
+ locked.push({ button, disabled: button.disabled });
4305
+ button.disabled = true;
4306
+ }
4307
+ };
4308
+ const recovery = document.createElement("div");
4309
+ recovery.className = "oauth-recovery";
4310
+ recovery.setAttribute("role", "status");
4311
+ const hint = document.createElement("p");
4312
+ const actions = document.createElement("div");
4313
+ actions.className = "oauth-recovery-actions";
4314
+ const reopen = document.createElement("button");
4315
+ reopen.type = "button";
4316
+ reopen.className = "oauth-retry";
4317
+ reopen.textContent = t("oauthReopen", locale);
4318
+ reopen.hidden = true;
4319
+ reopen.onclick = () => {
4320
+ this.select("oauth");
4321
+ this.activeProvider = providerId;
4322
+ this.startOAuthPopup(providerId, btn);
4323
+ };
4324
+ const fullPage = document.createElement("a");
4325
+ fullPage.href = loginUrl;
4326
+ fullPage.textContent = t("oauthFullPage", locale);
4327
+ fullPage.addEventListener("click", (event) => {
4328
+ const reconcile = recovery.dataset.state === "oauthUnconfirmed";
4329
+ if (exchanging && !settled) {
4330
+ event.preventDefault();
4331
+ return;
4332
+ }
4333
+ if (reconcile)
4334
+ event.preventDefault();
4335
+ this.options.onBeforeRedirect?.();
4336
+ this.oauthCleanup?.();
4337
+ if (reconcile)
4338
+ window.location.reload();
4339
+ });
4340
+ const cancel = document.createElement("button");
4341
+ cancel.type = "button";
4342
+ cancel.className = "oauth-cancel";
4343
+ cancel.textContent = t("cancel", locale);
4344
+ actions.append(reopen, fullPage, cancel);
4345
+ recovery.append(hint, actions);
4346
+ btn.parentElement?.after(recovery);
4347
+ const restoreButton = () => {
3798
4348
  btn.innerHTML = originalHTML;
3799
4349
  btn.disabled = false;
3800
4350
  btn.classList.remove("oauth-btn-loading");
3801
4351
  };
3802
- let settled = false;
3803
- let sessionPollTimer;
4352
+ const closePopup = () => {
4353
+ try {
4354
+ popup?.close();
4355
+ } catch {
4356
+ }
4357
+ };
4358
+ const cleanup = () => {
4359
+ settled = true;
4360
+ clearInterval(pollTimer);
4361
+ clearInterval(sessionTimer);
4362
+ clearTimeout(abandonTimer);
4363
+ window.removeEventListener("message", onMessage);
4364
+ for (const item of locked.splice(0))
4365
+ item.button.disabled = item.disabled;
4366
+ restoreButton();
4367
+ };
4368
+ const showRecovery = (key) => {
4369
+ hint.textContent = t(key, locale, { provider });
4370
+ recovery.dataset.state = key;
4371
+ actions.hidden = false;
4372
+ reopen.hidden = false;
4373
+ cancel.hidden = true;
4374
+ restoreButton();
4375
+ };
4376
+ const fail = (key) => {
4377
+ if (!current())
4378
+ return;
4379
+ cleanup();
4380
+ closePopup();
4381
+ showRecovery(key);
4382
+ this.emit("incomplete");
4383
+ };
3804
4384
  const succeed = () => {
3805
- if (settled)
4385
+ if (!current())
3806
4386
  return;
3807
- settled = true;
3808
4387
  cleanup();
3809
- btn.innerHTML = `<span class="oauth-check">\u2713</span> Success`;
4388
+ closePopup();
4389
+ recovery.remove();
4390
+ btn.textContent = t("success", locale);
4391
+ btn.disabled = true;
3810
4392
  this.handleSuccess();
3811
4393
  };
3812
- const onMessage = async (event) => {
3813
- if (event.origin !== oauthOrigin)
4394
+ cancel.onclick = () => {
4395
+ if (exchanging)
3814
4396
  return;
3815
- if (!event.data || event.data.type !== "authorization_response")
4397
+ fail("oauthCancelled");
4398
+ btn.focus();
4399
+ };
4400
+ const onMessage = async (event) => {
4401
+ if (event.origin !== oauthOrigin || event.source !== popup || !popup)
3816
4402
  return;
3817
- if (settled)
4403
+ if (!event.data || event.data.type !== "authorization_response" || !current() || exchanging)
3818
4404
  return;
3819
- if (pollTimer !== void 0) {
3820
- clearInterval(pollTimer);
3821
- pollTimer = void 0;
3822
- }
3823
- try {
3824
- popup.close();
3825
- } catch {
3826
- }
4405
+ clearInterval(pollTimer);
4406
+ clearInterval(sessionTimer);
3827
4407
  if (event.data.error) {
3828
- restore();
3829
- cleanup();
4408
+ fail(event.data.error === "access_denied" ? "oauthDenied" : event.data.error === "cancelled" ? "oauthCancelled" : "oauthFailed");
3830
4409
  return;
3831
4410
  }
3832
4411
  const { code, state } = event.data.response ?? event.data;
3833
- if (!code) {
3834
- restore();
3835
- cleanup();
4412
+ if (typeof code !== "string" || !code || typeof state !== "string" || !state) {
4413
+ fail("oauthFailed");
3836
4414
  return;
3837
4415
  }
3838
- btn.innerHTML = `<span class="oauth-spinner"></span> Signing in...`;
4416
+ exchanging = true;
4417
+ lockMethods();
4418
+ actions.hidden = true;
4419
+ closePopup();
4420
+ hint.textContent = t("verifying", locale);
4421
+ btn.textContent = t("verifying", locale);
3839
4422
  btn.disabled = true;
3840
4423
  try {
3841
4424
  const oauthBody = { provider: providerId, code, state };
3842
4425
  if (this.options.invitationId)
3843
4426
  oauthBody.invitationId = this.options.invitationId;
3844
- await this.http.post(`${servicePrefix}/api/oauth/login`, oauthBody);
4427
+ await this.http.post(`${servicePrefix}/api/oauth/login`, oauthBody, { signal: this.abort?.signal });
3845
4428
  succeed();
3846
- } catch (oauthErr) {
3847
- const errMsg = oauthErr instanceof Error ? oauthErr.message : "";
3848
- const errCode = oauthErr instanceof Error && "code" in oauthErr ? oauthErr.code : "";
3849
- if (errCode === "FORBIDDEN" || errMsg === "registration_closed" || errCode === "registration_closed") {
3850
- settled = true;
4429
+ } catch (err) {
4430
+ if (!current())
4431
+ return;
4432
+ const code2 = err instanceof Error && "code" in err ? String(err.code) : "";
4433
+ const message = err instanceof Error ? err.message : "";
4434
+ if (code2 === "FORBIDDEN" || code2 === "registration_closed" || message === "registration_closed") {
3851
4435
  cleanup();
3852
- this.showFullError(friendlyError(oauthErr, this.options.locale));
3853
- } else {
3854
- restore();
3855
- }
4436
+ recovery.remove();
4437
+ this.showFullError(friendlyError(err, locale));
4438
+ } else
4439
+ fail("oauthFailed");
4440
+ }
4441
+ };
4442
+ this.oauthCleanup = () => {
4443
+ cleanup();
4444
+ recovery.remove();
4445
+ closePopup();
4446
+ };
4447
+ window.addEventListener("message", onMessage);
4448
+ try {
4449
+ popup = window.open(loginUrl, "oauth-login:popup", `left=${window.screenX + (window.innerWidth - 500) / 2},top=${window.screenY + (window.innerHeight - 620) / 2},width=500,height=620,resizable,scrollbars=yes,status=1,popup`);
4450
+ } catch {
4451
+ }
4452
+ if (!popup) {
4453
+ fail("oauthBlocked");
4454
+ return;
4455
+ }
4456
+ hint.textContent = t("oauthWaiting", locale, { provider });
4457
+ btn.textContent = t("oauthWaiting", locale, { provider });
4458
+ btn.disabled = true;
4459
+ btn.classList.add("oauth-btn-loading");
4460
+ const checkSession = async () => {
4461
+ if (!current() || exchanging || probing)
4462
+ return;
4463
+ probing = true;
4464
+ try {
4465
+ const session = await this.http.get(`${servicePrefix}/api/did/session`);
4466
+ if (current() && !exchanging && session?.authenticated === true)
4467
+ succeed();
4468
+ } catch {
4469
+ } finally {
4470
+ probing = false;
3856
4471
  }
3857
4472
  };
3858
- let pollTimer = setInterval(() => {
4473
+ pollTimer = setInterval(() => {
3859
4474
  let closed = false;
3860
4475
  try {
3861
- closed = popup.closed;
4476
+ closed = !!popup?.closed;
3862
4477
  } catch {
3863
4478
  return;
3864
4479
  }
3865
- if (!closed)
4480
+ if (!closed || !current())
3866
4481
  return;
3867
- if (pollTimer !== void 0) {
3868
- clearInterval(pollTimer);
3869
- pollTimer = void 0;
3870
- }
3871
- restore();
3872
- const checkSession = () => {
3873
- void this.http.get(`${servicePrefix}/api/did/session`).then((session) => {
3874
- if (session?.authenticated)
3875
- succeed();
3876
- }).catch(() => {
3877
- });
3878
- };
3879
- checkSession();
3880
- sessionPollTimer = setInterval(checkSession, 500);
4482
+ clearInterval(pollTimer);
4483
+ showRecovery("oauthIncomplete");
4484
+ cancel.hidden = false;
4485
+ void checkSession();
4486
+ sessionTimer = setInterval(() => {
4487
+ void checkSession();
4488
+ }, 2e3);
3881
4489
  }, 500);
3882
- const abandonTimer = setTimeout(() => {
3883
- if (settled)
4490
+ abandonTimer = setTimeout(() => {
4491
+ if (!current())
4492
+ return;
4493
+ if (!exchanging) {
4494
+ fail("oauthTimedOut");
3884
4495
  return;
4496
+ }
4497
+ this.abort?.abort();
3885
4498
  cleanup();
3886
- restore();
4499
+ closePopup();
4500
+ lockMethods();
4501
+ hint.textContent = t("oauthUnconfirmed", locale);
4502
+ recovery.dataset.state = "oauthUnconfirmed";
4503
+ actions.hidden = false;
4504
+ reopen.hidden = true;
4505
+ cancel.hidden = true;
4506
+ fullPage.href = window.location.href;
4507
+ fullPage.textContent = t("oauthCheckSession", locale);
4508
+ this.emit("incomplete");
3887
4509
  }, 5 * 60 * 1e3);
3888
- const cleanup = () => {
3889
- if (pollTimer !== void 0) {
3890
- clearInterval(pollTimer);
3891
- pollTimer = void 0;
3892
- }
3893
- if (sessionPollTimer !== void 0) {
3894
- clearInterval(sessionPollTimer);
3895
- sessionPollTimer = void 0;
3896
- }
3897
- clearTimeout(abandonTimer);
3898
- window.removeEventListener("message", onMessage);
3899
- };
3900
- window.addEventListener("message", onMessage);
3901
4510
  }
3902
4511
  // (removed dead code: renderEmailButton — email now renders through renderSecondaryGrid)
3903
4512
  /**
@@ -3906,19 +4515,25 @@ var LoginPage = class {
3906
4515
  * Even count: all items in 2 columns (each half width).
3907
4516
  * Odd count: DID Wallet spans full width at the end.
3908
4517
  */
3909
- renderSecondaryGrid(parent, locale, methods) {
4518
+ renderSecondaryGrid(parent, locale, methods, group) {
3910
4519
  const grid = document.createElement("div");
3911
4520
  grid.className = "methods-grid";
3912
4521
  if (methods.includes("oauth") && this.options.oauthProviders?.length) {
3913
- const enabledProviders = (this.options.oauthProviders ?? []).filter((p) => p.enabled);
4522
+ const order = this.options.oauthProviderOrder ?? ["google", "apple", "github"];
4523
+ const rank = (id) => order.includes(id) ? order.indexOf(id) : order.length;
4524
+ const explicitOrder = !!this.options.oauthProviderOrder || this.options.oauthProviders?.some((p) => p.order !== void 0 && p.order !== 999);
4525
+ const enabledProviders = (this.options.oauthProviders ?? []).filter((p) => p.enabled && (!group || (explicitOrder || ["google", "apple"].includes(p.id)) === (group === "primary"))).sort((a, b) => this.options.oauthProviderOrder ? rank(a.id) - rank(b.id) : (a.order ?? 999) - (b.order ?? 999) || rank(a.id) - rank(b.id));
3914
4526
  for (const provider of enabledProviders) {
3915
4527
  const btn = document.createElement("button");
3916
4528
  btn.className = "method-btn";
3917
4529
  btn.dataset.provider = provider.id;
3918
4530
  const icon = getOAuthIcon(provider.id);
3919
- btn.innerHTML = `${icon} ${escapeHtml(provider.name)}`;
4531
+ btn.innerHTML = `${icon} ${escapeHtml(t("continueWith", locale, { provider: provider.name }))}`;
4532
+ this.markLastUsed(btn, `oauth:${provider.id}`, locale);
3920
4533
  grid.appendChild(btn);
3921
4534
  btn.addEventListener("click", () => {
4535
+ this.select("oauth");
4536
+ this.activeProvider = provider.id;
3922
4537
  this.startOAuthPopup(provider.id, btn);
3923
4538
  });
3924
4539
  }
@@ -3928,8 +4543,10 @@ var LoginPage = class {
3928
4543
  btn.className = "method-btn";
3929
4544
  btn.id = "email-btn";
3930
4545
  btn.innerHTML = `${ICON_EMAIL} ${escapeHtml(t("emailButton", locale))}`;
4546
+ this.markLastUsed(btn, "email", locale);
3931
4547
  grid.appendChild(btn);
3932
4548
  btn.addEventListener("click", () => {
4549
+ this.select("email");
3933
4550
  this.switchView("email");
3934
4551
  });
3935
4552
  }
@@ -3939,8 +4556,10 @@ var LoginPage = class {
3939
4556
  btn.id = "did-wallet-btn";
3940
4557
  const walletLabel = grid.children.length > 0 ? t("didWalletButtonShort", locale) : t("didWalletButton", locale);
3941
4558
  btn.innerHTML = `${ICON_DID_WALLET} ${escapeHtml(walletLabel)}`;
4559
+ this.markLastUsed(btn, "did-connect", locale);
3942
4560
  grid.appendChild(btn);
3943
4561
  btn.addEventListener("click", () => {
4562
+ this.select("did-connect");
3944
4563
  this.switchView("qr");
3945
4564
  });
3946
4565
  }
@@ -3949,19 +4568,27 @@ var LoginPage = class {
3949
4568
  // ─── Private: DID Connect flow (QR view) ───────────────────────────
3950
4569
  async startDIDConnectInView(statusEl, qrContainer, walletBtn, locale) {
3951
4570
  statusEl.textContent = t("connecting", locale);
4571
+ const epoch = this.generation;
4572
+ const isCurrent = () => this.mounted && epoch === this.generation;
3952
4573
  try {
3953
4574
  this.tokenSession?.destroy();
3954
4575
  this.tokenSession = new TokenSession(this.config, this.http);
3955
4576
  this.tokenSession.on("succeed", async () => {
4577
+ if (!isCurrent())
4578
+ return;
3956
4579
  this.showConnectResult(qrContainer, walletBtn, statusEl, t("verifying", locale));
3957
4580
  try {
3958
4581
  const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
3959
4582
  await this.http.post(`${servicePrefix}/api/did/connect/complete`, {
3960
4583
  token: this.tokenSession.state.token
3961
4584
  });
4585
+ if (!isCurrent())
4586
+ return;
3962
4587
  this.showConnectSuccess(qrContainer, walletBtn, statusEl, locale);
3963
4588
  this.handleSuccess();
3964
4589
  } catch (err) {
4590
+ if (!isCurrent())
4591
+ return;
3965
4592
  this.showConnectResult(qrContainer, walletBtn, statusEl, friendlyError(err, locale), true);
3966
4593
  }
3967
4594
  });
@@ -3992,17 +4619,21 @@ var LoginPage = class {
3992
4619
  }
3993
4620
  });
3994
4621
  const state = await this.tokenSession.create({ locale });
4622
+ if (!isCurrent())
4623
+ return;
3995
4624
  if (state.url) {
3996
4625
  renderQR(qrContainer, state.url);
3997
4626
  walletBtn.disabled = false;
3998
4627
  walletBtn.addEventListener("click", () => {
3999
- this.showConnectScanned(qrContainer, walletBtn, statusEl, locale);
4628
+ statusEl.textContent = t("walletRetry", locale);
4000
4629
  this.openInWallet(state.url, locale);
4001
4630
  });
4002
4631
  }
4003
4632
  statusEl.textContent = t("scanWithWallet", locale);
4004
4633
  this.tokenSession.startPolling();
4005
4634
  } catch (err) {
4635
+ if (!isCurrent())
4636
+ return;
4006
4637
  statusEl.textContent = friendlyError(err, locale);
4007
4638
  clearQR(qrContainer);
4008
4639
  }
@@ -4022,7 +4653,7 @@ var LoginPage = class {
4022
4653
  }
4023
4654
  this.cardEl.innerHTML = "";
4024
4655
  this.cardEl.dataset.view = "error";
4025
- this.renderLocaleSwitcher(this.cardEl);
4656
+ this.renderCardTools(this.cardEl);
4026
4657
  const resultContainer = document.createElement("div");
4027
4658
  resultContainer.className = "connect-result";
4028
4659
  resultContainer.innerHTML = `<div class="result-icon result-error">\u2715</div>`;
@@ -4161,13 +4792,241 @@ var LoginPage = class {
4161
4792
  }
4162
4793
  // ─── Private: success handler ────────────────────────────────────────
4163
4794
  handleSuccess() {
4795
+ if (!this.mounted || this.completed)
4796
+ return;
4797
+ this.completed = true;
4798
+ this.emit("success");
4799
+ if (this.options.preferenceKey && this.activeMethod) {
4800
+ try {
4801
+ localStorage.setItem(`did-login-method:${this.options.preferenceKey}`, this.activeMethod === "oauth" && this.activeProvider ? `oauth:${this.activeProvider}` : this.activeMethod);
4802
+ } catch {
4803
+ }
4804
+ }
4164
4805
  if (this.options.onSuccess) {
4165
- this.options.onSuccess();
4806
+ const epoch = this.generation;
4807
+ Promise.resolve().then(() => {
4808
+ if (this.mounted && epoch === this.generation)
4809
+ return this.options.onSuccess?.();
4810
+ }).catch(() => {
4811
+ if (!this.mounted || epoch !== this.generation)
4812
+ return;
4813
+ this.completed = false;
4814
+ this.showFullError(t("authFailed", this.options.locale));
4815
+ });
4166
4816
  } else {
4167
4817
  setTimeout(() => location.reload(), 300);
4168
4818
  }
4169
4819
  }
4170
4820
  };
4821
+
4822
+ // dist/ui/login-dialog.js
4823
+ var DIALOG_STYLES = `${DESIGN_TOKENS_CSS}${DESIGN_TOKENS_LIGHT_CSS}
4824
+ :host { color-scheme: dark; }
4825
+ :host([data-theme="light"]) { color-scheme: light; }
4826
+ dialog { box-sizing:border-box; position:fixed; inset:0; margin:auto; padding:0;
4827
+ width:min(420px,calc(100vw - 32px)); max-width:none; max-height:calc(100dvh - 32px);
4828
+ border:1px solid var(--border); border-radius:16px; background:var(--bg-card);
4829
+ color:var(--text); color-scheme:inherit; overflow:hidden; box-shadow:var(--shadow-lg); }
4830
+ dialog::backdrop { background:rgba(8,10,18,.56); }
4831
+ :host(:not([data-theme="light"])) dialog::backdrop { background:rgba(0,0,0,.68); }
4832
+ [data-dialog-scroll] { max-height:calc(100dvh - 34px); overflow:auto; overscroll-behavior:contain; }
4833
+ .close { position:absolute; top:8px; right:8px; z-index:3; display:grid; place-items:center;
4834
+ box-sizing:border-box; width:44px; height:44px; padding:0; border:1px solid transparent;
4835
+ border-radius:10px; background:var(--bg-card); color:var(--text-secondary);
4836
+ font:400 24px/1 system-ui,sans-serif; cursor:pointer; }
4837
+ .close:hover { color:var(--text); background:var(--bg-hover); }
4838
+ .close:focus-visible { outline:2px solid var(--blue); outline-offset:-3px; }
4839
+ .host-error { padding:16px 20px; font:14px/1.5 system-ui,sans-serif; overflow-wrap:anywhere; }
4840
+ .retry { padding:10px 16px; margin:0 20px 20px; color:var(--text); background:var(--bg-elevated);
4841
+ border:1px solid var(--border); border-radius:8px; font:inherit; cursor:pointer; }
4842
+ `;
4843
+ var LoginDialog = class {
4844
+ config;
4845
+ http;
4846
+ options;
4847
+ host;
4848
+ dialog;
4849
+ page;
4850
+ previous;
4851
+ themeObserver;
4852
+ systemTheme;
4853
+ syncTheme = () => this.applyTheme(this.hostTheme());
4854
+ constructor(config, http, options = {}) {
4855
+ this.config = config;
4856
+ this.http = http;
4857
+ this.options = options;
4858
+ }
4859
+ mount() {
4860
+ if (this.dialog)
4861
+ return;
4862
+ const hostLocale = document.documentElement.lang;
4863
+ if (hostLocale)
4864
+ this.options.locale = hostLocale;
4865
+ let focused = document.activeElement;
4866
+ while (focused?.shadowRoot?.activeElement)
4867
+ focused = focused.shadowRoot.activeElement;
4868
+ this.previous = this.options.returnFocus ?? (focused instanceof HTMLElement ? focused : void 0);
4869
+ const host = document.createElement("div");
4870
+ host.dataset.didLoginDialogHost = "";
4871
+ host.style.cssText = "position:fixed;width:0;height:0;";
4872
+ this.host = host;
4873
+ const root = host.attachShadow({ mode: "open" });
4874
+ const style = document.createElement("style");
4875
+ style.textContent = DIALOG_STYLES;
4876
+ root.append(style);
4877
+ const dialog = document.createElement("dialog");
4878
+ this.dialog = dialog;
4879
+ dialog.setAttribute("aria-label", this.options.purpose?.title ?? t("signIn", this.options.locale));
4880
+ root.append(dialog);
4881
+ const close = document.createElement("button");
4882
+ close.type = "button";
4883
+ close.className = "close";
4884
+ close.textContent = "\xD7";
4885
+ close.setAttribute("aria-label", t("cancel", this.options.locale));
4886
+ const cancel = () => {
4887
+ if (this.options.dismissible === false || this.dialog !== dialog)
4888
+ return;
4889
+ try {
4890
+ this.options.onEvent?.({ name: "cancel", mode: "dialog" });
4891
+ } catch {
4892
+ }
4893
+ this.destroy();
4894
+ this.options.onCancel?.();
4895
+ };
4896
+ close.addEventListener("click", cancel);
4897
+ if (this.options.dismissible !== false)
4898
+ dialog.append(close);
4899
+ const scroller = document.createElement("div");
4900
+ scroller.dataset.dialogScroll = "";
4901
+ dialog.append(scroller);
4902
+ const container = document.createElement("div");
4903
+ container.dataset.loginDialogMount = "";
4904
+ scroller.append(container);
4905
+ dialog.addEventListener("cancel", (event) => {
4906
+ event.preventDefault();
4907
+ cancel();
4908
+ });
4909
+ document.body.append(host);
4910
+ const theme = this.hostTheme();
4911
+ host.dataset.theme = theme;
4912
+ this.page = new LoginPage(container, this.config, this.http, {
4913
+ ...this.options,
4914
+ theme,
4915
+ isolate: true,
4916
+ mode: "dialog",
4917
+ onCancel: cancel,
4918
+ onLocaleChange: (locale) => {
4919
+ this.options.locale = locale;
4920
+ close.setAttribute("aria-label", t("cancel", locale));
4921
+ dialog.setAttribute("aria-label", this.options.purpose?.title ?? t("signIn", locale));
4922
+ this.options.onLocaleChange?.(locale);
4923
+ },
4924
+ onSuccess: async () => {
4925
+ const showFailure = () => {
4926
+ if (!dialog.isConnected || this.dialog !== dialog)
4927
+ return;
4928
+ for (const node of scroller.querySelectorAll(".host-error, .retry"))
4929
+ node.remove();
4930
+ const status = document.createElement("p");
4931
+ status.className = "host-error";
4932
+ status.setAttribute("role", "alert");
4933
+ status.textContent = t("authFailed", this.options.locale);
4934
+ const retry = document.createElement("button");
4935
+ retry.type = "button";
4936
+ retry.className = "retry";
4937
+ retry.textContent = t("tryAgain", this.options.locale);
4938
+ retry.onclick = async () => {
4939
+ retry.disabled = true;
4940
+ try {
4941
+ await this.options.onSuccess?.();
4942
+ if (this.dialog === dialog)
4943
+ this.destroy();
4944
+ } catch {
4945
+ showFailure();
4946
+ }
4947
+ };
4948
+ scroller.append(status, retry);
4949
+ };
4950
+ try {
4951
+ await this.options.onSuccess?.();
4952
+ if (this.dialog === dialog)
4953
+ this.destroy();
4954
+ } catch {
4955
+ showFailure();
4956
+ }
4957
+ }
4958
+ });
4959
+ this.page.mount();
4960
+ this.themeObserver = new MutationObserver(this.syncTheme);
4961
+ for (const target of [document.documentElement, document.body]) {
4962
+ this.themeObserver.observe(target, { attributes: true, attributeFilter: ["data-theme", "data-mode", "class", "style"] });
4963
+ }
4964
+ if (typeof window.matchMedia === "function") {
4965
+ this.systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
4966
+ this.systemTheme.addEventListener("change", this.syncTheme);
4967
+ }
4968
+ dialog.showModal();
4969
+ const title = container.shadowRoot?.querySelector("h1, h2");
4970
+ if (title) {
4971
+ title.tabIndex = -1;
4972
+ title.focus({ preventScroll: true });
4973
+ }
4974
+ }
4975
+ hostTheme() {
4976
+ for (const element of [document.documentElement, document.body]) {
4977
+ for (const name of ["data-mode", "data-theme"]) {
4978
+ const value = element.getAttribute(name);
4979
+ if (value === "light" || value === "dark")
4980
+ return value;
4981
+ if (value === "system" || value === "auto")
4982
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
4983
+ }
4984
+ }
4985
+ for (const element of [document.documentElement, document.body]) {
4986
+ if (element.classList.contains("dark"))
4987
+ return "dark";
4988
+ if (element.classList.contains("light"))
4989
+ return "light";
4990
+ }
4991
+ for (const element of [document.body, document.documentElement]) {
4992
+ const schemes = getComputedStyle(element).colorScheme.split(/\s+/);
4993
+ if (schemes.includes("light") && schemes.includes("dark")) {
4994
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
4995
+ }
4996
+ if (schemes.includes("light"))
4997
+ return "light";
4998
+ if (schemes.includes("dark"))
4999
+ return "dark";
5000
+ }
5001
+ if (this.options.theme === "light" || this.options.theme === "dark")
5002
+ return this.options.theme;
5003
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
5004
+ }
5005
+ setTheme(theme) {
5006
+ this.options.theme = theme;
5007
+ this.applyTheme(theme);
5008
+ }
5009
+ applyTheme(theme) {
5010
+ if (this.host)
5011
+ this.host.dataset.theme = theme;
5012
+ this.page?.setTheme(theme);
5013
+ }
5014
+ destroy() {
5015
+ this.themeObserver?.disconnect();
5016
+ this.themeObserver = void 0;
5017
+ this.systemTheme?.removeEventListener("change", this.syncTheme);
5018
+ this.systemTheme = void 0;
5019
+ this.page?.destroy();
5020
+ this.page = void 0;
5021
+ this.dialog?.close();
5022
+ this.dialog?.remove();
5023
+ this.dialog = void 0;
5024
+ this.host?.remove();
5025
+ this.host = void 0;
5026
+ if (this.previous?.isConnected)
5027
+ this.previous.focus({ preventScroll: true });
5028
+ }
5029
+ };
4171
5030
  export {
4172
5031
  DESIGN_RESET_CSS,
4173
5032
  DESIGN_TOKENS_CSS,
@@ -4177,6 +5036,7 @@ export {
4177
5036
  ICON_EMAIL,
4178
5037
  ICON_PASSKEY,
4179
5038
  LOGIN_PAGE_STYLES,
5039
+ LoginDialog,
4180
5040
  LoginPage,
4181
5041
  OAUTH_ICONS,
4182
5042
  SUPPORTED_LOCALES,