@arcblock/did-connect-core 4.1.51 → 4.1.53

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,32 @@ 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: {method}",
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
+ oauthFullPage: "Continue on this page",
391
+ emailEdit: "Change email",
392
+ emailCooldown: "Resend in {seconds}s",
393
+ walletRetry: "Wallet did not open? Try again or scan the QR code.",
394
+ tryAgain: "Try again",
395
+ signIn: "Sign in or create an account",
396
+ signInTo: "Sign in or join {appName}",
383
397
  passkeyButton: "Continue with Passkey",
384
398
  passkeySignIn: "Sign in with Passkey",
385
399
  passkeyCreate: "Create Passkey",
386
- passkeyFirstTime: "First time? Create a passkey",
400
+ passkeyFirstTime: "First time? Create an account",
387
401
  passkeyBackToSignIn: "Back to sign in",
388
402
  passkeyWaiting: "Waiting for passkey\u2026",
389
403
  passkeyCreating: "Creating your passkey\u2026",
390
- passkeyNoLocal: "No passkey found on this device.",
404
+ passkeyNoLocal: "Passkey verification was not completed. Retry, use another device, or choose another method.",
391
405
  passkeyUseAnotherDevice: "Use another device",
392
- passkeyCreateInstead: "Create a new passkey instead",
406
+ passkeyCreateInstead: "First time? Create an account",
393
407
  didWalletButton: "Continue with DID Wallet",
394
408
  didWalletButtonShort: "DID Wallet",
395
409
  scanWithWallet: "Scan with your DID Wallet",
@@ -422,26 +436,40 @@ var messages = {
422
436
  emailTitle: "Continue with Email",
423
437
  emailSubmit: "Send Code",
424
438
  emailSending: "Sending code...",
425
- emailSent: "We sent a 6-digit code to {email}",
426
- emailCodePlaceholder: "6-digit code",
439
+ emailSent: "We sent an 8-digit code to {email}",
440
+ emailCodePlaceholder: "8-digit code",
427
441
  emailVerify: "Verify",
428
442
  emailResend: "Resend code",
429
443
  emailInvalid: "Please enter a valid email address.",
430
- emailCodeInvalid: "Please enter the 6-digit code."
444
+ emailCodeInvalid: "Please enter the 8-digit code."
431
445
  },
432
446
  zh: {
433
- signIn: "\u767B\u5F55",
434
- signInTo: "\u767B\u5F55\u5230 {appName}",
447
+ emailExplanation: "\u6211\u4EEC\u4F1A\u53D1\u9001\u9A8C\u8BC1\u7801\uFF0C\u65E0\u9700\u5BC6\u7801\u3002",
448
+ otherMethods: "\u5176\u4ED6\u767B\u5F55\u65B9\u5F0F",
449
+ passkeyCreateAccount: "\u4F7F\u7528 Passkey \u521B\u5EFA\u8D26\u6237",
450
+ lastUsed: "\u4E0A\u6B21\u4F7F\u7528\uFF1A{method}",
451
+ 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",
452
+ continueWith: "\u4F7F\u7528 {provider} \u7EE7\u7EED",
453
+ oauthWaiting: "\u8BF7\u5728 {provider} \u7A97\u53E3\u4E2D\u7EE7\u7EED",
454
+ 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",
455
+ oauthReopen: "\u91CD\u65B0\u6253\u5F00\u7A97\u53E3",
456
+ oauthFullPage: "\u5728\u5F53\u524D\u9875\u9762\u7EE7\u7EED",
457
+ emailEdit: "\u4FEE\u6539\u90AE\u7BB1",
458
+ emailCooldown: "{seconds} \u79D2\u540E\u91CD\u53D1",
459
+ walletRetry: "\u94B1\u5305\u672A\u6253\u5F00\uFF1F\u8BF7\u91CD\u8BD5\u6216\u626B\u63CF\u4E8C\u7EF4\u7801\u3002",
460
+ tryAgain: "\u91CD\u8BD5",
461
+ signIn: "\u767B\u5F55\u6216\u521B\u5EFA\u8D26\u6237",
462
+ signInTo: "\u767B\u5F55\u6216\u52A0\u5165 {appName}",
435
463
  passkeyButton: "\u4F7F\u7528 Passkey \u7EE7\u7EED",
436
464
  passkeySignIn: "\u4F7F\u7528 Passkey \u767B\u5F55",
437
465
  passkeyCreate: "\u521B\u5EFA Passkey",
438
- passkeyFirstTime: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u521B\u5EFA Passkey",
466
+ passkeyFirstTime: "\u9996\u6B21\u4F7F\u7528\uFF1F\u521B\u5EFA\u8D26\u6237",
439
467
  passkeyBackToSignIn: "\u8FD4\u56DE\u767B\u5F55",
440
468
  passkeyWaiting: "\u6B63\u5728\u7B49\u5F85 Passkey\u2026",
441
469
  passkeyCreating: "\u6B63\u5728\u521B\u5EFA Passkey\u2026",
442
- passkeyNoLocal: "\u6B64\u8BBE\u5907\u4E0A\u6CA1\u6709\u627E\u5230 Passkey\u3002",
470
+ 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
471
  passkeyUseAnotherDevice: "\u7528\u5176\u4ED6\u8BBE\u5907\u767B\u5F55",
444
- passkeyCreateInstead: "\u6539\u4E3A\u521B\u5EFA\u65B0\u7684 Passkey",
472
+ passkeyCreateInstead: "\u9996\u6B21\u4F7F\u7528\uFF1F\u521B\u5EFA\u8D26\u6237",
445
473
  didWalletButton: "\u4F7F\u7528 DID Wallet \u7EE7\u7EED",
446
474
  didWalletButtonShort: "DID Wallet",
447
475
  scanWithWallet: "\u8BF7\u4F7F\u7528 DID Wallet \u626B\u7801",
@@ -475,25 +503,39 @@ var messages = {
475
503
  emailSubmit: "\u53D1\u9001\u9A8C\u8BC1\u7801",
476
504
  emailSending: "\u6B63\u5728\u53D1\u9001\u9A8C\u8BC1\u7801...",
477
505
  emailSent: "\u9A8C\u8BC1\u7801\u5DF2\u53D1\u9001\u81F3 {email}",
478
- emailCodePlaceholder: "6 \u4F4D\u9A8C\u8BC1\u7801",
506
+ emailCodePlaceholder: "8 \u4F4D\u9A8C\u8BC1\u7801",
479
507
  emailVerify: "\u9A8C\u8BC1",
480
508
  emailResend: "\u91CD\u65B0\u53D1\u9001",
481
509
  emailInvalid: "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740\u3002",
482
- emailCodeInvalid: "\u8BF7\u8F93\u5165 6 \u4F4D\u9A8C\u8BC1\u7801\u3002"
510
+ emailCodeInvalid: "\u8BF7\u8F93\u5165 8 \u4F4D\u9A8C\u8BC1\u7801\u3002"
483
511
  },
484
512
  "zh-TW": {
485
- signIn: "\u767B\u5165",
486
- signInTo: "\u767B\u5165\u81F3 {appName}",
513
+ emailExplanation: "\u6211\u5011\u6703\u50B3\u9001\u9A57\u8B49\u78BC\uFF0C\u7121\u9700\u5BC6\u78BC\u3002",
514
+ otherMethods: "\u5176\u4ED6\u767B\u5165\u65B9\u5F0F",
515
+ passkeyCreateAccount: "\u4F7F\u7528 Passkey \u5EFA\u7ACB\u5E33\u6236",
516
+ lastUsed: "\u4E0A\u6B21\u4F7F\u7528\uFF1A{method}",
517
+ 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",
518
+ continueWith: "\u4F7F\u7528 {provider} \u7E7C\u7E8C",
519
+ oauthWaiting: "\u8ACB\u5728 {provider} \u8996\u7A97\u4E2D\u7E7C\u7E8C",
520
+ 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",
521
+ oauthReopen: "\u91CD\u65B0\u958B\u555F\u8996\u7A97",
522
+ oauthFullPage: "\u5728\u76EE\u524D\u9801\u9762\u7E7C\u7E8C",
523
+ emailEdit: "\u4FEE\u6539\u96FB\u5B50\u90F5\u4EF6",
524
+ emailCooldown: "{seconds} \u79D2\u5F8C\u91CD\u65B0\u50B3\u9001",
525
+ walletRetry: "\u9322\u5305\u672A\u958B\u555F\uFF1F\u8ACB\u91CD\u8A66\u6216\u6383\u78BC\u3002",
526
+ tryAgain: "\u91CD\u8A66",
527
+ signIn: "\u767B\u5165\u6216\u5EFA\u7ACB\u5E33\u6236",
528
+ signInTo: "\u767B\u5165\u6216\u52A0\u5165 {appName}",
487
529
  passkeyButton: "\u4F7F\u7528 Passkey \u7E7C\u7E8C",
488
530
  passkeySignIn: "\u4F7F\u7528 Passkey \u767B\u5165",
489
531
  passkeyCreate: "\u5EFA\u7ACB Passkey",
490
- passkeyFirstTime: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB Passkey",
532
+ passkeyFirstTime: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB\u5E33\u6236",
491
533
  passkeyBackToSignIn: "\u8FD4\u56DE\u767B\u5165",
492
534
  passkeyWaiting: "\u6B63\u5728\u7B49\u5F85 Passkey\u2026",
493
535
  passkeyCreating: "\u6B63\u5728\u5EFA\u7ACB Passkey\u2026",
494
- passkeyNoLocal: "\u6B64\u88DD\u7F6E\u4E0A\u6C92\u6709\u627E\u5230 Passkey\u3002",
536
+ 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
537
  passkeyUseAnotherDevice: "\u7528\u5176\u4ED6\u88DD\u7F6E\u767B\u5165",
496
- passkeyCreateInstead: "\u6539\u70BA\u5EFA\u7ACB\u65B0\u7684 Passkey",
538
+ passkeyCreateInstead: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB\u5E33\u6236",
497
539
  didWalletButton: "\u4F7F\u7528 DID Wallet \u7E7C\u7E8C",
498
540
  didWalletButtonShort: "DID Wallet",
499
541
  scanWithWallet: "\u8ACB\u4F7F\u7528 DID Wallet \u6383\u78BC",
@@ -527,25 +569,39 @@ var messages = {
527
569
  emailSubmit: "\u50B3\u9001\u9A57\u8B49\u78BC",
528
570
  emailSending: "\u6B63\u5728\u50B3\u9001\u9A57\u8B49\u78BC...",
529
571
  emailSent: "\u9A57\u8B49\u78BC\u5DF2\u50B3\u9001\u81F3 {email}",
530
- emailCodePlaceholder: "6 \u4F4D\u6578\u9A57\u8B49\u78BC",
572
+ emailCodePlaceholder: "8 \u4F4D\u6578\u9A57\u8B49\u78BC",
531
573
  emailVerify: "\u9A57\u8B49",
532
574
  emailResend: "\u91CD\u65B0\u50B3\u9001",
533
575
  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"
576
+ emailCodeInvalid: "\u8ACB\u8F38\u5165 8 \u4F4D\u6578\u9A57\u8B49\u78BC\u3002"
535
577
  },
536
578
  ja: {
537
- signIn: "\u30ED\u30B0\u30A4\u30F3",
538
- signInTo: "{appName} \u306B\u30ED\u30B0\u30A4\u30F3",
579
+ emailExplanation: "\u78BA\u8A8D\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u307E\u3059\u3002\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u4E0D\u8981\u3067\u3059\u3002",
580
+ otherMethods: "\u4ED6\u306E\u30ED\u30B0\u30A4\u30F3\u65B9\u6CD5",
581
+ passkeyCreateAccount: "Passkey \u3067\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
582
+ lastUsed: "\u524D\u56DE\u306E\u65B9\u6CD5\uFF1A{method}",
583
+ 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",
584
+ continueWith: "{provider} \u3067\u7D9A\u3051\u308B",
585
+ oauthWaiting: "{provider} \u306E\u30A6\u30A3\u30F3\u30C9\u30A6\u3067\u7D9A\u3051\u3066\u304F\u3060\u3055\u3044",
586
+ 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",
587
+ oauthReopen: "\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304D\u76F4\u3059",
588
+ oauthFullPage: "\u3053\u306E\u30DA\u30FC\u30B8\u3067\u7D9A\u3051\u308B",
589
+ emailEdit: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5909\u66F4",
590
+ emailCooldown: "{seconds} \u79D2\u5F8C\u306B\u518D\u9001\u4FE1",
591
+ 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",
592
+ tryAgain: "\u518D\u8A66\u884C",
593
+ signIn: "\u30ED\u30B0\u30A4\u30F3\u307E\u305F\u306F\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210",
594
+ signInTo: "{appName} \u306B\u30ED\u30B0\u30A4\u30F3\u307E\u305F\u306F\u53C2\u52A0",
539
595
  passkeyButton: "Passkey \u3067\u7D9A\u3051\u308B",
540
596
  passkeySignIn: "Passkey \u3067\u30ED\u30B0\u30A4\u30F3",
541
597
  passkeyCreate: "Passkey \u3092\u4F5C\u6210",
542
- passkeyFirstTime: "\u521D\u3081\u3066\u3067\u3059\u304B\uFF1FPasskey \u3092\u4F5C\u6210",
598
+ passkeyFirstTime: "\u521D\u3081\u3066\u3067\u3059\u304B\uFF1F\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
543
599
  passkeyBackToSignIn: "\u30ED\u30B0\u30A4\u30F3\u306B\u623B\u308B",
544
600
  passkeyWaiting: "Passkey \u3092\u5F85\u3063\u3066\u3044\u307E\u3059\u2026",
545
601
  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",
602
+ 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
603
  passkeyUseAnotherDevice: "\u5225\u306E\u30C7\u30D0\u30A4\u30B9\u3092\u4F7F\u7528",
548
- passkeyCreateInstead: "\u4EE3\u308F\u308A\u306B\u65B0\u3057\u3044 Passkey \u3092\u4F5C\u6210",
604
+ passkeyCreateInstead: "\u521D\u3081\u3066\u3067\u3059\u304B\uFF1F\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
549
605
  didWalletButton: "DID Wallet \u3067\u7D9A\u3051\u308B",
550
606
  didWalletButtonShort: "DID Wallet",
551
607
  scanWithWallet: "DID Wallet \u3067\u30B9\u30AD\u30E3\u30F3\u3057\u3066\u304F\u3060\u3055\u3044",
@@ -578,12 +634,12 @@ var messages = {
578
634
  emailTitle: "\u30E1\u30FC\u30EB\u3067\u7D9A\u3051\u308B",
579
635
  emailSubmit: "\u30B3\u30FC\u30C9\u3092\u9001\u4FE1",
580
636
  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",
637
+ emailSent: "{email} \u306B 8 \u6841\u306E\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u307E\u3057\u305F",
638
+ emailCodePlaceholder: "8 \u6841\u306E\u30B3\u30FC\u30C9",
583
639
  emailVerify: "\u78BA\u8A8D",
584
640
  emailResend: "\u30B3\u30FC\u30C9\u3092\u518D\u9001\u4FE1",
585
641
  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"
642
+ emailCodeInvalid: "8 \u6841\u306E\u30B3\u30FC\u30C9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
587
643
  }
588
644
  };
589
645
  var SUPPORTED_LOCALES = [
@@ -592,7 +648,7 @@ var SUPPORTED_LOCALES = [
592
648
  ];
593
649
  function t(key, locale, params) {
594
650
  const lang = locale && messages[locale] ? locale : "en";
595
- let msg = messages[lang][key] ?? key;
651
+ let msg = messages[lang][key] ?? messages.en[key] ?? key;
596
652
  if (params) {
597
653
  for (const [k, v] of Object.entries(params)) {
598
654
  msg = msg.replace(new RegExp(`\\{${k}\\}`, "g"), v);
@@ -2258,8 +2314,17 @@ var qrcode_default = qrcode;
2258
2314
  var stringToBytes = qrcode.stringToBytes;
2259
2315
 
2260
2316
  // dist/ui/qr-renderer.js
2317
+ var QR_PLATE_DARK = "#e8e8e8";
2318
+ var QR_PLATE_LIGHT = "#ffffff";
2319
+ function plateColor(container) {
2320
+ const root = container.getRootNode();
2321
+ const themeEl = root instanceof ShadowRoot ? root.host : document.documentElement;
2322
+ const theme = themeEl instanceof Element ? themeEl.getAttribute("data-theme") : null;
2323
+ return theme === "light" ? QR_PLATE_LIGHT : QR_PLATE_DARK;
2324
+ }
2261
2325
  function renderQR(container, url, size = 200) {
2262
2326
  clearQR(container);
2327
+ container.dataset.qrUrl = url;
2263
2328
  const canvas = document.createElement("canvas");
2264
2329
  const ctx = canvas.getContext?.("2d") ?? null;
2265
2330
  if (!ctx) {
@@ -2283,7 +2348,7 @@ function renderQR(container, url, size = 200) {
2283
2348
  canvas.style.width = `${size}px`;
2284
2349
  canvas.style.height = `${size}px`;
2285
2350
  canvas.style.imageRendering = "pixelated";
2286
- const bgColor = "#e8e8e8";
2351
+ const bgColor = plateColor(container);
2287
2352
  ctx.fillStyle = bgColor;
2288
2353
  ctx.fillRect(0, 0, actualSize, actualSize);
2289
2354
  ctx.fillStyle = "#000000";
@@ -2295,12 +2360,18 @@ function renderQR(container, url, size = 200) {
2295
2360
  }
2296
2361
  }
2297
2362
  const wrapper = document.createElement("div");
2298
- wrapper.style.cssText = "display:inline-block;padding:10px;background:#e8e8e8;border-radius:8px;line-height:0";
2363
+ wrapper.className = "qr-plate";
2364
+ wrapper.style.display = "inline-block";
2365
+ wrapper.style.padding = "10px";
2366
+ wrapper.style.backgroundColor = bgColor;
2367
+ wrapper.style.borderRadius = "8px";
2368
+ wrapper.style.lineHeight = "0";
2299
2369
  wrapper.appendChild(canvas);
2300
2370
  container.appendChild(wrapper);
2301
2371
  }
2302
2372
  function clearQR(container) {
2303
2373
  container.innerHTML = "";
2374
+ delete container.dataset.qrUrl;
2304
2375
  }
2305
2376
 
2306
2377
  // dist/ui/styles.js
@@ -2317,6 +2388,14 @@ body {
2317
2388
  justify-content: center;
2318
2389
  background-image: radial-gradient(ellipse at 50% 0%, rgba(108,71,255,0.08) 0%, transparent 60%);
2319
2390
  }
2391
+ /* The body rule does not apply inside a shadow tree. The host page must
2392
+ give the LoginPage container a height for this centering to show. */
2393
+ :host {
2394
+ display: flex;
2395
+ align-items: center;
2396
+ justify-content: center;
2397
+ min-height: 100%;
2398
+ }
2320
2399
 
2321
2400
  /* Card \u2014 fixed width prevents shrinking when views switch */
2322
2401
  .card {
@@ -2614,19 +2693,25 @@ h1 {
2614
2693
  /* Email section (methods view) */
2615
2694
  .email-section { margin-top: 8px; }
2616
2695
 
2617
- /* Back button */
2696
+ /* Back button \u2014 same top row as Cancel and the locale/theme tools.
2697
+ A float sat inside the 48px content padding, so Back landed below the
2698
+ absolute locale pill. */
2618
2699
  .back-btn {
2700
+ position: absolute;
2701
+ top: 16px;
2702
+ left: 40px;
2619
2703
  display: inline-flex;
2620
2704
  align-items: center;
2621
2705
  gap: 4px;
2706
+ height: 28px;
2622
2707
  background: none;
2623
2708
  border: none;
2624
2709
  color: var(--text-secondary);
2625
2710
  font-size: 14px;
2711
+ line-height: 1;
2626
2712
  cursor: pointer;
2627
2713
  padding: 0;
2628
- margin-bottom: 16px;
2629
- float: left;
2714
+ margin: 0;
2630
2715
  }
2631
2716
  .back-btn:hover { color: var(--text); }
2632
2717
  .back-btn svg { width: 18px; height: 18px; }
@@ -2651,6 +2736,7 @@ h1 {
2651
2736
  line-height: 1;
2652
2737
  text-decoration: none;
2653
2738
  cursor: pointer;
2739
+ height: 28px;
2654
2740
  padding: 2px 4px 2px 0;
2655
2741
  transition: color 0.15s ease;
2656
2742
  }
@@ -2663,7 +2749,6 @@ h1 {
2663
2749
  font-weight: 600;
2664
2750
  color: var(--text-white);
2665
2751
  margin-bottom: 8px;
2666
- clear: both;
2667
2752
  padding-top: 8px;
2668
2753
  }
2669
2754
 
@@ -2837,23 +2922,32 @@ h1 {
2837
2922
  [data-theme="light"] body {
2838
2923
  background-image: radial-gradient(ellipse at 50% 0%, rgba(108,71,255,0.05) 0%, transparent 60%);
2839
2924
  }
2840
- [data-theme="light"] .card {
2925
+ [data-theme="light"] .card,
2926
+ :host([data-theme="light"]) .card {
2841
2927
  box-shadow: var(--shadow-lg), 0 0 80px -20px rgba(108,71,255,0.08);
2842
2928
  }
2843
- [data-theme="light"] .btn:hover {
2929
+ [data-theme="light"] .btn:hover,
2930
+ :host([data-theme="light"]) .btn:hover {
2844
2931
  box-shadow: 0 2px 8px rgba(108,71,255,0.15);
2845
2932
  }
2846
2933
 
2847
- /* Language switcher */
2848
- .locale-switcher {
2934
+ /* Language sits on the same top row as Back / Cancel. */
2935
+ .card-tools {
2849
2936
  position: absolute;
2850
2937
  top: 16px;
2851
2938
  right: 16px;
2939
+ z-index: 2;
2940
+ display: flex;
2941
+ align-items: flex-start;
2942
+ }
2943
+ .locale-switcher {
2944
+ position: relative;
2852
2945
  }
2853
2946
  .locale-btn {
2854
2947
  display: inline-flex;
2855
2948
  align-items: center;
2856
2949
  gap: 4px;
2950
+ height: 28px;
2857
2951
  padding: 4px 10px;
2858
2952
  font-size: 12px;
2859
2953
  color: var(--text-secondary);
@@ -2872,6 +2966,7 @@ h1 {
2872
2966
  display: inline-flex;
2873
2967
  align-items: center;
2874
2968
  gap: 4px;
2969
+ height: 28px;
2875
2970
  padding: 4px 10px;
2876
2971
  font-size: 12px;
2877
2972
  color: var(--text-secondary);
@@ -2901,6 +2996,58 @@ h1 {
2901
2996
  }
2902
2997
  .locale-check { font-size: 11px; line-height: 1; }
2903
2998
  .card { position: relative; }
2999
+
3000
+ /* Long localized method labels remain readable on narrow screens. */
3001
+ .methods-grid { grid-template-columns: 1fr; }
3002
+ .method-btn { min-height: 48px; white-space: normal; }
3003
+ .passkey-alt { min-height: 44px; font-weight: 600; }
3004
+ .oauth-recovery { font-size: 14px; line-height: 1.5; margin: 12px 0; }
3005
+ @media (max-width: 480px) { .card { padding: 24px 20px; width: 100%; box-sizing: border-box; } h1 { font-size: 24px; } }
3006
+
3007
+ .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; }
3008
+ .passkey-create-entry:hover { background: var(--bg-hover); text-decoration: none; }
3009
+ .status:empty { min-height: 0; padding: 0; margin: 0; }
3010
+ .passkey-section .subtitle { margin: 8px 0 16px; font-size: 14px; line-height: 1.5; }
3011
+ @media (max-width: 480px) { .card { padding: 20px 18px; } .brand-header { margin-bottom: 20px; } }
3012
+
3013
+ .card-modal .card-tools { left: 20px; right: 80px; top: 12px; }
3014
+ .card-modal .locale-switcher { min-width: 0; max-width: 100%; }
3015
+ .card-modal .locale-trigger { max-width: 100%; height: 36px; }
3016
+ .locale-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3017
+ .card-modal .locale-menu { position: absolute; top: calc(100% + 8px); left: 0; z-index: 4; margin-top: 0;
3018
+ width: max-content; max-width: min(260px,calc(100vw - 80px)); padding: 6px;
3019
+ background: var(--bg-card); border: 1px solid var(--border-strong); border-radius: 10px; box-shadow: var(--shadow-lg); }
3020
+ .card-modal .locale-menu .locale-btn { min-height: 40px; height: auto; text-align: left; border-radius: 6px; }
3021
+ .card-modal .locale-menu .locale-btn span { overflow-wrap: anywhere; }
3022
+
3023
+ .card { padding: 52px 24px 24px; box-sizing: border-box; }
3024
+ .card-modal { width: 100%; }
3025
+ .card-modal { padding-top: 68px; border: 0; border-radius: 0; }
3026
+ /* The dialog frame owns the shadow in both palettes. */
3027
+ .card.card-modal, :host([data-theme="light"]) .card.card-modal { box-shadow: none; }
3028
+ /* A translateY entrance expands an auto scroller by 4px until it finishes. */
3029
+ .card-modal[data-view] { animation: none; }
3030
+ .card-modal .back-btn { position: static; display: flex; width: max-content; max-width: 100%; margin: 0 0 16px; }
3031
+ .card-modal h1 { display: block; overflow: visible; -webkit-line-clamp: unset; line-clamp: unset; line-height: 1.4; padding-block: 2px; }
3032
+ .card-modal h1[tabindex="-1"]:focus { outline: none; }
3033
+ .card-modal h1, .card-modal h2, .card-modal .subtitle, .card-modal .status { overflow-wrap: anywhere; }
3034
+ .card-modal .btn { height: auto; min-height: 44px; line-height: 1.4; padding-block: 10px; white-space: normal; }
3035
+ .card-modal button, .card-modal a { overflow-wrap: anywhere; }
3036
+ .social-first .divider { margin: 14px 0; }
3037
+ .social-first .app-logo { width: 32px; height: 32px; margin-bottom: 12px; }
3038
+ .social-first .login-footer { margin-top: 16px; padding-top: 12px; }
3039
+ .other-methods { margin-top: 8px; color: var(--text-secondary); font-size: 14px; }
3040
+ .other-methods summary { min-height: 44px; padding: 12px 0; cursor: pointer; }
3041
+ .other-methods summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
3042
+ @media (max-width:480px) { .card { padding: 52px 18px 24px; } }
3043
+ @media (max-width:480px) { .card-modal { padding-top: 68px; } .card-modal .card-tools { left: 18px; } }
3044
+ @media (prefers-reduced-motion: reduce) { .card[data-view], .qr-view { animation: none; } }
3045
+
3046
+ .passkey-creating > .methods-grid, .passkey-creating > .divider, .passkey-creating > .other-methods { display: none; }
3047
+
3048
+ .input-label { display: block; text-align: left; margin: 8px 0; color: var(--text); font-size: 14px; font-weight: 500; }
3049
+ .input-label[hidden], .email-help[hidden] { display: none; }
3050
+ .email-help { text-align: left; font-size: 13px; line-height: 1.5; color: var(--text-secondary); margin: 0 0 12px; }
2904
3051
  `;
2905
3052
 
2906
3053
  // dist/ui/webauthn-browser.js
@@ -2962,7 +3109,7 @@ async function runPasskeyAuth(options) {
2962
3109
  const txtCreating = texts.creating || t("passkeyCreating", locale);
2963
3110
  const txtVerifying = texts.verifying || t("verifying", locale);
2964
3111
  async function fetchAuthChallenge() {
2965
- const res = await fetch(`${prefix}/auth`, { method: "GET" });
3112
+ const res = await fetch(`${prefix}/auth`, { method: "GET", signal: options.signal });
2966
3113
  if (!res.ok)
2967
3114
  throw new Error(t("authFailed", locale));
2968
3115
  return res.json();
@@ -2974,7 +3121,7 @@ async function runPasskeyAuth(options) {
2974
3121
  if (invitationId)
2975
3122
  params.push(`invitationId=${encodeURIComponent(invitationId)}`);
2976
3123
  const qs = params.length ? `?${params.join("&")}` : "";
2977
- const res = await fetch(`${prefix}/register${qs}`, { method: "GET" });
3124
+ const res = await fetch(`${prefix}/register${qs}`, { method: "GET", signal: options.signal });
2978
3125
  if (!res.ok)
2979
3126
  throw new Error(t("registrationFailed", locale));
2980
3127
  return res.json();
@@ -2988,6 +3135,7 @@ async function runPasskeyAuth(options) {
2988
3135
  async function verifyAuth(challengeId, credential) {
2989
3136
  const res = await fetch(`${prefix}/auth`, {
2990
3137
  method: "POST",
3138
+ signal: options.signal,
2991
3139
  headers: { "Content-Type": "application/json" },
2992
3140
  body: JSON.stringify({ challengeId, credential })
2993
3141
  });
@@ -3000,6 +3148,7 @@ async function runPasskeyAuth(options) {
3000
3148
  async function verifyRegister(challengeId, credential, name) {
3001
3149
  const res = await fetch(`${prefix}/register`, {
3002
3150
  method: "POST",
3151
+ signal: options.signal,
3003
3152
  headers: { "Content-Type": "application/json" },
3004
3153
  body: JSON.stringify({ challengeId, credential, name: name || void 0 })
3005
3154
  });
@@ -3016,6 +3165,7 @@ async function runPasskeyAuth(options) {
3016
3165
  const { challengeId } = challengeResult;
3017
3166
  const authentication = challengeResult.authentication || challengeResult;
3018
3167
  const authOpts = {
3168
+ signal: options.signal,
3019
3169
  publicKey: {
3020
3170
  ...authentication,
3021
3171
  challenge: b64urlDecode(authentication.challenge),
@@ -3033,6 +3183,10 @@ async function runPasskeyAuth(options) {
3033
3183
  return { success: false, error: "no_passkey" };
3034
3184
  }
3035
3185
  }
3186
+ if (!raw && authOnly)
3187
+ return { success: false, error: "no_passkey" };
3188
+ if (options.signal?.aborted)
3189
+ return { success: false, error: "cancelled" };
3036
3190
  if (raw) {
3037
3191
  const credential = serializeAuthentication(raw);
3038
3192
  onStatus(txtVerifying, false);
@@ -3046,15 +3200,16 @@ async function runPasskeyAuth(options) {
3046
3200
  return { success: false, error: errMsg, code: errCode };
3047
3201
  }
3048
3202
  }
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
3203
  try {
3204
+ const challengeData = await fetchRegisterChallenge(options.name || "");
3205
+ if (challengeData.registrationAllowed === false) {
3206
+ onStatus(t("regClosed", locale), true);
3207
+ return { success: false, error: "registration_closed" };
3208
+ }
3209
+ onStatus(txtCreating, false);
3056
3210
  const registration = challengeData.registration || challengeData;
3057
3211
  const regOpts = {
3212
+ signal: options.signal,
3058
3213
  publicKey: {
3059
3214
  ...registration,
3060
3215
  challenge: b64urlDecode(registration.challenge),
@@ -3069,12 +3224,14 @@ async function runPasskeyAuth(options) {
3069
3224
  }
3070
3225
  };
3071
3226
  const raw = await navigator.credentials.create(regOpts);
3227
+ if (!raw || options.signal?.aborted)
3228
+ return { success: false, error: t("passkeyNoLocal", locale) };
3072
3229
  const credential = serializeRegistration(raw);
3073
3230
  onStatus(txtVerifying, false);
3074
3231
  await verifyRegister(challengeData.challengeId, credential, options.name || "");
3075
3232
  return { success: true };
3076
3233
  } catch (regErr) {
3077
- const errMsg = regErr instanceof Error ? regErr.message : t("passkeySetupFailed", locale);
3234
+ const errMsg = regErr instanceof Error && (regErr.name === "NotAllowedError" || regErr.name === "AbortError") ? t("passkeyNoLocal", locale) : regErr instanceof Error ? regErr.message : t("passkeySetupFailed", locale);
3078
3235
  const errCode = regErr instanceof Error ? regErr.code : void 0;
3079
3236
  onStatus(errMsg, true);
3080
3237
  return { success: false, error: errMsg, code: errCode };
@@ -3113,15 +3270,42 @@ var LoginPage = class {
3113
3270
  tokenSession = null;
3114
3271
  styleEl = null;
3115
3272
  mounted = false;
3273
+ completed = false;
3274
+ generation = 0;
3275
+ activeMethod;
3276
+ activeProvider;
3277
+ oauthCleanup;
3278
+ emailTimer;
3279
+ emailDraft = "";
3280
+ abort;
3281
+ emit(name) {
3282
+ try {
3283
+ this.options.onEvent?.({ name, method: this.activeMethod, mode: this.options.mode ?? "page" });
3284
+ } catch {
3285
+ }
3286
+ }
3287
+ select(method) {
3288
+ this.generation++;
3289
+ this.oauthCleanup?.();
3290
+ this.abort?.abort();
3291
+ this.abort = new AbortController();
3292
+ this.activeMethod = method;
3293
+ this.activeProvider = void 0;
3294
+ this.emit("method_selected");
3295
+ }
3116
3296
  currentView = "methods";
3117
3297
  cardEl = null;
3118
3298
  mediaQuery = null;
3119
3299
  mediaListener = null;
3120
3300
  localeOutsideClick = null;
3301
+ /** Open shadow root when isolate is set. Reused across destroy/mount. */
3302
+ shadow = null;
3303
+ isolate;
3121
3304
  constructor(container, config, http, options) {
3122
3305
  this.container = container;
3123
3306
  this.config = config;
3124
3307
  this.http = http;
3308
+ this.isolate = options?.isolate === true;
3125
3309
  this.options = {
3126
3310
  methods: ["passkey", "did-connect"],
3127
3311
  locale: "en",
@@ -3142,32 +3326,50 @@ var LoginPage = class {
3142
3326
  if (this.mounted)
3143
3327
  return;
3144
3328
  this.mounted = true;
3329
+ this.completed = false;
3330
+ this.emit("view");
3145
3331
  this.styleEl = document.createElement("style");
3146
3332
  this.styleEl.textContent = LOGIN_PAGE_STYLES;
3147
- this.container.appendChild(this.styleEl);
3333
+ this.mountRoot().appendChild(this.styleEl);
3334
+ this.adoptStoredTheme();
3148
3335
  this.applyTheme();
3149
3336
  const available = this.filterMethods(this.options.methods);
3150
3337
  this.renderCard(available);
3151
3338
  }
3152
3339
  /** Destroy the login UI, clean up timers and listeners. Idempotent. */
3153
3340
  destroy() {
3341
+ this.generation++;
3342
+ this.oauthCleanup?.();
3343
+ this.abort?.abort();
3344
+ clearInterval(this.emailTimer);
3154
3345
  this.detachLocaleMenu();
3155
3346
  if (this.tokenSession) {
3156
3347
  this.tokenSession.destroy();
3157
3348
  this.tokenSession = null;
3158
3349
  }
3159
- if (this.mediaQuery && this.mediaListener) {
3160
- this.mediaQuery.removeEventListener("change", this.mediaListener);
3161
- this.mediaQuery = null;
3162
- this.mediaListener = null;
3350
+ this.detachMedia();
3351
+ if (this.isolate) {
3352
+ const shadow = this.shadow ?? this.container.shadowRoot;
3353
+ if (shadow)
3354
+ shadow.innerHTML = "";
3355
+ this.container.removeAttribute("data-theme");
3356
+ } else {
3357
+ document.documentElement.removeAttribute("data-theme");
3358
+ this.container.innerHTML = "";
3163
3359
  }
3164
- document.documentElement.removeAttribute("data-theme");
3165
- this.container.innerHTML = "";
3166
3360
  this.styleEl = null;
3167
3361
  this.cardEl = null;
3168
3362
  this.currentView = "methods";
3169
3363
  this.mounted = false;
3170
3364
  }
3365
+ /**
3366
+ * Apply a light or dark theme without remounting.
3367
+ * The host writes `web-mode` itself. Isolate updates the shadow host only.
3368
+ */
3369
+ setTheme(theme) {
3370
+ this.options.theme = theme;
3371
+ this.applyTheme();
3372
+ }
3171
3373
  /** Update options and re-render. */
3172
3374
  update(newOptions) {
3173
3375
  Object.assign(this.options, newOptions);
@@ -3177,24 +3379,82 @@ var LoginPage = class {
3177
3379
  }
3178
3380
  }
3179
3381
  // ─── Private: theme ────────────────────────────────────────────────
3382
+ /** Light DOM themes <html>. Isolate themes the shadow host only. */
3383
+ themeTarget() {
3384
+ return this.isolate ? this.container : document.documentElement;
3385
+ }
3386
+ /** Shadow root created once. attachShadow throws if called again. */
3387
+ mountRoot() {
3388
+ if (!this.isolate)
3389
+ return this.container;
3390
+ if (this.shadow)
3391
+ return this.shadow;
3392
+ this.shadow = this.container.shadowRoot ?? this.container.attachShadow({ mode: "open" });
3393
+ return this.shadow;
3394
+ }
3395
+ detachMedia() {
3396
+ if (this.mediaQuery && this.mediaListener) {
3397
+ this.mediaQuery.removeEventListener("change", this.mediaListener);
3398
+ }
3399
+ this.mediaQuery = null;
3400
+ this.mediaListener = null;
3401
+ }
3402
+ /**
3403
+ * Same order as theme/init.js: `?theme=` is a preview and wins;
3404
+ * otherwise `localStorage["web-mode"]`. Neither write happens here.
3405
+ */
3406
+ adoptStoredTheme() {
3407
+ if (this.options.mode === "dialog")
3408
+ return;
3409
+ if (typeof window === "undefined")
3410
+ return;
3411
+ const urlTheme = new URLSearchParams(window.location.search).get("theme");
3412
+ if (urlTheme === "light" || urlTheme === "dark" || urlTheme === "auto") {
3413
+ this.options.theme = urlTheme;
3414
+ return;
3415
+ }
3416
+ try {
3417
+ const stored = localStorage.getItem("web-mode");
3418
+ if (stored === "light" || stored === "dark" || stored === "auto")
3419
+ this.options.theme = stored;
3420
+ } catch {
3421
+ }
3422
+ }
3423
+ /** light sets data-theme. dark removes it. auto follows the OS. */
3180
3424
  applyTheme() {
3425
+ this.detachMedia();
3181
3426
  const theme = this.options.theme ?? "dark";
3427
+ const target = this.themeTarget();
3182
3428
  if (theme === "light") {
3183
- document.documentElement.dataset.theme = "light";
3429
+ target.dataset.theme = "light";
3184
3430
  } else if (theme === "auto" && typeof window.matchMedia === "function") {
3185
3431
  const mq = window.matchMedia("(prefers-color-scheme: light)");
3186
3432
  this.mediaQuery = mq;
3187
3433
  const apply = (light) => {
3188
- if (light) {
3189
- document.documentElement.dataset.theme = "light";
3190
- } else {
3191
- document.documentElement.removeAttribute("data-theme");
3192
- }
3434
+ if (light)
3435
+ target.dataset.theme = "light";
3436
+ else
3437
+ target.removeAttribute("data-theme");
3438
+ this.repaintQRPlate();
3193
3439
  };
3194
3440
  apply(mq.matches);
3195
3441
  this.mediaListener = (e) => apply(e.matches);
3196
3442
  mq.addEventListener("change", this.mediaListener);
3443
+ return;
3444
+ } else {
3445
+ target.removeAttribute("data-theme");
3197
3446
  }
3447
+ this.repaintQRPlate();
3448
+ }
3449
+ /** Redraw a visible QR so its plate matches the theme. Leaves a timeout overlay alone. */
3450
+ repaintQRPlate() {
3451
+ const qr = this.mountRoot().querySelector("#did-connect-qr");
3452
+ if (!(qr instanceof HTMLElement))
3453
+ return;
3454
+ const url = qr.dataset.qrUrl;
3455
+ if (!url || !qr.querySelector("canvas") || qr.querySelector(".qr-timeout-overlay"))
3456
+ return;
3457
+ renderQR(qr, url);
3198
3458
  }
3199
3459
  // ─── Private: rendering ──────────────────────────────────────────────
3200
3460
  filterMethods(methods) {
@@ -3207,11 +3467,18 @@ var LoginPage = class {
3207
3467
  }
3208
3468
  renderCard(methods) {
3209
3469
  const card = document.createElement("div");
3210
- card.className = "card";
3470
+ card.className = this.options.mode === "dialog" ? "card card-modal" : "card";
3211
3471
  this.cardEl = card;
3212
- this.container.appendChild(card);
3472
+ this.mountRoot().appendChild(card);
3213
3473
  this.switchView("methods", methods);
3214
3474
  }
3475
+ /** Locale sits on the card's top-right row, level with Back / Cancel. */
3476
+ renderCardTools(card) {
3477
+ const tools = document.createElement("div");
3478
+ tools.className = "card-tools";
3479
+ this.renderLocaleSwitcher(tools);
3480
+ card.appendChild(tools);
3481
+ }
3215
3482
  renderLocaleSwitcher(card) {
3216
3483
  const passed = this.options.locales;
3217
3484
  if (passed) {
@@ -3250,7 +3517,7 @@ var LoginPage = class {
3250
3517
  trigger.className = "locale-trigger";
3251
3518
  trigger.setAttribute("aria-haspopup", "menu");
3252
3519
  trigger.setAttribute("aria-expanded", "false");
3253
- trigger.innerHTML = `${ICON_GLOBE} ${escapeHtml(current.label)}`;
3520
+ trigger.innerHTML = `${ICON_GLOBE} <span class="locale-label">${escapeHtml(current.label)}</span>`;
3254
3521
  trigger.title = current.label;
3255
3522
  const menu = document.createElement("div");
3256
3523
  menu.className = "locale-menu";
@@ -3291,8 +3558,12 @@ var LoginPage = class {
3291
3558
  menu.appendChild(btn);
3292
3559
  }
3293
3560
  const onOutsideClick = (event) => {
3294
- if (!wrapper.contains(event.target))
3295
- setOpen(false);
3561
+ const path = event.composedPath();
3562
+ if (path.includes(wrapper))
3563
+ return;
3564
+ if (this.isolate && path.includes(this.container))
3565
+ return;
3566
+ setOpen(false);
3296
3567
  };
3297
3568
  document.addEventListener("click", onOutsideClick);
3298
3569
  this.localeOutsideClick = onOutsideClick;
@@ -3316,10 +3587,14 @@ var LoginPage = class {
3316
3587
  this.tokenSession = null;
3317
3588
  }
3318
3589
  }
3590
+ this.generation++;
3591
+ this.oauthCleanup?.();
3592
+ this.abort?.abort();
3593
+ clearInterval(this.emailTimer);
3319
3594
  this.currentView = view;
3320
3595
  this.cardEl.innerHTML = "";
3321
3596
  this.cardEl.dataset.view = view;
3322
- this.renderLocaleSwitcher(this.cardEl);
3597
+ this.renderCardTools(this.cardEl);
3323
3598
  switch (view) {
3324
3599
  case "methods":
3325
3600
  this.renderMethodsView(this.cardEl, locale, methods ?? this.filterMethods(this.options.methods));
@@ -3333,8 +3608,24 @@ var LoginPage = class {
3333
3608
  }
3334
3609
  }
3335
3610
  renderMethodsView(card, locale, methods) {
3336
- this.renderHomeLink(card, locale);
3611
+ if (this.options.mode !== "dialog")
3612
+ this.renderHomeLink(card, locale);
3337
3613
  this.renderBrandHeader(card, locale);
3614
+ if (this.options.preferenceKey) {
3615
+ try {
3616
+ const last = localStorage.getItem(`did-login-method:${this.options.preferenceKey}`);
3617
+ const labels = { passkey: "Passkey", oauth: "OAuth", email: t("emailButton", locale), "did-connect": "DID Wallet" };
3618
+ const provider = last?.startsWith("oauth:") ? this.options.oauthProviders?.find((p) => p.enabled && `oauth:${p.id}` === last) : void 0;
3619
+ const label = provider?.name ?? (last ? labels[last] : void 0);
3620
+ if (last && label && (methods.includes(last) || provider && methods.includes("oauth"))) {
3621
+ const hint = document.createElement("p");
3622
+ hint.className = "subtitle";
3623
+ hint.textContent = t("lastUsed", locale, { method: label });
3624
+ card.appendChild(hint);
3625
+ }
3626
+ } catch {
3627
+ }
3628
+ }
3338
3629
  const requestedPasskey = this.options.methods.includes("passkey");
3339
3630
  if (requestedPasskey && !methods.includes("passkey")) {
3340
3631
  const warn = document.createElement("div");
@@ -3342,6 +3633,12 @@ var LoginPage = class {
3342
3633
  warn.textContent = t("passkeyUnsupported", locale);
3343
3634
  card.appendChild(warn);
3344
3635
  }
3636
+ const socialFirst = this.options.preferredLayout !== "passkey-first" && methods.includes("oauth") && this.options.oauthProviders?.some((p) => p.enabled && ["google", "apple"].includes(p.id));
3637
+ card.classList.toggle("social-first", !!socialFirst);
3638
+ if (socialFirst) {
3639
+ this.renderSecondaryGrid(card, locale, ["oauth"], "primary");
3640
+ this.renderDivider(card, locale);
3641
+ }
3345
3642
  const hasPrimary = methods.includes("passkey");
3346
3643
  if (hasPrimary) {
3347
3644
  this.renderPasskeySection(card, locale);
@@ -3351,7 +3648,20 @@ var LoginPage = class {
3351
3648
  this.renderDivider(card, locale);
3352
3649
  }
3353
3650
  if (secondary.length > 0) {
3354
- this.renderSecondaryGrid(card, locale, methods);
3651
+ if (socialFirst) {
3652
+ if (methods.includes("email"))
3653
+ this.renderSecondaryGrid(card, locale, ["email"]);
3654
+ const details = document.createElement("details");
3655
+ details.className = "other-methods";
3656
+ const summary = document.createElement("summary");
3657
+ summary.textContent = t("otherMethods", locale);
3658
+ details.appendChild(summary);
3659
+ this.renderSecondaryGrid(details, locale, methods.filter((m) => m !== "email"), "secondary");
3660
+ if (details.querySelector("button"))
3661
+ card.appendChild(details);
3662
+ } else {
3663
+ this.renderSecondaryGrid(card, locale, methods);
3664
+ }
3355
3665
  }
3356
3666
  if (methods.length === 0 && !requestedPasskey) {
3357
3667
  const msg = document.createElement("p");
@@ -3378,10 +3688,14 @@ var LoginPage = class {
3378
3688
  walletBtn.id = "did-wallet-open-btn";
3379
3689
  walletBtn.textContent = t("openInWallet", locale);
3380
3690
  walletBtn.disabled = true;
3381
- card.appendChild(walletBtn);
3691
+ if (isMobile())
3692
+ card.insertBefore(walletBtn, qrContainer);
3693
+ else
3694
+ card.appendChild(walletBtn);
3382
3695
  const statusEl = document.createElement("div");
3383
3696
  statusEl.id = "did-connect-status";
3384
3697
  statusEl.className = "status";
3698
+ statusEl.setAttribute("role", "status");
3385
3699
  card.appendChild(statusEl);
3386
3700
  this.renderTrustFooter(card, locale);
3387
3701
  this.startDIDConnectInView(statusEl, qrContainer, walletBtn, locale);
@@ -3393,9 +3707,26 @@ var LoginPage = class {
3393
3707
  title.className = "view-title";
3394
3708
  title.textContent = t("emailTitle", locale);
3395
3709
  card.appendChild(title);
3710
+ const emailLabel = document.createElement("label");
3711
+ emailLabel.className = "input-label";
3712
+ emailLabel.htmlFor = "login-email-address";
3713
+ emailLabel.textContent = t("emailPlaceholder", locale);
3714
+ card.appendChild(emailLabel);
3715
+ const emailHelp = document.createElement("p");
3716
+ emailHelp.className = "email-help";
3717
+ emailHelp.textContent = t("emailExplanation", locale);
3718
+ card.appendChild(emailHelp);
3396
3719
  const emailInput = document.createElement("input");
3397
3720
  emailInput.className = "input";
3721
+ emailInput.id = "login-email-address";
3398
3722
  emailInput.type = "email";
3723
+ emailInput.required = true;
3724
+ emailInput.autocomplete = "email";
3725
+ emailInput.value = this.emailDraft;
3726
+ emailInput.setAttribute("aria-label", t("emailPlaceholder", locale));
3727
+ emailInput.addEventListener("input", () => {
3728
+ this.emailDraft = emailInput.value;
3729
+ });
3399
3730
  emailInput.placeholder = t("emailPlaceholder", locale);
3400
3731
  card.appendChild(emailInput);
3401
3732
  const sendBtn = document.createElement("button");
@@ -3404,15 +3735,23 @@ var LoginPage = class {
3404
3735
  card.appendChild(sendBtn);
3405
3736
  const status = document.createElement("div");
3406
3737
  status.className = "status";
3738
+ status.setAttribute("role", "status");
3407
3739
  card.appendChild(status);
3408
3740
  const codeSection = document.createElement("div");
3409
3741
  codeSection.className = "email-code-section";
3410
3742
  codeSection.style.display = "none";
3743
+ const codeLabel = document.createElement("label");
3744
+ codeLabel.className = "input-label";
3745
+ codeLabel.htmlFor = "login-email-code";
3746
+ codeLabel.textContent = t("emailCodePlaceholder", locale);
3747
+ codeSection.appendChild(codeLabel);
3411
3748
  const codeInput = document.createElement("input");
3749
+ codeInput.id = "login-email-code";
3412
3750
  codeInput.className = "input";
3413
3751
  codeInput.type = "text";
3414
3752
  codeInput.inputMode = "numeric";
3415
3753
  codeInput.maxLength = 8;
3754
+ codeInput.setAttribute("aria-label", t("emailCodePlaceholder", locale));
3416
3755
  codeInput.autocomplete = "one-time-code";
3417
3756
  codeInput.placeholder = t("emailCodePlaceholder", locale);
3418
3757
  codeSection.appendChild(codeInput);
@@ -3427,6 +3766,27 @@ var LoginPage = class {
3427
3766
  codeSection.appendChild(resendBtn);
3428
3767
  card.insertBefore(codeSection, status);
3429
3768
  this.renderTrustFooter(card, locale);
3769
+ const epoch = this.generation;
3770
+ const isCurrent = () => this.mounted && epoch === this.generation;
3771
+ const editBtn = document.createElement("button");
3772
+ editBtn.className = "passkey-alt";
3773
+ editBtn.textContent = t("emailEdit", locale);
3774
+ codeSection.appendChild(editBtn);
3775
+ editBtn.addEventListener("click", () => this.switchView("email"));
3776
+ const cooldown = () => {
3777
+ let remaining = 60;
3778
+ clearInterval(this.emailTimer);
3779
+ resendBtn.disabled = true;
3780
+ resendBtn.textContent = t("emailCooldown", locale, { seconds: String(remaining) });
3781
+ this.emailTimer = setInterval(() => {
3782
+ remaining--;
3783
+ resendBtn.textContent = remaining > 0 ? t("emailCooldown", locale, { seconds: String(remaining) }) : t("emailResend", locale);
3784
+ if (remaining <= 0) {
3785
+ clearInterval(this.emailTimer);
3786
+ resendBtn.disabled = false;
3787
+ }
3788
+ }, 1e3);
3789
+ };
3430
3790
  let currentEmail = "";
3431
3791
  let currentCodeId;
3432
3792
  const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
@@ -3434,7 +3794,7 @@ var LoginPage = class {
3434
3794
  if (sendBtn.disabled)
3435
3795
  return;
3436
3796
  const email = emailInput.value.trim();
3437
- if (!email?.includes("@")) {
3797
+ if (!emailInput.checkValidity()) {
3438
3798
  status.textContent = t("emailInvalid", locale);
3439
3799
  status.className = "status error";
3440
3800
  return;
@@ -3442,11 +3802,17 @@ var LoginPage = class {
3442
3802
  sendBtn.disabled = true;
3443
3803
  status.textContent = t("emailSending", locale);
3444
3804
  status.className = "status";
3805
+ status.setAttribute("role", "status");
3445
3806
  try {
3446
3807
  const sendResult = await this.http.post(`${servicePrefix}/api/email/sendCode`, { email, locale });
3808
+ if (!isCurrent())
3809
+ return;
3810
+ cooldown();
3447
3811
  currentEmail = email;
3448
3812
  currentCodeId = sendResult?.id;
3449
3813
  emailInput.style.display = "none";
3814
+ emailLabel.hidden = true;
3815
+ emailHelp.hidden = true;
3450
3816
  sendBtn.style.display = "none";
3451
3817
  codeSection.style.display = "";
3452
3818
  title.textContent = t("emailVerify", locale);
@@ -3454,6 +3820,8 @@ var LoginPage = class {
3454
3820
  status.className = "status";
3455
3821
  codeInput.focus();
3456
3822
  } catch (err) {
3823
+ if (!isCurrent())
3824
+ return;
3457
3825
  const msg = err instanceof Error ? err.message : "Failed to send email";
3458
3826
  status.textContent = msg;
3459
3827
  status.className = "status error";
@@ -3464,7 +3832,7 @@ var LoginPage = class {
3464
3832
  if (verifyBtn.disabled)
3465
3833
  return;
3466
3834
  const code = codeInput.value.trim();
3467
- if (!code || code.length !== 8) {
3835
+ if (!/^\d{8}$/.test(code)) {
3468
3836
  status.textContent = t("emailCodeInvalid", locale);
3469
3837
  status.className = "status error";
3470
3838
  return;
@@ -3474,10 +3842,14 @@ var LoginPage = class {
3474
3842
  status.className = "status";
3475
3843
  try {
3476
3844
  await this.http.post(`${servicePrefix}/api/email/login`, { code, id: currentCodeId });
3845
+ if (!isCurrent())
3846
+ return;
3477
3847
  status.textContent = t("success", locale);
3478
3848
  status.className = "status";
3479
3849
  this.handleSuccess();
3480
3850
  } catch (err) {
3851
+ if (!isCurrent())
3852
+ return;
3481
3853
  if (err instanceof Error && "code" in err && err.code === "FORBIDDEN") {
3482
3854
  this.showFullError(friendlyError(err, locale));
3483
3855
  } else {
@@ -3500,20 +3872,27 @@ var LoginPage = class {
3500
3872
  verifyCode();
3501
3873
  });
3502
3874
  resendBtn.addEventListener("click", async () => {
3875
+ if (resendBtn.disabled)
3876
+ return;
3503
3877
  resendBtn.disabled = true;
3504
3878
  status.textContent = t("emailSending", locale);
3505
3879
  status.className = "status";
3506
3880
  try {
3507
3881
  const resendResult = await this.http.post(`${servicePrefix}/api/email/sendCode`, { email: currentEmail, locale });
3882
+ if (!isCurrent())
3883
+ return;
3884
+ cooldown();
3508
3885
  currentCodeId = resendResult?.id;
3509
3886
  status.textContent = t("emailSent", locale, { email: currentEmail });
3510
3887
  status.className = "status";
3511
3888
  } catch (err) {
3889
+ if (!isCurrent())
3890
+ return;
3891
+ resendBtn.disabled = false;
3512
3892
  const msg = err instanceof Error ? err.message : "Failed to resend";
3513
3893
  status.textContent = msg;
3514
3894
  status.className = "status error";
3515
3895
  }
3516
- resendBtn.disabled = false;
3517
3896
  });
3518
3897
  }
3519
3898
  renderBackButton(parent, locale) {
@@ -3535,6 +3914,8 @@ var LoginPage = class {
3535
3914
  * sub-view back buttons that mean "return to the method list".
3536
3915
  */
3537
3916
  renderHomeLink(parent, locale) {
3917
+ if (this.options.dismissible === false)
3918
+ return;
3538
3919
  const { homeUrl, onCancel } = this.options;
3539
3920
  if (!onCancel && !homeUrl)
3540
3921
  return;
@@ -3544,7 +3925,10 @@ var LoginPage = class {
3544
3925
  btn.type = "button";
3545
3926
  btn.className = "home-link";
3546
3927
  btn.innerHTML = label;
3547
- btn.addEventListener("click", () => this.options.onCancel?.());
3928
+ btn.addEventListener("click", () => {
3929
+ this.emit("cancel");
3930
+ this.options.onCancel?.();
3931
+ });
3548
3932
  parent.appendChild(btn);
3549
3933
  } else {
3550
3934
  const link = document.createElement("a");
@@ -3572,7 +3956,16 @@ var LoginPage = class {
3572
3956
  } else {
3573
3957
  h1.textContent = t("signIn", locale);
3574
3958
  }
3959
+ if (this.options.purpose)
3960
+ h1.textContent = this.options.purpose.title;
3961
+ h1.id = "login-title";
3575
3962
  header.appendChild(h1);
3963
+ if (this.options.purpose?.description) {
3964
+ const description = document.createElement("p");
3965
+ description.className = "subtitle";
3966
+ description.textContent = this.options.purpose.description;
3967
+ header.appendChild(description);
3968
+ }
3576
3969
  if (this.options.masterAppName) {
3577
3970
  const sub = document.createElement("p");
3578
3971
  sub.className = "federated-hint";
@@ -3613,6 +4006,12 @@ var LoginPage = class {
3613
4006
  }
3614
4007
  footer.appendChild(links);
3615
4008
  }
4009
+ if (this.currentView !== "methods" && this.options.mode !== "dialog") {
4010
+ this.renderHomeLink(footer, locale);
4011
+ const cancel = footer.querySelector(".home-link");
4012
+ if (cancel)
4013
+ cancel.style.cssText = "position:static;display:inline-flex;margin-top:12px";
4014
+ }
3616
4015
  parent.appendChild(footer);
3617
4016
  }
3618
4017
  renderDivider(parent, locale) {
@@ -3673,14 +4072,24 @@ var LoginPage = class {
3673
4072
  retryBtn.addEventListener("click", () => doSignIn());
3674
4073
  const createLink = document.createElement("button");
3675
4074
  createLink.type = "button";
3676
- createLink.className = "passkey-alt";
4075
+ createLink.className = "passkey-alt passkey-create-entry";
3677
4076
  createLink.textContent = t("passkeyCreateInstead", locale);
3678
4077
  section.appendChild(createLink);
3679
4078
  createLink.addEventListener("click", () => renderCreateState());
3680
4079
  };
3681
4080
  const renderCreateState = () => {
4081
+ this.cardEl?.classList.add("passkey-creating");
4082
+ this.abort?.abort();
4083
+ this.generation++;
3682
4084
  section.innerHTML = "";
3683
4085
  clearStatus();
4086
+ const heading = this.cardEl?.querySelector("h1");
4087
+ if (heading)
4088
+ heading.textContent = t("passkeyCreateAccount", locale);
4089
+ const explanation = document.createElement("p");
4090
+ explanation.className = "subtitle";
4091
+ explanation.textContent = t("passkeyExplanation", locale);
4092
+ section.appendChild(explanation);
3684
4093
  const nameRow = document.createElement("div");
3685
4094
  nameRow.className = "name-row visible";
3686
4095
  nameRow.id = "passkey-name-row";
@@ -3688,6 +4097,7 @@ var LoginPage = class {
3688
4097
  nameInput.className = "input";
3689
4098
  nameInput.id = "passkey-name-input";
3690
4099
  nameInput.type = "text";
4100
+ nameInput.setAttribute("aria-label", t("namePlaceholder", locale));
3691
4101
  nameInput.placeholder = t("namePlaceholder", locale);
3692
4102
  nameInput.autocomplete = "name";
3693
4103
  nameRow.appendChild(nameInput);
@@ -3706,8 +4116,11 @@ var LoginPage = class {
3706
4116
  if (createBtn.disabled)
3707
4117
  return;
3708
4118
  createBtn.disabled = true;
4119
+ this.select("passkey");
4120
+ const epoch = this.generation;
3709
4121
  const result = await runPasskeyAuth({
3710
4122
  registerOnly: true,
4123
+ signal: this.abort?.signal,
3711
4124
  name: nameInput.value.trim(),
3712
4125
  onStatus: setStatus,
3713
4126
  texts: statusTexts,
@@ -3715,6 +4128,8 @@ var LoginPage = class {
3715
4128
  prefix: this.options.passkeyPrefix,
3716
4129
  invitationId: this.options.invitationId
3717
4130
  });
4131
+ if (!this.mounted || epoch !== this.generation || this.abort?.signal.aborted)
4132
+ return;
3718
4133
  if (handleResult(result))
3719
4134
  return;
3720
4135
  if (result.error)
@@ -3727,20 +4142,27 @@ var LoginPage = class {
3727
4142
  doCreate();
3728
4143
  });
3729
4144
  backLink.addEventListener("click", () => renderSignInState());
3730
- nameInput.focus();
4145
+ createBtn.focus();
3731
4146
  };
3732
4147
  const doSignIn = async () => {
3733
4148
  const btn = section.querySelector("#passkey-btn");
4149
+ if (btn?.disabled)
4150
+ return;
3734
4151
  if (btn)
3735
4152
  btn.disabled = true;
4153
+ this.select("passkey");
4154
+ const epoch = this.generation;
3736
4155
  const result = await runPasskeyAuth({
3737
4156
  authOnly: true,
4157
+ signal: this.abort?.signal,
3738
4158
  onStatus: setStatus,
3739
4159
  texts: statusTexts,
3740
4160
  locale,
3741
4161
  prefix: this.options.passkeyPrefix,
3742
4162
  invitationId: this.options.invitationId
3743
4163
  });
4164
+ if (!this.mounted || epoch !== this.generation || this.abort?.signal.aborted)
4165
+ return;
3744
4166
  if (handleResult(result))
3745
4167
  return;
3746
4168
  if (result.error && result.error !== "no_passkey") {
@@ -3749,20 +4171,27 @@ var LoginPage = class {
3749
4171
  btn.disabled = false;
3750
4172
  return;
3751
4173
  }
4174
+ this.emit("incomplete");
3752
4175
  renderCrossDeviceState();
3753
4176
  };
3754
4177
  const renderSignInState = () => {
4178
+ this.cardEl?.classList.remove("passkey-creating");
4179
+ this.abort?.abort();
4180
+ this.generation++;
4181
+ const heading = this.cardEl?.querySelector("h1");
4182
+ if (heading)
4183
+ heading.textContent = this.options.purpose?.title ?? (this.options.appName ? t("signInTo", locale, { appName: this.options.appName }) : t("signIn", locale));
3755
4184
  section.innerHTML = "";
3756
4185
  clearStatus();
3757
4186
  const signInBtn = document.createElement("button");
3758
- signInBtn.className = "btn";
4187
+ signInBtn.className = this.cardEl?.classList.contains("social-first") ? "btn btn-secondary" : "btn";
3759
4188
  signInBtn.id = "passkey-btn";
3760
4189
  signInBtn.innerHTML = `${ICON_PASSKEY} ${escapeHtml(t("passkeySignIn", locale))}`;
3761
4190
  section.appendChild(signInBtn);
3762
4191
  signInBtn.addEventListener("click", () => doSignIn());
3763
4192
  const createLink = document.createElement("button");
3764
4193
  createLink.type = "button";
3765
- createLink.className = "passkey-alt";
4194
+ createLink.className = "passkey-alt passkey-create-entry";
3766
4195
  createLink.textContent = t("passkeyFirstTime", locale);
3767
4196
  section.appendChild(createLink);
3768
4197
  createLink.addEventListener("click", () => renderCreateState());
@@ -3777,6 +4206,8 @@ var LoginPage = class {
3777
4206
  // methods now render through renderSecondaryGrid)
3778
4207
  // ─── Private: OAuth popup flow ──────────────────────────────────────
3779
4208
  startOAuthPopup(providerId, btn) {
4209
+ this.oauthCleanup?.();
4210
+ const epoch = this.generation;
3780
4211
  const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
3781
4212
  const oauthBase = this.options.masterOAuthOrigin ? `${this.options.masterOAuthOrigin}${servicePrefix}` : servicePrefix;
3782
4213
  const oauthOrigin = new URL(oauthBase, window.location.href).origin;
@@ -3787,14 +4218,32 @@ var LoginPage = class {
3787
4218
  const top = window.screenY + (window.innerHeight - height) / 2;
3788
4219
  const popup = window.open(loginUrl, "oauth-login:popup", `left=${left},top=${top},width=${width},height=${height},resizable,scrollbars=yes,status=1,popup`);
3789
4220
  if (!popup) {
4221
+ this.options.onBeforeRedirect?.();
3790
4222
  window.location.href = loginUrl;
3791
4223
  return;
3792
4224
  }
3793
4225
  const originalHTML = btn.innerHTML;
3794
- btn.innerHTML = `<span class="oauth-spinner"></span> Authorizing...`;
4226
+ btn.textContent = t("oauthWaiting", this.options.locale, { provider: this.options.oauthProviders?.find((p) => p.id === providerId)?.name ?? providerId });
3795
4227
  btn.disabled = true;
3796
4228
  btn.classList.add("oauth-btn-loading");
4229
+ const recovery = document.createElement("div");
4230
+ recovery.className = "oauth-recovery";
4231
+ recovery.setAttribute("role", "status");
4232
+ const hint = document.createElement("p");
4233
+ hint.textContent = t("oauthIncomplete", this.options.locale);
4234
+ const fullPage = document.createElement("a");
4235
+ fullPage.href = loginUrl;
4236
+ fullPage.textContent = t("oauthFullPage", this.options.locale);
4237
+ fullPage.addEventListener("click", () => {
4238
+ this.options.onBeforeRedirect?.();
4239
+ this.oauthCleanup?.();
4240
+ });
4241
+ recovery.append(hint, fullPage);
4242
+ recovery.hidden = true;
4243
+ btn.parentElement?.after(recovery);
3797
4244
  const restore = () => {
4245
+ recovery.hidden = false;
4246
+ this.emit("incomplete");
3798
4247
  btn.innerHTML = originalHTML;
3799
4248
  btn.disabled = false;
3800
4249
  btn.classList.remove("oauth-btn-loading");
@@ -3802,19 +4251,19 @@ var LoginPage = class {
3802
4251
  let settled = false;
3803
4252
  let sessionPollTimer;
3804
4253
  const succeed = () => {
3805
- if (settled)
4254
+ if (settled || !this.mounted || epoch !== this.generation)
3806
4255
  return;
3807
4256
  settled = true;
3808
4257
  cleanup();
3809
- btn.innerHTML = `<span class="oauth-check">\u2713</span> Success`;
4258
+ btn.textContent = t("success", this.options.locale);
3810
4259
  this.handleSuccess();
3811
4260
  };
3812
4261
  const onMessage = async (event) => {
3813
- if (event.origin !== oauthOrigin)
4262
+ if (event.origin !== oauthOrigin || event.source !== popup)
3814
4263
  return;
3815
4264
  if (!event.data || event.data.type !== "authorization_response")
3816
4265
  return;
3817
- if (settled)
4266
+ if (settled || !this.mounted || epoch !== this.generation)
3818
4267
  return;
3819
4268
  if (pollTimer !== void 0) {
3820
4269
  clearInterval(pollTimer);
@@ -3835,7 +4284,7 @@ var LoginPage = class {
3835
4284
  cleanup();
3836
4285
  return;
3837
4286
  }
3838
- btn.innerHTML = `<span class="oauth-spinner"></span> Signing in...`;
4287
+ btn.textContent = t("verifying", this.options.locale);
3839
4288
  btn.disabled = true;
3840
4289
  try {
3841
4290
  const oauthBody = { provider: providerId, code, state };
@@ -3844,6 +4293,8 @@ var LoginPage = class {
3844
4293
  await this.http.post(`${servicePrefix}/api/oauth/login`, oauthBody);
3845
4294
  succeed();
3846
4295
  } catch (oauthErr) {
4296
+ if (settled || !this.mounted || epoch !== this.generation)
4297
+ return;
3847
4298
  const errMsg = oauthErr instanceof Error ? oauthErr.message : "";
3848
4299
  const errCode = oauthErr instanceof Error && "code" in oauthErr ? oauthErr.code : "";
3849
4300
  if (errCode === "FORBIDDEN" || errMsg === "registration_closed" || errCode === "registration_closed") {
@@ -3886,6 +4337,10 @@ var LoginPage = class {
3886
4337
  restore();
3887
4338
  }, 5 * 60 * 1e3);
3888
4339
  const cleanup = () => {
4340
+ settled = true;
4341
+ btn.innerHTML = originalHTML;
4342
+ btn.disabled = false;
4343
+ btn.classList.remove("oauth-btn-loading");
3889
4344
  if (pollTimer !== void 0) {
3890
4345
  clearInterval(pollTimer);
3891
4346
  pollTimer = void 0;
@@ -3897,6 +4352,14 @@ var LoginPage = class {
3897
4352
  clearTimeout(abandonTimer);
3898
4353
  window.removeEventListener("message", onMessage);
3899
4354
  };
4355
+ this.oauthCleanup = () => {
4356
+ cleanup();
4357
+ recovery.remove();
4358
+ try {
4359
+ popup.close();
4360
+ } catch {
4361
+ }
4362
+ };
3900
4363
  window.addEventListener("message", onMessage);
3901
4364
  }
3902
4365
  // (removed dead code: renderEmailButton — email now renders through renderSecondaryGrid)
@@ -3906,19 +4369,24 @@ var LoginPage = class {
3906
4369
  * Even count: all items in 2 columns (each half width).
3907
4370
  * Odd count: DID Wallet spans full width at the end.
3908
4371
  */
3909
- renderSecondaryGrid(parent, locale, methods) {
4372
+ renderSecondaryGrid(parent, locale, methods, group) {
3910
4373
  const grid = document.createElement("div");
3911
4374
  grid.className = "methods-grid";
3912
4375
  if (methods.includes("oauth") && this.options.oauthProviders?.length) {
3913
- const enabledProviders = (this.options.oauthProviders ?? []).filter((p) => p.enabled);
4376
+ const order = this.options.oauthProviderOrder ?? ["google", "apple", "github"];
4377
+ const rank = (id) => order.includes(id) ? order.indexOf(id) : order.length;
4378
+ const explicitOrder = !!this.options.oauthProviderOrder || this.options.oauthProviders?.some((p) => p.order !== void 0 && p.order !== 999);
4379
+ 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
4380
  for (const provider of enabledProviders) {
3915
4381
  const btn = document.createElement("button");
3916
4382
  btn.className = "method-btn";
3917
4383
  btn.dataset.provider = provider.id;
3918
4384
  const icon = getOAuthIcon(provider.id);
3919
- btn.innerHTML = `${icon} ${escapeHtml(provider.name)}`;
4385
+ btn.innerHTML = `${icon} ${escapeHtml(t("continueWith", locale, { provider: provider.name }))}`;
3920
4386
  grid.appendChild(btn);
3921
4387
  btn.addEventListener("click", () => {
4388
+ this.select("oauth");
4389
+ this.activeProvider = provider.id;
3922
4390
  this.startOAuthPopup(provider.id, btn);
3923
4391
  });
3924
4392
  }
@@ -3930,6 +4398,7 @@ var LoginPage = class {
3930
4398
  btn.innerHTML = `${ICON_EMAIL} ${escapeHtml(t("emailButton", locale))}`;
3931
4399
  grid.appendChild(btn);
3932
4400
  btn.addEventListener("click", () => {
4401
+ this.select("email");
3933
4402
  this.switchView("email");
3934
4403
  });
3935
4404
  }
@@ -3941,6 +4410,7 @@ var LoginPage = class {
3941
4410
  btn.innerHTML = `${ICON_DID_WALLET} ${escapeHtml(walletLabel)}`;
3942
4411
  grid.appendChild(btn);
3943
4412
  btn.addEventListener("click", () => {
4413
+ this.select("did-connect");
3944
4414
  this.switchView("qr");
3945
4415
  });
3946
4416
  }
@@ -3949,19 +4419,27 @@ var LoginPage = class {
3949
4419
  // ─── Private: DID Connect flow (QR view) ───────────────────────────
3950
4420
  async startDIDConnectInView(statusEl, qrContainer, walletBtn, locale) {
3951
4421
  statusEl.textContent = t("connecting", locale);
4422
+ const epoch = this.generation;
4423
+ const isCurrent = () => this.mounted && epoch === this.generation;
3952
4424
  try {
3953
4425
  this.tokenSession?.destroy();
3954
4426
  this.tokenSession = new TokenSession(this.config, this.http);
3955
4427
  this.tokenSession.on("succeed", async () => {
4428
+ if (!isCurrent())
4429
+ return;
3956
4430
  this.showConnectResult(qrContainer, walletBtn, statusEl, t("verifying", locale));
3957
4431
  try {
3958
4432
  const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
3959
4433
  await this.http.post(`${servicePrefix}/api/did/connect/complete`, {
3960
4434
  token: this.tokenSession.state.token
3961
4435
  });
4436
+ if (!isCurrent())
4437
+ return;
3962
4438
  this.showConnectSuccess(qrContainer, walletBtn, statusEl, locale);
3963
4439
  this.handleSuccess();
3964
4440
  } catch (err) {
4441
+ if (!isCurrent())
4442
+ return;
3965
4443
  this.showConnectResult(qrContainer, walletBtn, statusEl, friendlyError(err, locale), true);
3966
4444
  }
3967
4445
  });
@@ -3992,17 +4470,21 @@ var LoginPage = class {
3992
4470
  }
3993
4471
  });
3994
4472
  const state = await this.tokenSession.create({ locale });
4473
+ if (!isCurrent())
4474
+ return;
3995
4475
  if (state.url) {
3996
4476
  renderQR(qrContainer, state.url);
3997
4477
  walletBtn.disabled = false;
3998
4478
  walletBtn.addEventListener("click", () => {
3999
- this.showConnectScanned(qrContainer, walletBtn, statusEl, locale);
4479
+ statusEl.textContent = t("walletRetry", locale);
4000
4480
  this.openInWallet(state.url, locale);
4001
4481
  });
4002
4482
  }
4003
4483
  statusEl.textContent = t("scanWithWallet", locale);
4004
4484
  this.tokenSession.startPolling();
4005
4485
  } catch (err) {
4486
+ if (!isCurrent())
4487
+ return;
4006
4488
  statusEl.textContent = friendlyError(err, locale);
4007
4489
  clearQR(qrContainer);
4008
4490
  }
@@ -4022,7 +4504,7 @@ var LoginPage = class {
4022
4504
  }
4023
4505
  this.cardEl.innerHTML = "";
4024
4506
  this.cardEl.dataset.view = "error";
4025
- this.renderLocaleSwitcher(this.cardEl);
4507
+ this.renderCardTools(this.cardEl);
4026
4508
  const resultContainer = document.createElement("div");
4027
4509
  resultContainer.className = "connect-result";
4028
4510
  resultContainer.innerHTML = `<div class="result-icon result-error">\u2715</div>`;
@@ -4161,13 +4643,238 @@ var LoginPage = class {
4161
4643
  }
4162
4644
  // ─── Private: success handler ────────────────────────────────────────
4163
4645
  handleSuccess() {
4646
+ if (!this.mounted || this.completed)
4647
+ return;
4648
+ this.completed = true;
4649
+ this.emit("success");
4650
+ if (this.options.preferenceKey && this.activeMethod) {
4651
+ try {
4652
+ localStorage.setItem(`did-login-method:${this.options.preferenceKey}`, this.activeMethod === "oauth" && this.activeProvider ? `oauth:${this.activeProvider}` : this.activeMethod);
4653
+ } catch {
4654
+ }
4655
+ }
4164
4656
  if (this.options.onSuccess) {
4165
- this.options.onSuccess();
4657
+ const epoch = this.generation;
4658
+ Promise.resolve().then(() => {
4659
+ if (this.mounted && epoch === this.generation)
4660
+ return this.options.onSuccess?.();
4661
+ }).catch(() => {
4662
+ if (!this.mounted || epoch !== this.generation)
4663
+ return;
4664
+ this.completed = false;
4665
+ this.showFullError(t("authFailed", this.options.locale));
4666
+ });
4166
4667
  } else {
4167
4668
  setTimeout(() => location.reload(), 300);
4168
4669
  }
4169
4670
  }
4170
4671
  };
4672
+
4673
+ // dist/ui/login-dialog.js
4674
+ var DIALOG_STYLES = `${DESIGN_TOKENS_CSS}${DESIGN_TOKENS_LIGHT_CSS}
4675
+ :host { color-scheme: dark; }
4676
+ :host([data-theme="light"]) { color-scheme: light; }
4677
+ dialog { box-sizing:border-box; position:fixed; inset:0; margin:auto; padding:0;
4678
+ width:min(420px,calc(100vw - 32px)); max-width:none; max-height:calc(100dvh - 32px);
4679
+ border:1px solid var(--border); border-radius:16px; background:var(--bg-card);
4680
+ color:var(--text); color-scheme:inherit; overflow:hidden; box-shadow:var(--shadow-lg); }
4681
+ dialog::backdrop { background:rgba(8,10,18,.56); }
4682
+ :host(:not([data-theme="light"])) dialog::backdrop { background:rgba(0,0,0,.68); }
4683
+ [data-dialog-scroll] { max-height:calc(100dvh - 34px); overflow:auto; overscroll-behavior:contain; }
4684
+ .close { position:absolute; top:8px; right:8px; z-index:3; display:grid; place-items:center;
4685
+ box-sizing:border-box; width:44px; height:44px; padding:0; border:1px solid transparent;
4686
+ border-radius:10px; background:var(--bg-card); color:var(--text-secondary);
4687
+ font:400 24px/1 system-ui,sans-serif; cursor:pointer; }
4688
+ .close:hover { color:var(--text); background:var(--bg-hover); }
4689
+ .close:focus-visible { outline:2px solid var(--blue); outline-offset:-3px; }
4690
+ .host-error { padding:16px 20px; font:14px/1.5 system-ui,sans-serif; overflow-wrap:anywhere; }
4691
+ .retry { padding:10px 16px; margin:0 20px 20px; color:var(--text); background:var(--bg-elevated);
4692
+ border:1px solid var(--border); border-radius:8px; font:inherit; cursor:pointer; }
4693
+ `;
4694
+ var LoginDialog = class {
4695
+ config;
4696
+ http;
4697
+ options;
4698
+ host;
4699
+ dialog;
4700
+ page;
4701
+ previous;
4702
+ themeObserver;
4703
+ systemTheme;
4704
+ syncTheme = () => this.applyTheme(this.hostTheme());
4705
+ constructor(config, http, options = {}) {
4706
+ this.config = config;
4707
+ this.http = http;
4708
+ this.options = options;
4709
+ }
4710
+ mount() {
4711
+ if (this.dialog)
4712
+ return;
4713
+ let focused = document.activeElement;
4714
+ while (focused?.shadowRoot?.activeElement)
4715
+ focused = focused.shadowRoot.activeElement;
4716
+ this.previous = this.options.returnFocus ?? (focused instanceof HTMLElement ? focused : void 0);
4717
+ const host = document.createElement("div");
4718
+ host.dataset.didLoginDialogHost = "";
4719
+ host.style.cssText = "position:fixed;width:0;height:0;";
4720
+ this.host = host;
4721
+ const root = host.attachShadow({ mode: "open" });
4722
+ const style = document.createElement("style");
4723
+ style.textContent = DIALOG_STYLES;
4724
+ root.append(style);
4725
+ const dialog = document.createElement("dialog");
4726
+ this.dialog = dialog;
4727
+ dialog.setAttribute("aria-label", this.options.purpose?.title ?? t("signIn", this.options.locale));
4728
+ root.append(dialog);
4729
+ const close = document.createElement("button");
4730
+ close.type = "button";
4731
+ close.className = "close";
4732
+ close.textContent = "\xD7";
4733
+ close.setAttribute("aria-label", t("cancel", this.options.locale));
4734
+ const cancel = () => {
4735
+ if (this.options.dismissible === false || this.dialog !== dialog)
4736
+ return;
4737
+ try {
4738
+ this.options.onEvent?.({ name: "cancel", mode: "dialog" });
4739
+ } catch {
4740
+ }
4741
+ this.destroy();
4742
+ this.options.onCancel?.();
4743
+ };
4744
+ close.addEventListener("click", cancel);
4745
+ if (this.options.dismissible !== false)
4746
+ dialog.append(close);
4747
+ const scroller = document.createElement("div");
4748
+ scroller.dataset.dialogScroll = "";
4749
+ dialog.append(scroller);
4750
+ const container = document.createElement("div");
4751
+ container.dataset.loginDialogMount = "";
4752
+ scroller.append(container);
4753
+ dialog.addEventListener("cancel", (event) => {
4754
+ event.preventDefault();
4755
+ cancel();
4756
+ });
4757
+ document.body.append(host);
4758
+ const theme = this.hostTheme();
4759
+ host.dataset.theme = theme;
4760
+ this.page = new LoginPage(container, this.config, this.http, {
4761
+ ...this.options,
4762
+ theme,
4763
+ isolate: true,
4764
+ mode: "dialog",
4765
+ onCancel: cancel,
4766
+ onLocaleChange: (locale) => {
4767
+ this.options.locale = locale;
4768
+ close.setAttribute("aria-label", t("cancel", locale));
4769
+ dialog.setAttribute("aria-label", this.options.purpose?.title ?? t("signIn", locale));
4770
+ this.options.onLocaleChange?.(locale);
4771
+ },
4772
+ onSuccess: async () => {
4773
+ const showFailure = () => {
4774
+ if (!dialog.isConnected || this.dialog !== dialog)
4775
+ return;
4776
+ for (const node of scroller.querySelectorAll(".host-error, .retry"))
4777
+ node.remove();
4778
+ const status = document.createElement("p");
4779
+ status.className = "host-error";
4780
+ status.setAttribute("role", "alert");
4781
+ status.textContent = t("authFailed", this.options.locale);
4782
+ const retry = document.createElement("button");
4783
+ retry.type = "button";
4784
+ retry.className = "retry";
4785
+ retry.textContent = t("tryAgain", this.options.locale);
4786
+ retry.onclick = async () => {
4787
+ retry.disabled = true;
4788
+ try {
4789
+ await this.options.onSuccess?.();
4790
+ if (this.dialog === dialog)
4791
+ this.destroy();
4792
+ } catch {
4793
+ showFailure();
4794
+ }
4795
+ };
4796
+ scroller.append(status, retry);
4797
+ };
4798
+ try {
4799
+ await this.options.onSuccess?.();
4800
+ if (this.dialog === dialog)
4801
+ this.destroy();
4802
+ } catch {
4803
+ showFailure();
4804
+ }
4805
+ }
4806
+ });
4807
+ this.page.mount();
4808
+ this.themeObserver = new MutationObserver(this.syncTheme);
4809
+ for (const target of [document.documentElement, document.body]) {
4810
+ this.themeObserver.observe(target, { attributes: true, attributeFilter: ["data-theme", "data-mode", "class", "style"] });
4811
+ }
4812
+ if (typeof window.matchMedia === "function") {
4813
+ this.systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
4814
+ this.systemTheme.addEventListener("change", this.syncTheme);
4815
+ }
4816
+ dialog.showModal();
4817
+ const title = container.shadowRoot?.querySelector("h1, h2");
4818
+ if (title) {
4819
+ title.tabIndex = -1;
4820
+ title.focus({ preventScroll: true });
4821
+ }
4822
+ }
4823
+ hostTheme() {
4824
+ for (const element of [document.documentElement, document.body]) {
4825
+ for (const name of ["data-mode", "data-theme"]) {
4826
+ const value = element.getAttribute(name);
4827
+ if (value === "light" || value === "dark")
4828
+ return value;
4829
+ if (value === "system" || value === "auto")
4830
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
4831
+ }
4832
+ }
4833
+ for (const element of [document.documentElement, document.body]) {
4834
+ if (element.classList.contains("dark"))
4835
+ return "dark";
4836
+ if (element.classList.contains("light"))
4837
+ return "light";
4838
+ }
4839
+ for (const element of [document.body, document.documentElement]) {
4840
+ const schemes = getComputedStyle(element).colorScheme.split(/\s+/);
4841
+ if (schemes.includes("light") && schemes.includes("dark")) {
4842
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
4843
+ }
4844
+ if (schemes.includes("light"))
4845
+ return "light";
4846
+ if (schemes.includes("dark"))
4847
+ return "dark";
4848
+ }
4849
+ if (this.options.theme === "light" || this.options.theme === "dark")
4850
+ return this.options.theme;
4851
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
4852
+ }
4853
+ setTheme(theme) {
4854
+ this.options.theme = theme;
4855
+ this.applyTheme(theme);
4856
+ }
4857
+ applyTheme(theme) {
4858
+ if (this.host)
4859
+ this.host.dataset.theme = theme;
4860
+ this.page?.setTheme(theme);
4861
+ }
4862
+ destroy() {
4863
+ this.themeObserver?.disconnect();
4864
+ this.themeObserver = void 0;
4865
+ this.systemTheme?.removeEventListener("change", this.syncTheme);
4866
+ this.systemTheme = void 0;
4867
+ this.page?.destroy();
4868
+ this.page = void 0;
4869
+ this.dialog?.close();
4870
+ this.dialog?.remove();
4871
+ this.dialog = void 0;
4872
+ this.host?.remove();
4873
+ this.host = void 0;
4874
+ if (this.previous?.isConnected)
4875
+ this.previous.focus({ preventScroll: true });
4876
+ }
4877
+ };
4171
4878
  export {
4172
4879
  DESIGN_RESET_CSS,
4173
4880
  DESIGN_TOKENS_CSS,
@@ -4177,6 +4884,7 @@ export {
4177
4884
  ICON_EMAIL,
4178
4885
  ICON_PASSKEY,
4179
4886
  LOGIN_PAGE_STYLES,
4887
+ LoginDialog,
4180
4888
  LoginPage,
4181
4889
  OAUTH_ICONS,
4182
4890
  SUPPORTED_LOCALES,