@descope/web-components-ui 3.20.3 → 3.21.0

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.
Files changed (130) hide show
  1. package/dist/cjs/index.cjs.js +414 -27
  2. package/dist/cjs/index.cjs.js.map +1 -1
  3. package/dist/index.esm.js +414 -27
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/umd/1411.js +339 -0
  6. package/dist/umd/1411.js.map +1 -0
  7. package/dist/umd/189.js +171 -0
  8. package/dist/umd/189.js.map +1 -0
  9. package/dist/umd/2894.js +339 -0
  10. package/dist/umd/2894.js.map +1 -0
  11. package/dist/umd/3792.js +339 -0
  12. package/dist/umd/3792.js.map +1 -0
  13. package/dist/umd/{9117.js → 384.js} +121 -9
  14. package/dist/umd/{descope-tooltip.js.LICENSE.txt → 384.js.LICENSE.txt} +2 -2
  15. package/dist/umd/384.js.map +1 -0
  16. package/dist/umd/4268.js +339 -0
  17. package/dist/umd/4268.js.LICENSE.txt +11 -0
  18. package/dist/umd/4268.js.map +1 -0
  19. package/dist/umd/513.js +339 -0
  20. package/dist/umd/513.js.LICENSE.txt +11 -0
  21. package/dist/umd/513.js.map +1 -0
  22. package/dist/umd/6227.js +320 -0
  23. package/dist/umd/6227.js.map +1 -0
  24. package/dist/umd/6535.js +115 -0
  25. package/dist/umd/{9275.js.LICENSE.txt → 6535.js.LICENSE.txt} +0 -6
  26. package/dist/umd/6535.js.map +1 -0
  27. package/dist/umd/6649.js +339 -0
  28. package/dist/umd/6649.js.LICENSE.txt +11 -0
  29. package/dist/umd/6649.js.map +1 -0
  30. package/dist/umd/7125.js +339 -0
  31. package/dist/umd/7125.js.LICENSE.txt +11 -0
  32. package/dist/umd/7125.js.map +1 -0
  33. package/dist/umd/{5263.js → 8264.js} +5 -5
  34. package/dist/umd/8264.js.map +1 -0
  35. package/dist/umd/8983.js +7 -324
  36. package/dist/umd/8983.js.map +1 -1
  37. package/dist/umd/9030.js +339 -0
  38. package/dist/umd/9030.js.LICENSE.txt +11 -0
  39. package/dist/umd/9030.js.map +1 -0
  40. package/dist/umd/9380.js +130 -0
  41. package/dist/umd/9380.js.map +1 -0
  42. package/dist/umd/9506.js +339 -0
  43. package/dist/umd/9506.js.LICENSE.txt +11 -0
  44. package/dist/umd/9506.js.map +1 -0
  45. package/dist/umd/DescopeDev.js +1 -1
  46. package/dist/umd/DescopeDev.js.map +1 -1
  47. package/dist/umd/button-selection-group-fields-descope-button-multi-selection-group-index-js.js +1 -1
  48. package/dist/umd/button-selection-group-fields-descope-button-multi-selection-group-index-js.js.map +1 -1
  49. package/dist/umd/button-selection-group-fields-descope-button-selection-group-index-js.js +1 -1
  50. package/dist/umd/button-selection-group-fields-descope-button-selection-group-index-js.js.map +1 -1
  51. package/dist/umd/button-selection-group-fields-descope-button-selection-group-item-index-js.js +1 -338
  52. package/dist/umd/button-selection-group-fields-descope-button-selection-group-item-index-js.js.map +1 -1
  53. package/dist/umd/descope-apps-list.js +161 -1
  54. package/dist/umd/descope-apps-list.js.LICENSE.txt +17 -0
  55. package/dist/umd/descope-apps-list.js.map +1 -1
  56. package/dist/umd/descope-avatar.js +5 -20
  57. package/dist/umd/descope-avatar.js.LICENSE.txt +0 -12
  58. package/dist/umd/descope-avatar.js.map +1 -1
  59. package/dist/umd/descope-button.js +3 -3
  60. package/dist/umd/descope-button.js.LICENSE.txt +6 -0
  61. package/dist/umd/descope-button.js.map +1 -1
  62. package/dist/umd/descope-date-field-descope-calendar-index-js.js +1 -1
  63. package/dist/umd/descope-date-field-descope-calendar-index-js.js.map +1 -1
  64. package/dist/umd/descope-email-field-index-js.js +4 -4
  65. package/dist/umd/descope-email-field-index-js.js.map +1 -1
  66. package/dist/umd/descope-grid-descope-grid-custom-column-index-js.js +4 -321
  67. package/dist/umd/descope-grid-descope-grid-custom-column-index-js.js.LICENSE.txt +0 -6
  68. package/dist/umd/descope-grid-descope-grid-custom-column-index-js.js.map +1 -1
  69. package/dist/umd/descope-grid-descope-grid-item-details-column-index-js.js +5 -322
  70. package/dist/umd/descope-grid-descope-grid-item-details-column-index-js.js.map +1 -1
  71. package/dist/umd/descope-grid-descope-grid-text-column-index-js.js +4 -321
  72. package/dist/umd/descope-grid-descope-grid-text-column-index-js.js.LICENSE.txt +0 -6
  73. package/dist/umd/descope-grid-descope-grid-text-column-index-js.js.map +1 -1
  74. package/dist/umd/descope-hybrid-field-index-js.js +2 -2
  75. package/dist/umd/descope-hybrid-field-index-js.js.map +1 -1
  76. package/dist/umd/descope-multi-line-mappings.js +1 -1
  77. package/dist/umd/descope-multi-line-mappings.js.map +1 -1
  78. package/dist/umd/descope-multi-sso.js +1 -338
  79. package/dist/umd/descope-multi-sso.js.map +1 -1
  80. package/dist/umd/descope-outbound-app-button.js +1 -338
  81. package/dist/umd/descope-outbound-app-button.js.map +1 -1
  82. package/dist/umd/descope-recovery-codes.js +1 -1
  83. package/dist/umd/descope-recovery-codes.js.map +1 -1
  84. package/dist/umd/descope-timer-button.js +1 -338
  85. package/dist/umd/descope-timer-button.js.map +1 -1
  86. package/dist/umd/descope-tooltip.js +1 -2
  87. package/dist/umd/descope-tooltip.js.map +1 -1
  88. package/dist/umd/descope-upload-file-index-js.js +1 -338
  89. package/dist/umd/descope-upload-file-index-js.js.map +1 -1
  90. package/dist/umd/descope-user-attribute-index-js.js +1 -338
  91. package/dist/umd/descope-user-attribute-index-js.js.map +1 -1
  92. package/dist/umd/descope-user-auth-method-index-js.js +1 -338
  93. package/dist/umd/descope-user-auth-method-index-js.js.map +1 -1
  94. package/dist/umd/descope-user-passkeys.js +1 -338
  95. package/dist/umd/descope-user-passkeys.js.map +1 -1
  96. package/dist/umd/index.js +1 -1
  97. package/dist/umd/index.js.map +1 -1
  98. package/package.json +42 -42
  99. package/src/components/descope-email-field/EmailFieldClass.js +50 -2
  100. package/src/components/descope-email-field/emailPattern.js +7 -0
  101. package/stories/descope-email-field.stories.js +8 -0
  102. package/dist/umd/1453.js +0 -177
  103. package/dist/umd/1453.js.LICENSE.txt +0 -29
  104. package/dist/umd/1453.js.map +0 -1
  105. package/dist/umd/5263.js.map +0 -1
  106. package/dist/umd/6063.js +0 -18
  107. package/dist/umd/6063.js.map +0 -1
  108. package/dist/umd/6818.js +0 -3
  109. package/dist/umd/6818.js.map +0 -1
  110. package/dist/umd/8248.js +0 -171
  111. package/dist/umd/8248.js.map +0 -1
  112. package/dist/umd/8968.js +0 -432
  113. package/dist/umd/8968.js.map +0 -1
  114. package/dist/umd/9117.js.map +0 -1
  115. package/dist/umd/9275.js +0 -18
  116. package/dist/umd/9275.js.map +0 -1
  117. package/dist/umd/button-selection-group-fields-descope-button-selection-group-item-index-js.js.LICENSE.txt +0 -5
  118. package/dist/umd/descope-multi-sso.js.LICENSE.txt +0 -5
  119. package/dist/umd/descope-outbound-app-button.js.LICENSE.txt +0 -5
  120. package/dist/umd/descope-timer-button.js.LICENSE.txt +0 -5
  121. package/dist/umd/descope-upload-file-index-js.js.LICENSE.txt +0 -5
  122. package/dist/umd/descope-user-attribute-index-js.js.LICENSE.txt +0 -5
  123. package/dist/umd/descope-user-passkeys.js.LICENSE.txt +0 -5
  124. /package/dist/umd/{8968.js.LICENSE.txt → 1411.js.LICENSE.txt} +0 -0
  125. /package/dist/umd/{6063.js.LICENSE.txt → 189.js.LICENSE.txt} +0 -0
  126. /package/dist/umd/{9117.js.LICENSE.txt → 2894.js.LICENSE.txt} +0 -0
  127. /package/dist/umd/{descope-user-auth-method-index-js.js.LICENSE.txt → 3792.js.LICENSE.txt} +0 -0
  128. /package/dist/umd/{6818.js.LICENSE.txt → 6227.js.LICENSE.txt} +0 -0
  129. /package/dist/umd/{5263.js.LICENSE.txt → 8264.js.LICENSE.txt} +0 -0
  130. /package/dist/umd/{8248.js.LICENSE.txt → 9380.js.LICENSE.txt} +0 -0
@@ -9546,6 +9546,36 @@ var collapsibleContainer$1 = /*#__PURE__*/Object.freeze({
9546
9546
 
9547
9547
  const componentName$16 = getComponentName('recovery-codes');
9548
9548
 
9549
+ const NO_ACTION = 'none';
9550
+ const DEFAULT_ERROR_MESSAGE = 'Please save your recovery codes to continue';
9551
+
9552
+ // default button label per action (buttons variant). the icons variant uses the vaadin
9553
+ // glyphs hardcoded in the shadow-DOM template below; the buttons variant uses image srcs.
9554
+ const ACTIONS = {
9555
+ copy: { defaultText: 'Copy' },
9556
+ download: { defaultText: 'Download' },
9557
+ };
9558
+
9559
+ // per-button config suffixes; the full attribute is `${action}-${suffix}`.
9560
+ // `icon`/`icon-dark` are image srcs (the seeded glyph or a custom upload), matching
9561
+ // the standard button's start-icon (light/dark), not a boolean.
9562
+ const BUTTON_SUFFIXES = [
9563
+ 'text',
9564
+ 'variant',
9565
+ 'color',
9566
+ 'size',
9567
+ 'icon',
9568
+ 'icon-dark',
9569
+ ];
9570
+
9571
+ // changing any of these rebuilds the buttons (so the editor preview stays live)
9572
+ const BUTTON_ATTRS = [
9573
+ 'show-download',
9574
+ ...Object.keys(ACTIONS).flatMap((action) =>
9575
+ BUTTON_SUFFIXES.map((suffix) => `${action}-${suffix}`),
9576
+ ),
9577
+ ];
9578
+
9549
9579
  const itemRenderer$5 = ({ value }, _, ref) => {
9550
9580
  return `
9551
9581
  <descope-text variant="${ref.variant}" mode="${ref.mode}">
@@ -9559,24 +9589,44 @@ class RawRecoveryCodes extends createBaseClass$1({
9559
9589
  baseSelector: ':host > div',
9560
9590
  }) {
9561
9591
  static get observedAttributes() {
9562
- return ['readonly'];
9592
+ return ['readonly', 'required', ...BUTTON_ATTRS];
9563
9593
  }
9564
9594
 
9595
+ // 'none' | 'copied' | 'downloaded' - the last action the user took on the codes
9596
+ #action = NO_ACTION;
9597
+
9598
+ // the error is shown only after a submit attempt (reportValidity), like inputs
9599
+ #showError = false;
9600
+
9601
+ // init() builds the buttons once with all seeded attrs; guards against rebuilding
9602
+ // on every per-attribute change fired during element upgrade (before init runs)
9603
+ #initialized = false;
9604
+
9565
9605
  constructor() {
9566
9606
  super();
9567
9607
 
9568
9608
  this.attachShadow({ mode: 'open' }).innerHTML = `
9569
9609
  <div class="wrapper">
9570
- <div class="list"></div>
9571
- <descope-text class="icon-wrapper">
9572
- <vaadin-icon class="icon" icon="vaadin:copy-o" title="Copy"></vaadin-icon>
9573
- </descope-text>
9610
+ <div class="codes-box">
9611
+ <div class="list"></div>
9612
+ <descope-text class="icon-wrapper">
9613
+ <div class="icons">
9614
+ <vaadin-icon class="icon copy-icon" icon="vaadin:copy-o" title="Copy"></vaadin-icon>
9615
+ <vaadin-icon class="icon download-icon" icon="vaadin:download-alt" title="Download"></vaadin-icon>
9616
+ </div>
9617
+ </descope-text>
9618
+ </div>
9619
+ <div class="error-message"></div>
9620
+ <div class="buttons-row"></div>
9574
9621
  </div>
9575
9622
  `;
9576
9623
 
9577
9624
  this.list = this.shadowRoot.querySelector('.list');
9578
- this.icon = this.shadowRoot.querySelector('.icon');
9625
+ this.copyIcon = this.shadowRoot.querySelector('.copy-icon');
9626
+ this.downloadIcon = this.shadowRoot.querySelector('.download-icon');
9579
9627
  this.iconWrapper = this.shadowRoot.querySelector('.icon-wrapper');
9628
+ this.errorMessageEle = this.shadowRoot.querySelector('.error-message');
9629
+ this.buttonsRow = this.shadowRoot.querySelector('.buttons-row');
9580
9630
 
9581
9631
  injectStyle(
9582
9632
  `
@@ -9585,8 +9635,14 @@ class RawRecoveryCodes extends createBaseClass$1({
9585
9635
  position: relative;
9586
9636
  }
9587
9637
  .wrapper {
9638
+ display: flex;
9639
+ flex-direction: column;
9640
+ }
9641
+ .codes-box {
9588
9642
  display: flex;
9589
9643
  justify-content: space-between;
9644
+ box-sizing: border-box;
9645
+ /* border shows the error ring inside the box; style/width/color/radius come from theme */
9590
9646
  }
9591
9647
  .list {
9592
9648
  display: flex;
@@ -9605,15 +9661,57 @@ class RawRecoveryCodes extends createBaseClass$1({
9605
9661
  .icon-wrapper {
9606
9662
  align-self: flex-start;
9607
9663
  }
9664
+ .icons {
9665
+ display: flex;
9666
+ flex-direction: column;
9667
+ }
9608
9668
  .icon {
9609
9669
  cursor: pointer;
9610
9670
  }
9611
9671
  .icon.copied {
9612
9672
  cursor: initial;
9673
+ /* block re-clicks while the checkmark shows, so rapid clicks cannot repeat the action */
9674
+ pointer-events: none;
9613
9675
  }
9614
- .icon.readonly {
9676
+ /* readonly is a host attribute, so disable pointer events from one place */
9677
+ :host([readonly="true"]) .icon,
9678
+ :host([readonly="true"]) .buttons-row {
9615
9679
  pointer-events: none;
9616
9680
  }
9681
+ .download-icon {
9682
+ display: none;
9683
+ }
9684
+ :host([show-download="true"]) .download-icon {
9685
+ display: inline-flex;
9686
+ }
9687
+ .error-message {
9688
+ display: none;
9689
+ /* width:0 + min-width:100% lets the message wrap to the component width without
9690
+ its text expanding the (shrink-to-fit) component; full-width still fills the parent */
9691
+ width: 0;
9692
+ min-width: 100%;
9693
+ box-sizing: border-box;
9694
+ line-height: 1.2;
9695
+ overflow-wrap: break-word;
9696
+ }
9697
+ :host([invalid]) .error-message {
9698
+ display: block;
9699
+ }
9700
+ .buttons-row {
9701
+ display: none;
9702
+ }
9703
+ :host([display-variant="buttons"]) .icon-wrapper {
9704
+ display: none;
9705
+ }
9706
+ :host([display-variant="buttons"]) .buttons-row {
9707
+ display: flex;
9708
+ flex-wrap: wrap;
9709
+ }
9710
+ /* grow to fill equally; don't shrink below content, so they wrap to a new line
9711
+ when they don't fit (like a container with auto-fit items) */
9712
+ .buttons-row > descope-button {
9713
+ flex: 1 0 auto;
9714
+ }
9617
9715
  `,
9618
9716
  this,
9619
9717
  );
@@ -9627,41 +9725,190 @@ class RawRecoveryCodes extends createBaseClass$1({
9627
9725
  return this.getAttribute('mode');
9628
9726
  }
9629
9727
 
9728
+ // the form value read by the flow SDK (#getFormData reads `.value`)
9729
+ get value() {
9730
+ return this.#action;
9731
+ }
9732
+
9733
+ // a boolean attribute is "on" when present and not explicitly "false"
9734
+ #boolAttr(name) {
9735
+ return this.hasAttribute(name) && this.getAttribute(name) !== 'false';
9736
+ }
9737
+
9738
+ get isRequired() {
9739
+ return this.#boolAttr('required');
9740
+ }
9741
+
9742
+ get isReadOnly() {
9743
+ return this.#boolAttr('readonly');
9744
+ }
9745
+
9746
+ get errorMessage() {
9747
+ return (
9748
+ this.getAttribute('data-errormessage-value-missing') ||
9749
+ DEFAULT_ERROR_MESSAGE
9750
+ );
9751
+ }
9752
+
9753
+ get codesText() {
9754
+ return String((this.data || []).map((rc) => rc.value).join('\n'));
9755
+ }
9756
+
9630
9757
  init() {
9631
9758
  super.init();
9632
9759
 
9633
- this.icon.addEventListener('click', this.onCopy.bind(this));
9760
+ this.copyIcon.addEventListener('click', this.onCopy.bind(this));
9761
+ this.downloadIcon.addEventListener('click', this.onDownload.bind(this));
9634
9762
 
9635
9763
  forwardAttrs(this, this.iconWrapper, { includeAttrs: ['variant', 'mode'] });
9764
+
9765
+ this.renderButtons();
9766
+ this.#initialized = true;
9636
9767
  }
9637
9768
 
9638
- onCopy() {
9639
- navigator.clipboard.writeText(
9640
- String(this.data.map((rc) => rc.value).join('\n')),
9769
+ get isDownloadEnabled() {
9770
+ return this.getAttribute('show-download') === 'true';
9771
+ }
9772
+
9773
+ // buttons variant: copy (+ download when enabled) as descope-buttons, each independently
9774
+ // configurable. rendered even in the icons variant (CSS hides the row) so a variant switch is instant.
9775
+ renderButtons() {
9776
+ if (!this.buttonsRow) return;
9777
+
9778
+ const buttons = [this.buildButton('copy')];
9779
+ if (this.isDownloadEnabled) {
9780
+ buttons.push(this.buildButton('download'));
9781
+ }
9782
+ this.buttonsRow.replaceChildren(...buttons);
9783
+ }
9784
+
9785
+ buildButton(action) {
9786
+ const { defaultText } = ACTIONS[action];
9787
+ const attr = (name, fallback) =>
9788
+ this.getAttribute(`${action}-${name}`) || fallback;
9789
+
9790
+ const button = document.createElement('descope-button');
9791
+ button.setAttribute('data-action', action);
9792
+ button.setAttribute('variant', attr('variant', 'outline'));
9793
+ button.setAttribute('mode', attr('color', 'primary'));
9794
+ button.setAttribute('size', attr('size', 'md'));
9795
+
9796
+ // icon is an image src - the seeded glyph or a custom upload (light/dark),
9797
+ // same as the standard button's start icon. rendered via descope-icon in the
9798
+ // default slot (not slot="prefix") so it centers with the label when the
9799
+ // button grows. descope-icon inlines SVGs, so the glyph (authored with
9800
+ // fill="currentColor") tracks the button text color while an uploaded image
9801
+ // keeps its own colors.
9802
+ const iconSrc = this.getAttribute(`${action}-icon`);
9803
+ if (iconSrc) {
9804
+ const icon = document.createElement('descope-icon');
9805
+ icon.setAttribute('src', iconSrc);
9806
+ const iconDark = this.getAttribute(`${action}-icon-dark`);
9807
+ if (iconDark) icon.setAttribute('src-dark', iconDark);
9808
+ button.appendChild(icon);
9809
+ }
9810
+
9811
+ button.appendChild(document.createTextNode(attr('text', defaultText)));
9812
+ button.addEventListener('click', () =>
9813
+ action === 'copy' ? this.onCopy() : this.onDownload(),
9641
9814
  );
9815
+ return button;
9816
+ }
9642
9817
 
9643
- this.icon.setAttribute('icon', 'vaadin:check-circle-o');
9644
- this.icon.setAttribute('title', 'Copied');
9645
- this.icon.classList.add('copied');
9818
+ onCopy() {
9819
+ if (this.isReadOnly) return;
9820
+ navigator.clipboard.writeText(this.codesText);
9821
+ this.showActionFeedback(this.copyIcon, 'Copied');
9822
+ this.recordAction('copied');
9823
+ }
9646
9824
 
9647
- setTimeout(() => {
9648
- this.icon.setAttribute('icon', 'vaadin:copy-o');
9649
- this.icon.setAttribute('title', 'Copy');
9650
- this.icon.classList.remove('copied');
9825
+ onDownload() {
9826
+ if (this.isReadOnly) return;
9827
+ const blob = new Blob([this.codesText], { type: 'text/plain' });
9828
+ const url = URL.createObjectURL(blob);
9829
+ const anchor = document.createElement('a');
9830
+ anchor.href = url;
9831
+ anchor.download = 'recovery-codes.txt';
9832
+ anchor.click();
9833
+ URL.revokeObjectURL(url);
9834
+
9835
+ this.showActionFeedback(this.downloadIcon, 'Downloaded');
9836
+ this.recordAction('downloaded');
9837
+ }
9838
+
9839
+ // copy/download share this - only the visual feedback differs by variant
9840
+ recordAction(action) {
9841
+ this.#action = action;
9842
+ // let the flow re-evaluate real-time conditions and pick up the new value
9843
+ this.dispatchEvent(new Event('change', { bubbles: true }));
9844
+ // acting clears the mandatory error
9845
+ if (this.checkValidity()) {
9846
+ this.#showError = false;
9847
+ }
9848
+ this.syncValidityUI();
9849
+ }
9850
+
9851
+ // icons variant: swap the icon to a checkmark for 5s (buttons variant is static)
9852
+ showActionFeedback(iconEle, doneTitle) {
9853
+ // on a rapid re-click the checkmark is already showing, so cancel the pending restore
9854
+ // and keep the glyph/title we saved the first time (never capture the checkmark itself)
9855
+ if (iconEle.feedbackTimer) {
9856
+ clearTimeout(iconEle.feedbackTimer);
9857
+ } else {
9858
+ iconEle.originalIcon = iconEle.getAttribute('icon');
9859
+ iconEle.originalTitle = iconEle.getAttribute('title');
9860
+ }
9861
+
9862
+ iconEle.setAttribute('icon', 'vaadin:check-circle-o');
9863
+ iconEle.setAttribute('title', doneTitle);
9864
+ iconEle.classList.add('copied');
9865
+
9866
+ iconEle.feedbackTimer = setTimeout(() => {
9867
+ iconEle.setAttribute('icon', iconEle.originalIcon);
9868
+ iconEle.setAttribute('title', iconEle.originalTitle);
9869
+ iconEle.classList.remove('copied');
9870
+ iconEle.feedbackTimer = null;
9651
9871
  }, 5000);
9652
9872
  }
9653
9873
 
9654
- onReadOnly(isReadOnly) {
9655
- this.icon.classList.toggle('readonly', isReadOnly);
9874
+ // the flow SDK's #validateInputs calls checkValidity/reportValidity on every *[name]
9875
+ checkValidity() {
9876
+ return this.isReadOnly || !this.isRequired || this.value !== NO_ACTION;
9877
+ }
9878
+
9879
+ reportValidity() {
9880
+ const valid = this.checkValidity();
9881
+ this.#showError = !valid;
9882
+ this.syncValidityUI();
9883
+ return valid;
9884
+ }
9885
+
9886
+ // single source of truth for the error UI - reflects `invalid` (theme `_invalid`
9887
+ // draws the outline) and fills the message. never toggled anywhere else.
9888
+ syncValidityUI() {
9889
+ this.toggleAttribute('invalid', this.#showError);
9890
+ if (this.errorMessageEle) {
9891
+ this.errorMessageEle.textContent = this.#showError
9892
+ ? this.errorMessage
9893
+ : '';
9894
+ }
9656
9895
  }
9657
9896
 
9658
9897
  attributeChangedCallback(attrName, oldValue, newValue) {
9659
9898
  super.attributeChangedCallback?.(attrName, oldValue, newValue);
9660
9899
 
9661
- if (newValue !== oldValue) {
9662
- if (attrName === 'readonly') {
9663
- this.onReadOnly(newValue === 'true');
9664
- }
9900
+ if (newValue === oldValue) return;
9901
+
9902
+ // if it stops being required, drop any error already shown
9903
+ if (attrName === 'required' && !this.isRequired) {
9904
+ this.#showError = false;
9905
+ this.syncValidityUI();
9906
+ }
9907
+
9908
+ // only rebuild for live edits after init (init already built once with all attrs);
9909
+ // avoids rebuilding once per attribute during the initial element upgrade
9910
+ if (this.#initialized && BUTTON_ATTRS.includes(attrName)) {
9911
+ this.renderButtons();
9665
9912
  }
9666
9913
  }
9667
9914
  }
@@ -9681,10 +9928,66 @@ const RecoveryCodesClass = compose(
9681
9928
  textAlign: { selector: () => '.list', property: 'align-items' },
9682
9929
  iconColor: { selector: () => 'vaadin-icon', property: 'color' },
9683
9930
  iconSize: { selector: () => 'vaadin-icon', property: 'font-size' },
9684
- iconGap: { selector: () => '.wrapper', property: 'gap' },
9931
+ iconGap: { selector: () => '.codes-box', property: 'gap' },
9932
+ iconStackGap: { selector: () => '.icons', property: 'gap' },
9933
+ buttonsGap: { selector: () => '.buttons-row', property: 'gap' },
9934
+ sectionsGap: { selector: () => '.wrapper', property: 'gap' },
9685
9935
  bulletGap: { selector: () => 'descope-text span', property: 'gap' },
9936
+ // top + bottom padding sit inside the border (below the codes) in both variants; in the
9937
+ // buttons variant the bottom padding also repeats below the buttons (the buttons-row entry
9938
+ // is inert in the icons variant, where the row is display:none).
9939
+ verticalPadding: [
9940
+ { selector: () => '.codes-box', property: 'padding-top' },
9941
+ { selector: () => '.codes-box', property: 'padding-bottom' },
9942
+ { selector: () => '.buttons-row', property: 'padding-bottom' },
9943
+ ],
9944
+ // horizontal padding insets the codes (inside the border) and the buttons, but NOT the
9945
+ // error message, which stays aligned to the component edge (like the input error).
9946
+ horizontalPadding: [
9947
+ { selector: () => '.codes-box', property: 'padding-left' },
9948
+ { selector: () => '.codes-box', property: 'padding-right' },
9949
+ { selector: () => '.buttons-row', property: 'padding-left' },
9950
+ { selector: () => '.buttons-row', property: 'padding-right' },
9951
+ ],
9952
+ errorBorderColor: {
9953
+ selector: () => '.codes-box',
9954
+ property: 'border-color',
9955
+ },
9956
+ errorMessageColor: {
9957
+ selector: () => '.error-message',
9958
+ property: 'color',
9959
+ },
9960
+ errorMessageFontSize: {
9961
+ selector: () => '.error-message',
9962
+ property: 'font-size',
9963
+ },
9964
+ errorMessageFontWeight: {
9965
+ selector: () => '.error-message',
9966
+ property: 'font-weight',
9967
+ },
9968
+ errorMessageFontFamily: {
9969
+ selector: () => '.error-message',
9970
+ property: 'font-family',
9971
+ },
9972
+ boxBorderRadius: {
9973
+ selector: () => '.codes-box',
9974
+ property: 'border-radius',
9975
+ },
9976
+ errorBorderWidth: {
9977
+ selector: () => '.codes-box',
9978
+ property: 'border-width',
9979
+ },
9980
+ boxBorderStyle: {
9981
+ selector: () => '.codes-box',
9982
+ property: 'border-style',
9983
+ },
9686
9984
  },
9687
9985
  }),
9986
+ // also inject the shared input-wrapper theme onto our host (like the input components do),
9987
+ // so the error message + border track the console "customize inputs" tokens
9988
+ createStyleMixin$1({
9989
+ componentNameOverride: getComponentName('input-wrapper'),
9990
+ }),
9688
9991
  createDynamicDataMixin$1({
9689
9992
  itemRenderer: itemRenderer$5,
9690
9993
  rerenderAttrsList: ['variant', 'mode'],
@@ -9703,9 +10006,33 @@ const recoveryCodes = {
9703
10006
  [vars$T.hostDirection]: globalRefs$t.direction,
9704
10007
  [vars$T.iconColor]: useVar$1(textVars$6.textColor),
9705
10008
  [vars$T.iconSize]: useVar$1(textVars$6.fontSize),
10009
+ // gap between the codes and the icon column
9706
10010
  [vars$T.iconGap]: '8px',
10011
+ // gap between the stacked copy and download icons
10012
+ [vars$T.iconStackGap]: '16px',
10013
+ // gap between the copy and download buttons (buttons variant)
10014
+ [vars$T.buttonsGap]: '8px',
10015
+ // gap between the codes, the error message and the buttons
10016
+ [vars$T.sectionsGap]: '8px',
9707
10017
  [vars$T.bulletGap]: '0.35em',
9708
10018
 
10019
+ // no padding by default so the current look is unchanged; the editor overrides per instance
10020
+ [vars$T.verticalPadding]: '0',
10021
+ [vars$T.horizontalPadding]: '0',
10022
+
10023
+ // error ring is transparent until the component is marked invalid (see _invalid below).
10024
+ // message color/font + border come from the shared input-wrapper tokens, so customizing
10025
+ // inputs in the console updates the recovery-codes error styling from the same place.
10026
+ [vars$T.errorBorderColor]: 'transparent',
10027
+ [vars$T.errorBorderWidth]: refs$1.borderWidth,
10028
+ [vars$T.boxBorderRadius]: refs$1.borderRadius,
10029
+ // local default; can later map to the input-wrapper border style if it exposes one
10030
+ [vars$T.boxBorderStyle]: 'solid',
10031
+ [vars$T.errorMessageColor]: refs$1.errorMessageTextColor,
10032
+ [vars$T.errorMessageFontSize]: refs$1.errorMessageFontSize,
10033
+ [vars$T.errorMessageFontWeight]: refs$1.errorMessageFontWeight,
10034
+ [vars$T.errorMessageFontFamily]: refs$1.fontFamily,
10035
+
9709
10036
  textAlign: {
9710
10037
  right: { [vars$T.textAlign]: 'flex-end' },
9711
10038
  left: { [vars$T.textAlign]: 'flex-start' },
@@ -9715,6 +10042,11 @@ const recoveryCodes = {
9715
10042
  _fullWidth: {
9716
10043
  [vars$T.hostWidth]: '100%',
9717
10044
  },
10045
+
10046
+ // reflected `invalid` attribute (set by the component on a failed submit)
10047
+ _invalid: {
10048
+ [vars$T.errorBorderColor]: refs$1.errorMessageTextColor,
10049
+ },
9718
10050
  };
9719
10051
 
9720
10052
  var recoveryCodes$1 = /*#__PURE__*/Object.freeze({
@@ -19462,25 +19794,80 @@ var numberField$1 = /*#__PURE__*/Object.freeze({
19462
19794
  vars: vars$E
19463
19795
  });
19464
19796
 
19797
+ // Kept in its own module (with no imports) so it can be unit tested without
19798
+ // pulling in the whole component / vaadin dependency chain.
19799
+
19800
+ const asciiPattern = "^[\\w\\.\\%\\+\\-']+@[\\w\\.\\-]+\\.[A-Za-z]{2,}$";
19801
+
19802
+ const unicodePattern =
19803
+ "^[\\p{L}\\p{M}\\p{N}._%+'\\-]+@[\\p{L}\\p{M}\\p{N}.\\-]+\\.[\\p{L}\\p{M}][\\p{L}\\p{M}\\p{N}\\-]+$";
19804
+
19465
19805
  const componentName$P = getComponentName$1('email-field');
19466
19806
 
19467
- const defaultPattern = "^[\\w\\.\\%\\+\\-']+@[\\w\\.\\-]+\\.[A-Za-z]{2,}$";
19468
19807
  const defaultAutocomplete = 'username';
19808
+ const defaultInputMode = 'email';
19469
19809
 
19470
19810
  const customMixin$8 = (superclass) =>
19471
19811
  class EmailFieldMixinClass extends superclass {
19812
+ // On by default - `accept-unicode="false"` restores the pre-unicode field:
19813
+ // native `type="email"` validation and the ASCII pattern.
19814
+ //
19815
+ // Read once, in `init`. Changing the attribute afterwards has no effect; it
19816
+ // is static configuration, which is how every consumer sets it.
19817
+ get acceptsUnicode() {
19818
+ return this.getAttribute('accept-unicode') !== 'false';
19819
+ }
19820
+
19472
19821
  init() {
19473
19822
  super.init?.();
19474
19823
 
19475
19824
  // we need to set the pattern on the base element because vaadin-email-field is overriding it
19476
- this.baseElement.setAttribute('pattern', defaultPattern);
19825
+ this.baseElement.setAttribute('pattern', asciiPattern);
19477
19826
 
19478
19827
  if (!this.getAttribute('autocomplete')) {
19479
19828
  this.setAttribute('autocomplete', defaultAutocomplete);
19480
19829
  }
19481
19830
 
19482
19831
  this.createExternalInput();
19832
+
19833
+ [this.inputElement, this.externalInput].filter(Boolean).forEach((input) => {
19834
+ input.addEventListener('change', this.stripSurroundingWhitespace);
19835
+ });
19836
+
19837
+ if (this.acceptsUnicode) {
19838
+ this.enableUnicode();
19839
+ }
19483
19840
  }
19841
+
19842
+ enableUnicode() {
19843
+ this.baseElement.setAttribute('pattern', unicodePattern);
19844
+
19845
+ // native `type="email"` validation is ASCII only and sets typeMismatch
19846
+ // before `pattern` is consulted, so `text` is what lets the pattern decide.
19847
+ // Through vaadin's `_setType` because `type` is a readOnly property bound
19848
+ // to the inner input - assigning `input.type` is re-applied over.
19849
+ this.baseElement._setType('text');
19850
+
19851
+ if (this.externalInput) {
19852
+ this.externalInput.type = 'text';
19853
+ }
19854
+
19855
+ // `type="text"` loses the email keyboard on mobile
19856
+ if (!this.getAttribute('inputmode')) {
19857
+ this.setAttribute('inputmode', defaultInputMode);
19858
+ }
19859
+
19860
+ this.externalInput?.setAttribute('inputmode', defaultInputMode);
19861
+ }
19862
+
19863
+ stripSurroundingWhitespace = (e) => {
19864
+ const trimmed = e.target.value.trim();
19865
+ if (trimmed === e.target.value) return;
19866
+ this.value = trimmed;
19867
+
19868
+ // validity is cached and was computed against the untrimmed value
19869
+ this.inputElement.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
19870
+ };
19484
19871
  };
19485
19872
 
19486
19873
  const EmailFieldClass = compose$1(