@authhero/widget 0.37.1 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/authhero-widget/authhero-widget.esm.js +1 -1
  2. package/dist/authhero-widget/index.esm.js +1 -1
  3. package/dist/authhero-widget/p-c2744f87.entry.js +1 -0
  4. package/dist/authhero-widget/p-f94037cd.entry.js +1 -0
  5. package/dist/cjs/authhero-node.cjs.entry.js +526 -13
  6. package/dist/cjs/authhero-widget.cjs.entry.js +79 -10
  7. package/dist/cjs/authhero-widget.cjs.js +1 -1
  8. package/dist/cjs/index.cjs.js +1 -1
  9. package/dist/cjs/loader.cjs.js +1 -1
  10. package/dist/collection/components/authhero-node/authhero-node.css +94 -0
  11. package/dist/collection/components/authhero-node/authhero-node.js +302 -13
  12. package/dist/collection/components/authhero-widget/authhero-widget.js +98 -10
  13. package/dist/collection/utils/date-format.js +244 -0
  14. package/dist/components/authhero-node.js +1 -1
  15. package/dist/components/authhero-widget.js +1 -1
  16. package/dist/components/index.js +1 -1
  17. package/dist/components/p-B_R7ma-J.js +1 -0
  18. package/dist/esm/authhero-node.entry.js +526 -13
  19. package/dist/esm/authhero-widget.entry.js +79 -10
  20. package/dist/esm/authhero-widget.js +1 -1
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/loader.js +1 -1
  23. package/dist/types/components/authhero-node/authhero-node.d.ts +83 -0
  24. package/dist/types/components/authhero-widget/authhero-widget.d.ts +19 -0
  25. package/dist/types/components.d.ts +18 -0
  26. package/dist/types/utils/date-format.d.ts +52 -0
  27. package/hydrate/index.js +609 -24
  28. package/hydrate/index.mjs +609 -24
  29. package/package.json +2 -2
  30. package/dist/authhero-widget/p-cdfc4555.entry.js +0 -1
  31. package/dist/authhero-widget/p-e95c436f.entry.js +0 -1
  32. package/dist/components/p-BP46GHTc.js +0 -1
package/hydrate/index.mjs CHANGED
@@ -5498,7 +5498,252 @@ function getCountryByCode(code) {
5498
5498
  countries.find((c) => c.code === "US"));
5499
5499
  }
5500
5500
 
5501
- const authheroNodeCss = () => `:host{display:block}.input-wrapper{display:flex;flex-direction:column;position:relative;margin-top:8px;margin-bottom:16px}.input-container{position:relative;width:100%}.input-label{position:absolute;left:16px;top:50%;transform:translateY(-50%);font-size:var(--ah-font-size-label, 16px);font-weight:var(--ah-font-weight-label, 400);color:var(--ah-color-text-muted, #65676e);pointer-events:none;transition:all 0.15s ease-out;background-color:transparent;padding:0;z-index:1}.input-label.floating,.input-field:focus+.input-label,.input-field:not(:placeholder-shown)+.input-label,select.input-field+.input-label,input[type="date"].input-field+.input-label{top:-8px;transform:translateY(0);font-size:12px;background-color:var(--ah-color-bg, #ffffff);padding:0 4px;left:12px;color:var(--ah-color-text-muted, #65676e)}.input-field:focus+.input-label{color:var(--ah-color-primary, #635dff)}.required{color:var(--ah-color-error, #d03c38);margin-left:2px}.input-field{width:100%;padding:16px;font-size:16px;font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);outline:none;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out;box-sizing:border-box}.input-field::placeholder{color:transparent}.input-field:hover{border-color:var(--ah-color-border-hover, #65676e)}.input-field:focus{border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.input-field.has-error{border-color:var(--ah-color-error, #d03c38)}.input-field.has-error:focus{box-shadow:inset 0 0 0 1px var(--ah-color-error, #d03c38)}.input-field:disabled{background-color:var(--ah-color-bg-disabled, #f5f5f5);border-color:var(--ah-color-border-disabled, #e0e1e3);cursor:not-allowed;opacity:0.7}.password-container{position:relative;display:flex;align-items:center}.password-container .input-field{padding-right:48px}.password-toggle{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:var(--ah-color-icon, var(--ah-color-text-muted, #65676e));transition:color 0.15s ease}.password-toggle:hover{color:var(--ah-color-icon, var(--ah-color-text, #1e212a))}.password-toggle svg{width:20px;height:20px}.error-text{font-size:12px;color:var(--ah-color-error, #d03c38);margin-top:4px;line-height:1.4}.helper-text{font-size:12px;color:var(--ah-color-text-muted, #65676e);margin-top:4px;line-height:1.4}.field-link{display:block;text-align:left;margin-top:8px;margin-bottom:16px}.field-link a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);font-weight:var(--ah-font-weight-link, 400)}.field-link a:hover{text-decoration:underline}.checkbox-wrapper{display:flex;align-items:flex-start;gap:10px;cursor:pointer;margin-bottom:16px}.checkbox-wrapper input[type="checkbox"]{width:18px;height:18px;margin:0;accent-color:var(--ah-color-primary, #635dff);cursor:pointer;flex-shrink:0}.checkbox-label{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);color:var(--ah-color-text, #1e212a);line-height:1.5}.btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:14px 20px;font-size:var(--ah-font-size-btn, 16px);font-weight:var(--ah-font-weight-btn, 400);font-family:inherit;line-height:1.25;text-align:center;text-decoration:none;border:none;border-radius:var(--ah-btn-radius, 3px);cursor:pointer;transition:background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;box-sizing:border-box}.btn:disabled{opacity:0.6;cursor:not-allowed}.btn:not(:disabled):active{transform:scale(0.98)}.btn:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-primary, #635dff));outline-offset:2px}.btn-primary{background-color:var(--ah-color-primary, #635dff);color:var(--ah-color-text-on-primary, #ffffff);margin-top:12px}.btn-primary:not(:disabled):hover{background-color:var( --ah-color-primary-hover, color-mix(in srgb, var(--ah-color-primary, #635dff) 85%, black) )}.btn-secondary{background-color:var(--ah-color-bg, #ffffff);color:var(--ah-btn-secondary-text, var(--ah-color-text, #1e212a));border:var(--ah-btn-border-width, 1px) solid var(--ah-btn-secondary-border, var(--ah-color-border, #c9cace))}.btn-secondary:not(:disabled):hover{background-color:var(--ah-color-bg-hover, #f5f5f5);border-color:var(--ah-color-border-hover, #65676e)}.btn-link{background:none;border:none;color:var(--ah-color-link, #635dff);padding:8px 0;font-weight:var(--ah-font-weight-link, 400);text-decoration:none}.btn-link:hover{text-decoration:underline}.phone-input-wrapper{display:flex;align-items:stretch;gap:0;width:100%}.country-select{flex-shrink:0;width:auto;padding:12px 8px;font-size:var(--ah-font-size-body, 16px);font-family:var(--ah-font-family, inherit);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-right:none;border-radius:var(--ah-input-radius, 4px) 0 0 var(--ah-input-radius, 4px);background-color:var(--ah-color-input-bg, var(--ah-color-surface, #fff));color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center;padding-right:22px}.country-select:focus{outline:none;border-color:var(--ah-color-primary, #635dff);box-shadow:0 0 0 1px var(--ah-color-primary, #635dff)}.country-select:disabled{opacity:0.5;cursor:not-allowed}.phone-input-wrapper .input-container{flex:1;min-width:0}.phone-input-wrapper .input-field{border-radius:0 var(--ah-input-radius, 4px) var(--ah-input-radius, 4px) 0}.phone-input-wrapper .input-label.floating,.phone-input-wrapper .input-field:focus+.input-label,.phone-input-wrapper .input-field:not(:placeholder-shown)+.input-label{left:8px}.social-buttons{display:flex;flex-direction:column;gap:12px}.btn-social{display:flex;align-items:center;justify-content:center;gap:12px;position:relative}.btn-social-badge{position:absolute;right:12px;top:0;transform:translateY(-50%);font-size:11px;font-weight:500;line-height:1;padding:4px 9px;border-radius:999px;background:var(--ah-color-last-used, var(--ah-color-primary, #635dff));color:var(--ah-color-last-used-text, var(--ah-color-text-on-primary, #fff));white-space:nowrap;pointer-events:none}.btn-social.btn-social-last-used{border-color:var(--ah-color-last-used, var(--ah-color-primary, #635dff));box-shadow:inset 0 0 0 1px var(--ah-color-last-used, var(--ah-color-primary, #635dff))}.input-badge{position:absolute;right:12px;top:0;transform:translateY(-50%);font-size:11px;font-weight:500;line-height:1;padding:4px 9px;border-radius:999px;background:var(--ah-color-last-used, var(--ah-color-primary, #635dff));color:var(--ah-color-last-used-text, var(--ah-color-text-on-primary, #fff));white-space:nowrap;pointer-events:none;z-index:2}.btn-social-content{display:flex;flex-direction:column;align-items:center;text-align:center}.btn-social-subtitle{font-size:12px;font-style:italic;opacity:0.8;margin-top:2px}.btn-social-subtitle:empty{display:none}.social-icon{width:20px;height:20px;flex-shrink:0}@media (max-width: 480px){.social-buttons:has(.btn-social:nth-child(3)){flex-direction:row;flex-wrap:nowrap;justify-content:stretch;gap:8px}.social-buttons:has(.btn-social:nth-child(3)) .btn-social{width:auto;min-width:0;padding:12px;flex:1 1 0}.social-buttons:has(.btn-social:nth-child(3)) .btn-social span{display:none}.social-buttons:has(.btn-social:nth-child(3)) .social-icon{width:24px;height:24px}}.btn-icon{width:20px;height:20px;flex-shrink:0}.btn-icon img{width:100%;height:100%;object-fit:contain}.text-title{font-size:var(--ah-font-size-title, 20px);font-weight:var(--ah-font-weight-title, 400);color:var(--ah-color-text-header, var(--ah-color-header, #1e212a));margin:8px 0;line-height:1.3}.text-title.text-success{color:var(--ah-color-success, #13a769)}.text-description{font-size:var(--ah-font-size-subtitle, 14px);font-weight:var(--ah-font-weight-subtitle, 400);color:var(--ah-color-text-muted, #65676e);margin:4px 0;line-height:1.5}.image{display:block;max-width:100%;height:auto;border-radius:4px}.image-centered{margin:0 auto 16px;width:52px;height:52px;object-fit:contain}.link{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);transition:color 0.15s ease}.link:hover{text-decoration:underline}.link:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-link, #635dff));outline-offset:2px;border-radius:2px}.node-error{padding:12px 16px;background-color:var(--ah-color-error-bg, #ffeaea);color:var(--ah-color-error, #d03c38);border-left:3px solid var(--ah-color-error, #d03c38);border-radius:0;font-size:14px;margin-bottom:16px}.node-success{padding:12px 16px;background-color:var(--ah-color-success-bg, #e6f9f1);color:var(--ah-color-success, #13a769);border-left:3px solid var(--ah-color-success, #13a769);border-radius:0;font-size:14px;margin-bottom:16px}.divider{display:flex;align-items:center;text-align:center;margin:16px 0}.divider::before,.divider::after{content:"";flex:1;border-bottom:1px solid var(--ah-color-border-muted, #c9cace)}.divider-text{padding:0 10px;font-size:12px;font-weight:400;color:var(--ah-color-text-muted, #65676e);text-transform:uppercase;letter-spacing:0.5px}.rich-text{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);line-height:1.5;color:var(--ah-color-text, #1e212a)}.rich-text a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);transition:color 0.15s ease}.rich-text a:hover{text-decoration:underline}.rich-text .terms-text{margin-top:16px;text-align:center;font-size:12px;color:var(--ah-color-text-muted, #65676e)}.rich-text .terms-text a{font-size:12px}.rich-text .forgot-password-link{text-align:right;font-size:13px;margin-top:4px}.rich-text .forgot-password-link a{font-size:13px;color:var(--ah-color-link, #635dff)}.rich-text .passwordless-link{margin-top:16px;text-align:center;font-size:14px}.rich-text .passwordless-link a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none)}.rich-text .passwordless-link a:hover{text-decoration:underline}.rich-text .signup-link{margin-top:16px;text-align:center;font-size:14px;color:var(--ah-color-text, #1e212a)}.code-field{position:relative}.code-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:none;background:transparent;color:transparent;caret-color:transparent;font-family:inherit;letter-spacing:0;outline:none;cursor:text;-webkit-text-fill-color:transparent}.code-boxes{display:flex;align-items:center;gap:var(--ah-code-group-gap, 12px);justify-content:center}.code-group{display:flex;gap:0}.code-separator{color:var(--ah-color-text-muted, #65676e);font-size:var(--ah-code-font-size, 24px);user-select:none}.code-box-placeholder{color:var(--ah-color-text-muted, #65676e);opacity:0.4}.code-box{position:relative;flex:0 0 auto;width:var(--ah-code-box-width, 48px);height:var(--ah-code-box-height, 56px);display:flex;align-items:center;justify-content:center;font-size:var(--ah-code-font-size, 24px);font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:0;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out;box-sizing:border-box}.code-box+.code-box{margin-left:calc(-1 * var(--ah-input-border-width, 1px))}.code-group .code-box:first-child{border-top-left-radius:var(--ah-input-radius, 3px);border-bottom-left-radius:var(--ah-input-radius, 3px)}.code-group .code-box:last-child{border-top-right-radius:var(--ah-input-radius, 3px);border-bottom-right-radius:var(--ah-input-radius, 3px)}.code-field:focus-within .code-box.is-active{z-index:1;border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.code-field.has-error .code-box{border-color:var(--ah-color-error, #d03c38)}.code-input:disabled{cursor:not-allowed}.code-input:disabled~.code-boxes .code-box{background-color:var(--ah-color-bg-disabled, #f5f5f5);border-color:var(--ah-color-border-disabled, #e0e1e3)}`;
5501
+ /**
5502
+ * Locale-aware layout for the segmented DATE input.
5503
+ *
5504
+ * The segment order is resolved from a static table rather than from
5505
+ * `Intl.DateTimeFormat`, deliberately: the widget is server-rendered and then
5506
+ * hydrated in the browser, and the two runtimes do not ship the same ICU data
5507
+ * (a Workers runtime commonly falls back to en-US ordering). A static table
5508
+ * gives the server and the client the same answer, so the hydrated DOM matches
5509
+ * the rendered one.
5510
+ */
5511
+ /** Languages that write dates big-endian: year, month, day. */
5512
+ const YEAR_FIRST_LANGUAGES = new Set([
5513
+ "bo",
5514
+ "dz",
5515
+ "hu",
5516
+ "ja",
5517
+ "ko",
5518
+ "lt",
5519
+ "mn",
5520
+ "sv",
5521
+ "ug",
5522
+ "zh",
5523
+ ]);
5524
+ /**
5525
+ * Regions that write month before day. Month-first is essentially a US
5526
+ * convention plus the places that adopted it.
5527
+ */
5528
+ const MONTH_FIRST_REGIONS = new Set(["us", "ph", "fm", "mh", "pw", "as", "gu"]);
5529
+ /**
5530
+ * Locales whose language is normally year-first but which write dates
5531
+ * day-first in a particular region — Finland-Swedish is d.m.yyyy, unlike
5532
+ * Sweden-Swedish.
5533
+ */
5534
+ const DAY_FIRST_TAGS = new Set(["sv-fi"]);
5535
+ const DEFAULT_TOKENS = {
5536
+ day: "DD",
5537
+ month: "MM",
5538
+ year: "YYYY",
5539
+ };
5540
+ /**
5541
+ * Placeholder tokens in the language's own convention — a German field reads
5542
+ * TT.MM.JJJJ, a French one JJ/MM/AAAA. Languages without an entry fall back to
5543
+ * the English DD/MM/YYYY tokens.
5544
+ */
5545
+ const TOKENS_BY_LANGUAGE = {
5546
+ cs: { day: "DD", month: "MM", year: "RRRR" },
5547
+ da: { day: "DD", month: "MM", year: "ÅÅÅÅ" },
5548
+ de: { day: "TT", month: "MM", year: "JJJJ" },
5549
+ es: { day: "DD", month: "MM", year: "AAAA" },
5550
+ fi: { day: "PP", month: "KK", year: "VVVV" },
5551
+ fr: { day: "JJ", month: "MM", year: "AAAA" },
5552
+ hu: { day: "NN", month: "HH", year: "ÉÉÉÉ" },
5553
+ it: { day: "GG", month: "MM", year: "AAAA" },
5554
+ nb: { day: "DD", month: "MM", year: "ÅÅÅÅ" },
5555
+ nl: { day: "DD", month: "MM", year: "JJJJ" },
5556
+ nn: { day: "DD", month: "MM", year: "ÅÅÅÅ" },
5557
+ no: { day: "DD", month: "MM", year: "ÅÅÅÅ" },
5558
+ pl: { day: "DD", month: "MM", year: "RRRR" },
5559
+ pt: { day: "DD", month: "MM", year: "AAAA" },
5560
+ ru: { day: "ДД", month: "ММ", year: "ГГГГ" },
5561
+ sk: { day: "DD", month: "MM", year: "RRRR" },
5562
+ sv: { day: "DD", month: "MM", year: "ÅÅÅÅ" },
5563
+ tr: { day: "GG", month: "AA", year: "YYYY" },
5564
+ uk: { day: "ДД", month: "ММ", year: "РРРР" },
5565
+ };
5566
+ /**
5567
+ * Separator by language, for languages that do not use the defaults ("/" for
5568
+ * day- or month-first, "-" for year-first). Mostly the locales that write
5569
+ * 14.08.2026 with dots — German, the Nordics minus Sweden, and most of
5570
+ * Central/Eastern Europe.
5571
+ */
5572
+ const SEPARATOR_BY_LANGUAGE = {
5573
+ bg: ".",
5574
+ cs: ".",
5575
+ da: ".",
5576
+ de: ".",
5577
+ et: ".",
5578
+ fi: ".",
5579
+ hr: ".",
5580
+ hu: ".",
5581
+ is: ".",
5582
+ ja: "/",
5583
+ ko: ".",
5584
+ lv: ".",
5585
+ mk: ".",
5586
+ nb: ".",
5587
+ nl: "-",
5588
+ nn: ".",
5589
+ no: ".",
5590
+ pl: ".",
5591
+ ro: ".",
5592
+ ru: ".",
5593
+ sk: ".",
5594
+ sl: ".",
5595
+ sr: ".",
5596
+ tr: ".",
5597
+ uk: ".",
5598
+ zh: "/",
5599
+ };
5600
+ /** Full-tag overrides — Finland-Swedish writes 14.8.2026 with dots. */
5601
+ const SEPARATOR_BY_TAG = {
5602
+ "sv-fi": ".",
5603
+ };
5604
+ /**
5605
+ * Split a BCP-47 tag into a lowercase language subtag and, when present, a
5606
+ * two-letter region subtag. Script and variant subtags are ignored.
5607
+ */
5608
+ function parseTag(locale) {
5609
+ const tag = (locale || "").toLowerCase().replace(/_/g, "-");
5610
+ const parts = tag.split("-").filter(Boolean);
5611
+ const language = parts[0] || "";
5612
+ const region = parts.slice(1).find((part) => /^[a-z]{2}$/.test(part));
5613
+ return { language, region };
5614
+ }
5615
+ /**
5616
+ * Resolve the segment order for a locale. Falls back to day-month-year, which
5617
+ * is what most of the world writes.
5618
+ */
5619
+ function getDateOrder(locale) {
5620
+ const { language, region } = parseTag(locale);
5621
+ if (region && MONTH_FIRST_REGIONS.has(region)) {
5622
+ return ["month", "day", "year"];
5623
+ }
5624
+ if (region && DAY_FIRST_TAGS.has(`${language}-${region}`)) {
5625
+ return ["day", "month", "year"];
5626
+ }
5627
+ if (YEAR_FIRST_LANGUAGES.has(language)) {
5628
+ return ["year", "month", "day"];
5629
+ }
5630
+ // A bare "en" carries no region — treat it the way Intl does and assume
5631
+ // en-US. Regional English (en-gb, en-au, …) falls through to day-first.
5632
+ if (language === "en" && !region) {
5633
+ return ["month", "day", "year"];
5634
+ }
5635
+ return ["day", "month", "year"];
5636
+ }
5637
+ /**
5638
+ * Parse an explicit format string from the component config, e.g.
5639
+ * "DD/MM/YYYY" or "YYYY-MM-DD". Returns null when the string does not name
5640
+ * all three segments.
5641
+ */
5642
+ function parseFormat(format) {
5643
+ const matches = format.match(/y+|m+|d+/gi);
5644
+ if (!matches)
5645
+ return null;
5646
+ const order = [];
5647
+ const tokens = { ...DEFAULT_TOKENS };
5648
+ for (const match of matches) {
5649
+ const initial = match[0].toLowerCase();
5650
+ const segment = initial === "y" ? "year" : initial === "m" ? "month" : "day";
5651
+ if (order.includes(segment))
5652
+ continue;
5653
+ order.push(segment);
5654
+ // A "YY" format still collects a four-digit year — the segment holds four
5655
+ // characters and `toIsoDate` rejects anything shorter, with a two-digit
5656
+ // entry expanded on blur. Showing "YY" as the placeholder would promise an
5657
+ // input the field does not accept, so short year tokens are widened.
5658
+ tokens[segment] =
5659
+ segment === "year" && match.length < 4 ? DEFAULT_TOKENS.year : match;
5660
+ }
5661
+ if (order.length !== 3)
5662
+ return null;
5663
+ const separator = format.match(/[^a-z\s]/i)?.[0] ?? "/";
5664
+ return { order, tokens, separator };
5665
+ }
5666
+ /**
5667
+ * Resolve the full layout for a DATE field. An explicit `config.format` wins;
5668
+ * otherwise the locale decides the order.
5669
+ */
5670
+ function getDateLayout(format, locale) {
5671
+ if (format) {
5672
+ const parsed = parseFormat(format);
5673
+ if (parsed)
5674
+ return parsed;
5675
+ }
5676
+ const { language, region } = parseTag(locale);
5677
+ const order = getDateOrder(locale);
5678
+ const separator = (region ? SEPARATOR_BY_TAG[`${language}-${region}`] : undefined) ??
5679
+ SEPARATOR_BY_LANGUAGE[language] ??
5680
+ (order[0] === "year" ? "-" : "/");
5681
+ return {
5682
+ order,
5683
+ tokens: TOKENS_BY_LANGUAGE[language] ?? DEFAULT_TOKENS,
5684
+ separator,
5685
+ };
5686
+ }
5687
+ /** Number of days in a given month, honouring leap years. */
5688
+ function daysInMonth(year, month) {
5689
+ if (month < 1 || month > 12)
5690
+ return 0;
5691
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
5692
+ }
5693
+ /** Split an ISO "YYYY-MM-DD" value into its segments. */
5694
+ function parseIsoDate(value) {
5695
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec((value || "").trim());
5696
+ if (!match)
5697
+ return null;
5698
+ return { year: match[1], month: match[2], day: match[3] };
5699
+ }
5700
+ /**
5701
+ * Build an ISO "YYYY-MM-DD" string from segment values. Returns "" unless all
5702
+ * three segments are complete and form a real calendar date — a half-filled or
5703
+ * impossible date (31 February) is not a value worth submitting.
5704
+ */
5705
+ function toIsoDate(segments) {
5706
+ const { year, month, day } = segments;
5707
+ if (year.length !== 4 || month.length === 0 || day.length === 0)
5708
+ return "";
5709
+ const y = Number(year);
5710
+ const m = Number(month);
5711
+ const d = Number(day);
5712
+ if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) {
5713
+ return "";
5714
+ }
5715
+ if (m < 1 || m > 12)
5716
+ return "";
5717
+ if (d < 1 || d > daysInMonth(y, m))
5718
+ return "";
5719
+ return `${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
5720
+ }
5721
+ /**
5722
+ * Expand a two-digit year into the most recent matching year at or before
5723
+ * `anchorYear` — "85" becomes 1985, "05" becomes 2005. Anything that is not
5724
+ * exactly two digits is returned unchanged, so a fully typed year is never
5725
+ * second-guessed.
5726
+ */
5727
+ function expandTwoDigitYear(year, anchorYear) {
5728
+ if (!/^\d{2}$/.test(year))
5729
+ return year;
5730
+ const century = Math.floor(anchorYear / 100) * 100;
5731
+ const candidate = century + Number(year);
5732
+ return String(candidate > anchorYear ? candidate - 100 : candidate);
5733
+ }
5734
+ /**
5735
+ * The latest year a two-digit entry may expand to. `config.max` pins it when
5736
+ * the field has an upper bound; otherwise we assume a date in the past (the
5737
+ * birthdate case) and anchor on the current year.
5738
+ */
5739
+ function resolveYearAnchor(max, today) {
5740
+ const parsed = parseIsoDate(max);
5741
+ if (parsed)
5742
+ return Number(parsed.year);
5743
+ return today.getUTCFullYear();
5744
+ }
5745
+
5746
+ const authheroNodeCss = () => `:host{display:block}.input-wrapper{display:flex;flex-direction:column;position:relative;margin-top:8px;margin-bottom:16px}.input-container{position:relative;width:100%}.input-label{position:absolute;left:16px;top:50%;transform:translateY(-50%);font-size:var(--ah-font-size-label, 16px);font-weight:var(--ah-font-weight-label, 400);color:var(--ah-color-text-muted, #65676e);pointer-events:none;transition:all 0.15s ease-out;background-color:transparent;padding:0;z-index:1}.input-label.floating,.input-field:focus+.input-label,.input-field:not(:placeholder-shown)+.input-label,select.input-field+.input-label,input[type="date"].input-field+.input-label{top:-8px;transform:translateY(0);font-size:12px;background-color:var(--ah-color-bg, #ffffff);padding:0 4px;left:12px;color:var(--ah-color-text-muted, #65676e)}.input-field:focus+.input-label{color:var(--ah-color-primary, #635dff)}.required{color:var(--ah-color-error, #d03c38);margin-left:2px}.input-field{width:100%;padding:16px;font-size:16px;font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);outline:none;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out;box-sizing:border-box}.input-field::placeholder{color:transparent}.input-field:hover{border-color:var(--ah-color-border-hover, #65676e)}.input-field:focus{border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.input-field.has-error{border-color:var(--ah-color-error, #d03c38)}.input-field.has-error:focus{box-shadow:inset 0 0 0 1px var(--ah-color-error, #d03c38)}.input-field:disabled{background-color:var(--ah-color-bg-disabled, #f5f5f5);border-color:var(--ah-color-border-disabled, #e0e1e3);cursor:not-allowed;opacity:0.7}.password-container{position:relative;display:flex;align-items:center}.password-container .input-field{padding-right:48px}.password-toggle{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:var(--ah-color-icon, var(--ah-color-text-muted, #65676e));transition:color 0.15s ease}.password-toggle:hover{color:var(--ah-color-icon, var(--ah-color-text, #1e212a))}.password-toggle svg{width:20px;height:20px}.error-text{font-size:12px;color:var(--ah-color-error, #d03c38);margin-top:4px;line-height:1.4}.helper-text{font-size:12px;color:var(--ah-color-text-muted, #65676e);margin-top:4px;line-height:1.4}.field-link{display:block;text-align:left;margin-top:8px;margin-bottom:16px}.field-link a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);font-weight:var(--ah-font-weight-link, 400)}.field-link a:hover{text-decoration:underline}.checkbox-wrapper{display:flex;align-items:flex-start;gap:10px;cursor:pointer;margin-bottom:16px}.checkbox-wrapper input[type="checkbox"]{width:18px;height:18px;margin:0;accent-color:var(--ah-color-primary, #635dff);cursor:pointer;flex-shrink:0}.checkbox-label{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);color:var(--ah-color-text, #1e212a);line-height:1.5}.btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:14px 20px;font-size:var(--ah-font-size-btn, 16px);font-weight:var(--ah-font-weight-btn, 400);font-family:inherit;line-height:1.25;text-align:center;text-decoration:none;border:none;border-radius:var(--ah-btn-radius, 3px);cursor:pointer;transition:background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;box-sizing:border-box}.btn:disabled{opacity:0.6;cursor:not-allowed}.btn:not(:disabled):active{transform:scale(0.98)}.btn:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-primary, #635dff));outline-offset:2px}.btn-primary{background-color:var(--ah-color-primary, #635dff);color:var(--ah-color-text-on-primary, #ffffff);margin-top:12px}.btn-primary:not(:disabled):hover{background-color:var( --ah-color-primary-hover, color-mix(in srgb, var(--ah-color-primary, #635dff) 85%, black) )}.btn-secondary{background-color:var(--ah-color-bg, #ffffff);color:var(--ah-btn-secondary-text, var(--ah-color-text, #1e212a));border:var(--ah-btn-border-width, 1px) solid var(--ah-btn-secondary-border, var(--ah-color-border, #c9cace))}.btn-secondary:not(:disabled):hover{background-color:var(--ah-color-bg-hover, #f5f5f5);border-color:var(--ah-color-border-hover, #65676e)}.btn-link{background:none;border:none;color:var(--ah-color-link, #635dff);padding:8px 0;font-weight:var(--ah-font-weight-link, 400);text-decoration:none}.btn-link:hover{text-decoration:underline}.phone-input-wrapper{display:flex;align-items:stretch;gap:0;width:100%}.country-select{flex-shrink:0;width:auto;padding:12px 8px;font-size:var(--ah-font-size-body, 16px);font-family:var(--ah-font-family, inherit);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-right:none;border-radius:var(--ah-input-radius, 4px) 0 0 var(--ah-input-radius, 4px);background-color:var(--ah-color-input-bg, var(--ah-color-surface, #fff));color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center;padding-right:22px}.country-select:focus{outline:none;border-color:var(--ah-color-primary, #635dff);box-shadow:0 0 0 1px var(--ah-color-primary, #635dff)}.country-select:disabled{opacity:0.5;cursor:not-allowed}.phone-input-wrapper .input-container{flex:1;min-width:0}.phone-input-wrapper .input-field{border-radius:0 var(--ah-input-radius, 4px) var(--ah-input-radius, 4px) 0}.phone-input-wrapper .input-label.floating,.phone-input-wrapper .input-field:focus+.input-label,.phone-input-wrapper .input-field:not(:placeholder-shown)+.input-label{left:8px}.date-input{display:flex;align-items:center;width:100%;padding:16px;box-sizing:border-box;background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out}.date-input:hover{border-color:var(--ah-color-border-hover, #65676e)}.date-input:focus-within{border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.date-input.has-error{border-color:var(--ah-color-error, #d03c38)}.date-input.has-error:focus-within{box-shadow:inset 0 0 0 1px var(--ah-color-error, #d03c38)}@media (forced-colors: active){.date-input:focus-within,.date-input.has-error:focus-within{outline:2px solid Highlight;outline-offset:1px}}.date-segment{width:3.25ch;min-width:0;padding:0;font-size:16px;font-family:inherit;text-align:center;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background:transparent;border:none;outline:none}.date-segment[data-date-segment="year"]{width:5.5ch}.date-segment::placeholder{color:var(--ah-color-text-muted, #65676e);opacity:1}.date-segment:disabled{color:var(--ah-color-text-disabled, #a0a0a0);cursor:not-allowed}.date-input:has(.date-segment:disabled){background-color:var(--ah-color-bg-disabled, #f5f5f5)}.date-separator{padding:0 4px;font-size:16px;color:var(--ah-color-text-muted, #65676e);user-select:none}.social-buttons{display:flex;flex-direction:column;gap:12px}.btn-social{display:flex;align-items:center;justify-content:center;gap:12px;position:relative}.btn-social-badge{position:absolute;right:12px;top:0;transform:translateY(-50%);font-size:11px;font-weight:500;line-height:1;padding:4px 9px;border-radius:999px;background:var(--ah-color-last-used, var(--ah-color-primary, #635dff));color:var(--ah-color-last-used-text, var(--ah-color-text-on-primary, #fff));white-space:nowrap;pointer-events:none}.btn-social.btn-social-last-used{border-color:var(--ah-color-last-used, var(--ah-color-primary, #635dff));box-shadow:inset 0 0 0 1px var(--ah-color-last-used, var(--ah-color-primary, #635dff))}.input-badge{position:absolute;right:12px;top:0;transform:translateY(-50%);font-size:11px;font-weight:500;line-height:1;padding:4px 9px;border-radius:999px;background:var(--ah-color-last-used, var(--ah-color-primary, #635dff));color:var(--ah-color-last-used-text, var(--ah-color-text-on-primary, #fff));white-space:nowrap;pointer-events:none;z-index:2}.btn-social-content{display:flex;flex-direction:column;align-items:center;text-align:center}.btn-social-subtitle{font-size:12px;font-style:italic;opacity:0.8;margin-top:2px}.btn-social-subtitle:empty{display:none}.social-icon{width:20px;height:20px;flex-shrink:0}@media (max-width: 480px){.social-buttons:has(.btn-social:nth-child(3)){flex-direction:row;flex-wrap:nowrap;justify-content:stretch;gap:8px}.social-buttons:has(.btn-social:nth-child(3)) .btn-social{width:auto;min-width:0;padding:12px;flex:1 1 0}.social-buttons:has(.btn-social:nth-child(3)) .btn-social span{display:none}.social-buttons:has(.btn-social:nth-child(3)) .social-icon{width:24px;height:24px}}.btn-icon{width:20px;height:20px;flex-shrink:0}.btn-icon img{width:100%;height:100%;object-fit:contain}.text-title{font-size:var(--ah-font-size-title, 20px);font-weight:var(--ah-font-weight-title, 400);color:var(--ah-color-text-header, var(--ah-color-header, #1e212a));margin:8px 0;line-height:1.3}.text-title.text-success{color:var(--ah-color-success, #13a769)}.text-description{font-size:var(--ah-font-size-subtitle, 14px);font-weight:var(--ah-font-weight-subtitle, 400);color:var(--ah-color-text-muted, #65676e);margin:4px 0;line-height:1.5}.image{display:block;max-width:100%;height:auto;border-radius:4px}.image-centered{margin:0 auto 16px;width:52px;height:52px;object-fit:contain}.link{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);transition:color 0.15s ease}.link:hover{text-decoration:underline}.link:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-link, #635dff));outline-offset:2px;border-radius:2px}.node-error{padding:12px 16px;background-color:var(--ah-color-error-bg, #ffeaea);color:var(--ah-color-error, #d03c38);border-left:3px solid var(--ah-color-error, #d03c38);border-radius:0;font-size:14px;margin-bottom:16px}.node-success{padding:12px 16px;background-color:var(--ah-color-success-bg, #e6f9f1);color:var(--ah-color-success, #13a769);border-left:3px solid var(--ah-color-success, #13a769);border-radius:0;font-size:14px;margin-bottom:16px}.divider{display:flex;align-items:center;text-align:center;margin:16px 0}.divider::before,.divider::after{content:"";flex:1;border-bottom:1px solid var(--ah-color-border-muted, #c9cace)}.divider-text{padding:0 10px;font-size:12px;font-weight:400;color:var(--ah-color-text-muted, #65676e);text-transform:uppercase;letter-spacing:0.5px}.rich-text{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);line-height:1.5;color:var(--ah-color-text, #1e212a)}.rich-text a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);transition:color 0.15s ease}.rich-text a:hover{text-decoration:underline}.rich-text .terms-text{margin-top:16px;text-align:center;font-size:12px;color:var(--ah-color-text-muted, #65676e)}.rich-text .terms-text a{font-size:12px}.rich-text .forgot-password-link{text-align:right;font-size:13px;margin-top:4px}.rich-text .forgot-password-link a{font-size:13px;color:var(--ah-color-link, #635dff)}.rich-text .passwordless-link{margin-top:16px;text-align:center;font-size:14px}.rich-text .passwordless-link a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none)}.rich-text .passwordless-link a:hover{text-decoration:underline}.rich-text .signup-link{margin-top:16px;text-align:center;font-size:14px;color:var(--ah-color-text, #1e212a)}.code-field{position:relative}.code-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:none;background:transparent;color:transparent;caret-color:transparent;font-family:inherit;letter-spacing:0;outline:none;cursor:text;-webkit-text-fill-color:transparent}.code-boxes{display:flex;align-items:center;gap:var(--ah-code-group-gap, 12px);justify-content:center}.code-group{display:flex;gap:0}.code-separator{color:var(--ah-color-text-muted, #65676e);font-size:var(--ah-code-font-size, 24px);user-select:none}.code-box-placeholder{color:var(--ah-color-text-muted, #65676e);opacity:0.4}.code-box{position:relative;flex:0 0 auto;width:var(--ah-code-box-width, 48px);height:var(--ah-code-box-height, 56px);display:flex;align-items:center;justify-content:center;font-size:var(--ah-code-font-size, 24px);font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:0;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out;box-sizing:border-box}.code-box+.code-box{margin-left:calc(-1 * var(--ah-input-border-width, 1px))}.code-group .code-box:first-child{border-top-left-radius:var(--ah-input-radius, 3px);border-bottom-left-radius:var(--ah-input-radius, 3px)}.code-group .code-box:last-child{border-top-right-radius:var(--ah-input-radius, 3px);border-bottom-right-radius:var(--ah-input-radius, 3px)}.code-field:focus-within .code-box.is-active{z-index:1;border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.code-field.has-error .code-box{border-color:var(--ah-color-error, #d03c38)}.code-input:disabled{cursor:not-allowed}.code-input:disabled~.code-boxes .code-box{background-color:var(--ah-color-bg-disabled, #f5f5f5);border-color:var(--ah-color-border-disabled, #e0e1e3)}`;
5502
5747
 
5503
5748
  class AuthheroNode {
5504
5749
  constructor(hostRef) {
@@ -5516,6 +5761,12 @@ class AuthheroNode {
5516
5761
  * Whether the component is disabled.
5517
5762
  */
5518
5763
  disabled = false;
5764
+ /**
5765
+ * BCP-47 locale used for locale-dependent field layout (currently the
5766
+ * segment order of DATE fields). Resolved server-side so SSR and hydration
5767
+ * agree; falls back to day-month-year when absent.
5768
+ */
5769
+ locale;
5519
5770
  /**
5520
5771
  * Whether the password field is visible.
5521
5772
  */
@@ -5537,6 +5788,15 @@ class AuthheroNode {
5537
5788
  * When true, the value is emitted as-is without dial code prefix.
5538
5789
  */
5539
5790
  telEmailMode = false;
5791
+ /**
5792
+ * Segment values for the DATE input, each held as typed (unpadded while the
5793
+ * user is mid-entry) and normalised on blur.
5794
+ */
5795
+ dateSegments = {
5796
+ year: "",
5797
+ month: "",
5798
+ day: "",
5799
+ };
5540
5800
  /**
5541
5801
  * Emitted when a field value changes.
5542
5802
  */
@@ -5548,13 +5808,16 @@ class AuthheroNode {
5548
5808
  componentChanged() {
5549
5809
  this.initCountryFromConfig();
5550
5810
  this.initTelValue();
5811
+ this.initDateValue();
5551
5812
  }
5552
5813
  valueChanged() {
5553
5814
  this.initTelValue();
5815
+ this.initDateValue();
5554
5816
  }
5555
5817
  componentWillLoad() {
5556
5818
  this.initCountryFromConfig();
5557
5819
  this.initTelValue();
5820
+ this.initDateValue();
5558
5821
  }
5559
5822
  componentDidLoad() {
5560
5823
  // Auto-focus a code field so the caret starts in the first (leftmost) box.
@@ -5580,6 +5843,20 @@ class AuthheroNode {
5580
5843
  }
5581
5844
  }
5582
5845
  }
5846
+ /**
5847
+ * The last value this field emitted for a TEL component. The widget mirrors
5848
+ * every emitted value back onto the `value` prop, and that echo must not be
5849
+ * re-parsed: the wire format carries only a dial code, so a country picked
5850
+ * from the list would be replaced by the first entry sharing that code — a
5851
+ * user who chose Canada would be silently moved to the US on their next
5852
+ * keystroke.
5853
+ */
5854
+ lastEmittedTel;
5855
+ /** Emit a TEL value and remember it, so the echo can be recognised. */
5856
+ emitTelValue(value) {
5857
+ this.lastEmittedTel = value;
5858
+ this.fieldChange.emit({ id: this.component.id, value });
5859
+ }
5583
5860
  /**
5584
5861
  * Hydrate localPhoneNumber (and selectedCountry) from the effective value
5585
5862
  * for TEL fields. The full value is stored as `{dialCode}{localNumber}`,
@@ -5588,6 +5865,10 @@ class AuthheroNode {
5588
5865
  initTelValue() {
5589
5866
  if (this.component?.type !== "TEL")
5590
5867
  return;
5868
+ if (this.lastEmittedTel !== undefined &&
5869
+ this.getEffectiveValue() === this.lastEmittedTel) {
5870
+ return;
5871
+ }
5591
5872
  const config = this.component.config;
5592
5873
  const allowEmail = config?.allow_email === true;
5593
5874
  const fullValue = this.getEffectiveValue();
@@ -5622,11 +5903,16 @@ class AuthheroNode {
5622
5903
  handleCountryChange = (e) => {
5623
5904
  const target = e.target;
5624
5905
  this.selectedCountry = getCountryByCode(target.value);
5906
+ // A half-typed "+xx" prefix is replaced by the picked country, not kept
5907
+ // as a local number (it would emit "+46+4").
5908
+ if (this.localPhoneNumber.startsWith("+")) {
5909
+ this.localPhoneNumber = "";
5910
+ }
5625
5911
  // Re-emit the full phone number with new dial code
5626
5912
  const fullNumber = this.localPhoneNumber
5627
5913
  ? `${this.selectedCountry.dialCode}${this.localPhoneNumber}`
5628
5914
  : "";
5629
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
5915
+ this.emitTelValue(fullNumber);
5630
5916
  };
5631
5917
  /**
5632
5918
  * Try to detect a dial code prefix in the raw input (e.g. "+46", "0046")
@@ -5664,7 +5950,21 @@ class AuthheroNode {
5664
5950
  target.value = cleanedLocal;
5665
5951
  this.localPhoneNumber = cleanedLocal;
5666
5952
  const fullNumber = `${this.selectedCountry.dialCode}${cleanedLocal}`;
5667
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
5953
+ this.emitTelValue(fullNumber);
5954
+ }
5955
+ else if (value.startsWith("+")) {
5956
+ // A leading "+" is an international prefix the user is still typing.
5957
+ // Keep it in the field so the dial code can build up until it matches
5958
+ // a country — stripping it per keystroke means "+46" never resolves to
5959
+ // Sweden, since each character is discarded before the next arrives.
5960
+ const cleaned = `+${value.slice(1).replace(/[^\d\s\-()]/g, "")}`;
5961
+ if (cleaned !== value) {
5962
+ target.value = cleaned;
5963
+ }
5964
+ this.localPhoneNumber = cleaned;
5965
+ // Emit the partial prefix as-is; prefixing it with the (not yet chosen)
5966
+ // country's dial code would produce nonsense like "+1+4".
5967
+ this.emitTelValue(cleaned);
5668
5968
  }
5669
5969
  else {
5670
5970
  const cleaned = allowPlus
@@ -5677,7 +5977,7 @@ class AuthheroNode {
5677
5977
  const fullNumber = cleaned
5678
5978
  ? `${this.selectedCountry.dialCode}${cleaned}`
5679
5979
  : "";
5680
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
5980
+ this.emitTelValue(fullNumber);
5681
5981
  }
5682
5982
  }
5683
5983
  handlePhoneInput = (e) => {
@@ -5697,7 +5997,7 @@ class AuthheroNode {
5697
5997
  else {
5698
5998
  // Email or text — emit as-is
5699
5999
  this.localPhoneNumber = value;
5700
- this.fieldChange.emit({ id: this.component.id, value });
6000
+ this.emitTelValue(value);
5701
6001
  }
5702
6002
  return;
5703
6003
  }
@@ -5708,6 +6008,196 @@ class AuthheroNode {
5708
6008
  const target = e.target;
5709
6009
  this.fieldChange.emit({ id: this.component.id, value: target.value });
5710
6010
  };
6011
+ // ===========================================================================
6012
+ // DATE segment handling
6013
+ // ===========================================================================
6014
+ /**
6015
+ * The last ISO value this field emitted. The widget mirrors every emitted
6016
+ * value back onto the `value` prop, and that echo must not be treated as an
6017
+ * external change: it would re-format the segments under the caret while
6018
+ * the user is still typing (a day of "3" comes back as "03", swallowing the
6019
+ * next digit).
6020
+ */
6021
+ lastEmittedDate;
6022
+ /** Hydrate the day/month/year segments from the effective ISO value. */
6023
+ initDateValue() {
6024
+ if (this.component?.type !== "DATE")
6025
+ return;
6026
+ if (this.lastEmittedDate !== undefined &&
6027
+ this.getEffectiveValue() === this.lastEmittedDate) {
6028
+ return;
6029
+ }
6030
+ const parsed = parseIsoDate(this.getEffectiveValue());
6031
+ if (parsed) {
6032
+ this.dateSegments = parsed;
6033
+ return;
6034
+ }
6035
+ // No parseable value. Only clear when we currently hold a complete date,
6036
+ // i.e. this is a genuine external reset: a half-typed date emits "" and
6037
+ // the parent echoes that straight back as the value prop, which would
6038
+ // otherwise wipe the segment the user just typed into.
6039
+ if (toIsoDate(this.dateSegments) !== "") {
6040
+ this.dateSegments = { year: "", month: "", day: "" };
6041
+ }
6042
+ }
6043
+ dateConfig() {
6044
+ return this.component.config;
6045
+ }
6046
+ getDateLayoutForField() {
6047
+ const config = this.dateConfig();
6048
+ return getDateLayout(config?.format, this.locale);
6049
+ }
6050
+ static SEGMENT_LENGTH = {
6051
+ day: 2,
6052
+ month: 2,
6053
+ year: 4,
6054
+ };
6055
+ /** Birthdate autofill tokens — the common case for a typed date. */
6056
+ static SEGMENT_AUTOCOMPLETE = {
6057
+ day: "bday-day",
6058
+ month: "bday-month",
6059
+ year: "bday-year",
6060
+ };
6061
+ /**
6062
+ * The submittable value for the current segments: an ISO date, or "" when
6063
+ * it is incomplete, impossible, or outside the field's `min`/`max`. ISO
6064
+ * strings compare chronologically, so the bounds need no parsing.
6065
+ */
6066
+ dateIsoValue(segments) {
6067
+ const iso = toIsoDate(segments);
6068
+ if (!iso)
6069
+ return "";
6070
+ const config = this.dateConfig();
6071
+ const min = config?.min;
6072
+ const max = config?.max;
6073
+ if (typeof min === "string" && parseIsoDate(min) && iso < min)
6074
+ return "";
6075
+ if (typeof max === "string" && parseIsoDate(max) && iso > max)
6076
+ return "";
6077
+ return iso;
6078
+ }
6079
+ setDateSegments(segments) {
6080
+ this.dateSegments = segments;
6081
+ this.lastEmittedDate = this.dateIsoValue(segments);
6082
+ this.fieldChange.emit({
6083
+ id: this.component.id,
6084
+ value: this.lastEmittedDate,
6085
+ });
6086
+ }
6087
+ focusDateSegment(segment) {
6088
+ const input = this.host.shadowRoot?.querySelector(`input[data-date-segment="${segment}"]`);
6089
+ input?.focus();
6090
+ input?.select();
6091
+ }
6092
+ /**
6093
+ * Whether typing this digit completes the segment. A segment is complete at
6094
+ * its full width, and also as soon as a further digit could not produce a
6095
+ * valid number — no day starts with 4 and no month starts with 2.
6096
+ */
6097
+ isDateSegmentComplete(segment, value) {
6098
+ if (value.length >= AuthheroNode.SEGMENT_LENGTH[segment])
6099
+ return true;
6100
+ if (value.length !== 1)
6101
+ return false;
6102
+ if (segment === "day")
6103
+ return Number(value) > 3;
6104
+ if (segment === "month")
6105
+ return Number(value) > 1;
6106
+ return false;
6107
+ }
6108
+ handleDateSegmentInput = (segment) => (e) => {
6109
+ const target = e.target;
6110
+ const digits = target.value
6111
+ .replace(/\D/g, "")
6112
+ .slice(0, AuthheroNode.SEGMENT_LENGTH[segment]);
6113
+ if (digits !== target.value) {
6114
+ target.value = digits;
6115
+ }
6116
+ this.setDateSegments({ ...this.dateSegments, [segment]: digits });
6117
+ if (this.isDateSegmentComplete(segment, digits)) {
6118
+ const order = this.getDateLayoutForField().order;
6119
+ const next = order[order.indexOf(segment) + 1];
6120
+ if (next)
6121
+ this.focusDateSegment(next);
6122
+ }
6123
+ };
6124
+ /**
6125
+ * Pad a single digit ("5" -> "05") and expand a two-digit year against the
6126
+ * field's upper bound ("85" -> 1985). Shared by blur and paste so a pasted
6127
+ * date is read the same way as a typed one.
6128
+ */
6129
+ normalizeDateSegment(segment, value) {
6130
+ if (segment !== "year")
6131
+ return value.padStart(2, "0");
6132
+ return expandTwoDigitYear(value, resolveYearAnchor(this.dateConfig()?.max, new Date()));
6133
+ }
6134
+ /**
6135
+ * Normalise on blur: pad a single digit ("5" -> "05") and expand a two-digit
6136
+ * year against the field's upper bound ("85" -> 1985).
6137
+ */
6138
+ handleDateSegmentBlur = (segment) => (e) => {
6139
+ const target = e.target;
6140
+ const value = target.value.replace(/\D/g, "");
6141
+ if (!value)
6142
+ return;
6143
+ const normalized = this.normalizeDateSegment(segment, value);
6144
+ if (normalized === value)
6145
+ return;
6146
+ target.value = normalized;
6147
+ this.setDateSegments({ ...this.dateSegments, [segment]: normalized });
6148
+ };
6149
+ /**
6150
+ * Backspace at the start of an empty segment moves to the previous one, so
6151
+ * the whole date can be cleared without reaching for the mouse.
6152
+ */
6153
+ handleDateSegmentKeyDown = (segment) => (e) => {
6154
+ if (e.key === "Enter") {
6155
+ e.preventDefault();
6156
+ this.buttonClick.emit({ id: "submit", type: "submit", value: "next" });
6157
+ return;
6158
+ }
6159
+ if (e.key !== "Backspace")
6160
+ return;
6161
+ const target = e.target;
6162
+ if (target.value.length > 0 || target.selectionStart !== 0)
6163
+ return;
6164
+ const order = this.getDateLayoutForField().order;
6165
+ const previous = order[order.indexOf(segment) - 1];
6166
+ if (previous) {
6167
+ e.preventDefault();
6168
+ this.focusDateSegment(previous);
6169
+ }
6170
+ };
6171
+ /**
6172
+ * Pasting a whole date into any segment fills all three. An ISO value is
6173
+ * read year-first; anything else follows the field's displayed order. A
6174
+ * two-digit year is expanded the same way a typed one is, so "15/03/85"
6175
+ * pastes as 1985 rather than falling through to the browser's own paste.
6176
+ */
6177
+ handleDatePaste = (e) => {
6178
+ const text = e.clipboardData?.getData("text") ?? "";
6179
+ const groups = text.match(/\d+/g);
6180
+ if (!groups || groups.length < 3)
6181
+ return;
6182
+ const [first, second, third] = groups;
6183
+ const isoLike = first.length === 4;
6184
+ const order = isoLike
6185
+ ? ["year", "month", "day"]
6186
+ : this.getDateLayoutForField().order;
6187
+ const segments = {
6188
+ year: "",
6189
+ month: "",
6190
+ day: "",
6191
+ };
6192
+ [first, second, third].forEach((group, index) => {
6193
+ const segment = order[index];
6194
+ segments[segment] = this.normalizeDateSegment(segment, group.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]));
6195
+ });
6196
+ if (!toIsoDate(segments))
6197
+ return;
6198
+ e.preventDefault();
6199
+ this.setDateSegments(segments);
6200
+ };
5711
6201
  handleKeyDown = (e) => {
5712
6202
  if (e.key === "Enter") {
5713
6203
  e.preventDefault();
@@ -5720,7 +6210,7 @@ class AuthheroNode {
5720
6210
  if (config?.allow_email === true && target.value.length === 0) {
5721
6211
  this.telEmailMode = true;
5722
6212
  this.localPhoneNumber = "";
5723
- this.fieldChange.emit({ id: this.component.id, value: "" });
6213
+ this.emitTelValue("");
5724
6214
  }
5725
6215
  }
5726
6216
  };
@@ -5995,17 +6485,40 @@ class AuthheroNode {
5995
6485
  const effectiveValue = this.getEffectiveValue();
5996
6486
  return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, this.renderLabel(component.label, inputId, component.required), hAsync("input", { id: inputId, class: this.getInputFieldClass(errors.length > 0), part: "input", type: "url", name: component.id, value: effectiveValue ?? "", placeholder: component.config?.placeholder, required: component.required, disabled: this.disabled, onInput: this.handleInput, onKeyDown: this.handleKeyDown }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
5997
6487
  }
6488
+ /**
6489
+ * DATE is rendered as three numeric segments rather than `input type=date`.
6490
+ * A native date input renders differently in every browser (Safari adds a
6491
+ * stepper), keeps its dd/mm/yyyy hint visible while half-filled, and asks
6492
+ * for a calendar gesture to reach a year decades back — all wrong for a
6493
+ * date typed from memory, which is what these fields collect.
6494
+ */
5998
6495
  renderDateField(component) {
5999
- const inputId = `input-${component.id}`;
6000
6496
  const errors = this.getErrors();
6001
- const { min, max } = component.config ?? {};
6002
- // Date fields always have a value (even if placeholder format), so always float the label
6003
- const hasValue = true;
6004
- const effectiveValue = this.getEffectiveValue();
6005
- return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, hAsync("div", { class: "input-container" }, hAsync("input", { id: inputId, class: this.getInputFieldClass(errors.length > 0), part: "input", type: "date", name: component.id, "data-input-name": component.id, value: effectiveValue ?? "", placeholder: " ", required: component.required, disabled: this.disabled, min: min, max: max, onInput: this.handleInput, onKeyDown: this.handleKeyDown }), this.renderFloatingLabel(component.label, inputId, component.required, hasValue)), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
6497
+ const { format } = component.config ?? {};
6498
+ const layout = getDateLayout(format, this.locale);
6499
+ const isoValue = this.dateIsoValue(this.dateSegments);
6500
+ // Every segment filled but no ISO value means the date does not exist
6501
+ // (31 February) or falls outside min/max. Flag it rather than silently
6502
+ // submitting nothing — the widget posts JSON, so the browser's own
6503
+ // constraint validation never runs.
6504
+ const isIncomplete = layout.order.some((segment) => this.dateSegments[segment].length === 0);
6505
+ const hasInvalidDate = !isoValue && !isIncomplete;
6506
+ const hasError = errors.length > 0 || hasInvalidDate;
6507
+ const firstInputId = `input-${component.id}`;
6508
+ const segmentInput = (segment) => (hAsync("input", { key: segment, id: segment === layout.order[0] ? firstInputId : undefined, class: "date-segment", part: `input date-segment date-segment-${segment}`, type: "text", inputmode: "numeric", autocomplete: AuthheroNode.SEGMENT_AUTOCOMPLETE[segment], "data-date-segment": segment, maxlength: AuthheroNode.SEGMENT_LENGTH[segment], size: AuthheroNode.SEGMENT_LENGTH[segment], value: this.dateSegments[segment], placeholder: layout.tokens[segment], "aria-label": layout.tokens[segment], "aria-invalid": hasError ? "true" : "false", required: component.required, disabled: this.disabled, onInput: this.handleDateSegmentInput(segment), onBlur: this.handleDateSegmentBlur(segment), onKeyDown: this.handleDateSegmentKeyDown(segment), onPaste: this.handleDatePaste }));
6509
+ return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, hAsync("div", { class: "input-container" }, hAsync("div", { class: hasError ? "date-input has-error" : "date-input", part: "date-input", role: "group", "aria-label": component.label }, layout.order.map((segment, index) => [
6510
+ index > 0 && (hAsync("span", { key: `sep-${segment}`, class: "date-separator", part: "date-separator", "aria-hidden": "true" }, layout.separator)),
6511
+ segmentInput(segment),
6512
+ ])), this.renderFloatingLabel(component.label, firstInputId, component.required, true)), hAsync("input", { type: "hidden", name: component.id, "data-input-name": component.id, value: isoValue }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
6006
6513
  }
6007
6514
  renderBooleanField(component) {
6008
- return (hAsync("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, hAsync("input", { type: "checkbox", part: "checkbox", name: component.id, checked: this.value === "true" || component.config?.default_value === true, required: component.required, disabled: this.disabled, onChange: this.handleCheckbox }), hAsync("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
6515
+ return (hAsync("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, hAsync("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
6516
+ // Once the field has a value it is authoritative — falling back to
6517
+ // the default here would keep a ticked-by-default box ticked after
6518
+ // the user unticks it.
6519
+ this.value === undefined
6520
+ ? component.config?.default_value === true
6521
+ : this.value === "true", required: component.required, disabled: this.disabled, onChange: this.handleCheckbox }), hAsync("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
6009
6522
  }
6010
6523
  renderLegalField(component) {
6011
6524
  const text = component.config?.text ?? component.label ?? "";
@@ -6185,11 +6698,13 @@ class AuthheroNode {
6185
6698
  "component": [16],
6186
6699
  "value": [1],
6187
6700
  "disabled": [4],
6701
+ "locale": [1],
6188
6702
  "passwordVisible": [32],
6189
6703
  "selectedCountry": [32],
6190
6704
  "localPhoneNumber": [32],
6191
6705
  "countryDropdownOpen": [32],
6192
- "telEmailMode": [32]
6706
+ "telEmailMode": [32],
6707
+ "dateSegments": [32]
6193
6708
  },
6194
6709
  "$listeners$": undefined,
6195
6710
  "$lazyBundleId$": "-",
@@ -6720,6 +7235,27 @@ const authheroWidgetCss = () => `:host{display:block;font-family:var(--ah-font-f
6720
7235
  * buttons. Used by long pick-one screens such as tenant selection.
6721
7236
  */
6722
7237
  const CHOICE_LIST_SEARCH_THRESHOLD = 5;
7238
+ /**
7239
+ * Component types the widget renders as something the user can actually fill
7240
+ * in. Only these gate the primary action button: types that render nothing
7241
+ * (CARDS, FILE, RECAPTCHA, …) can carry `required` in the schema too, and
7242
+ * gating on them would leave Continue disabled with no way to satisfy it.
7243
+ */
7244
+ const FILLABLE_FIELD_TYPES = new Set([
7245
+ "TEXT",
7246
+ "EMAIL",
7247
+ "CODE",
7248
+ "PASSWORD",
7249
+ "NUMBER",
7250
+ "TEL",
7251
+ "URL",
7252
+ "DATE",
7253
+ "BOOLEAN",
7254
+ "LEGAL",
7255
+ "COUNTRY",
7256
+ "DROPDOWN",
7257
+ "CHOICE",
7258
+ ]);
6723
7259
  class AuthheroWidget {
6724
7260
  constructor(hostRef) {
6725
7261
  registerInstance(this, hostRef);
@@ -6812,6 +7348,13 @@ class AuthheroWidget {
6812
7348
  * @default false (same as autoSubmit when not specified)
6813
7349
  */
6814
7350
  autoNavigate;
7351
+ /**
7352
+ * BCP-47 locale for locale-dependent field layout, e.g. whether a DATE
7353
+ * field reads DD/MM/YYYY, MM/DD/YYYY or YYYY-MM-DD. Resolve it server-side
7354
+ * and pass it in so the server-rendered markup and the hydrated one agree;
7355
+ * screen text itself is already localized by the server.
7356
+ */
7357
+ locale;
6815
7358
  /**
6816
7359
  * Internal parsed screen state.
6817
7360
  */
@@ -6912,14 +7455,19 @@ class AuthheroWidget {
6912
7455
  initFormDataFromDefaults(screen) {
6913
7456
  const defaults = {};
6914
7457
  for (const comp of screen.components || []) {
6915
- if ("config" in comp &&
6916
- comp.config &&
6917
- "default_value" in comp.config &&
6918
- comp.config.default_value) {
6919
- const val = comp.config.default_value;
6920
- if (typeof val === "string" && val !== "") {
6921
- defaults[comp.id] = val;
6922
- }
7458
+ if (!("config" in comp) || !comp.config)
7459
+ continue;
7460
+ if (!("default_value" in comp.config))
7461
+ continue;
7462
+ const val = comp.config.default_value;
7463
+ if (typeof val === "string" && val !== "") {
7464
+ defaults[comp.id] = val;
7465
+ }
7466
+ else if (typeof val === "boolean") {
7467
+ // A BOOLEAN's default decides whether the checkbox renders ticked, so
7468
+ // seed the matching value — otherwise a field the user never touches
7469
+ // submits nothing and the screen's state is lost.
7470
+ defaults[comp.id] = val ? "true" : "false";
6923
7471
  }
6924
7472
  }
6925
7473
  if (Object.keys(defaults).length > 0) {
@@ -7920,6 +8468,11 @@ class AuthheroWidget {
7920
8468
  handleButtonClick = (detail) => {
7921
8469
  // If this is a submit button click, trigger form submission
7922
8470
  if (detail.type === "submit") {
8471
+ // Enter in a text field submits too, so the required-field gate has to
8472
+ // live here and not only on the button's disabled state.
8473
+ if (this._screen && this.hasUnfilledRequiredFields(this._screen)) {
8474
+ return;
8475
+ }
7923
8476
  // For GET screens (or missing method), navigate directly — no form submission needed
7924
8477
  if ((!this._screen?.method ||
7925
8478
  this._screen.method.toUpperCase() === "GET") &&
@@ -8108,6 +8661,32 @@ class AuthheroWidget {
8108
8661
  isDividerComponent(component) {
8109
8662
  return component.type === "DIVIDER";
8110
8663
  }
8664
+ /**
8665
+ * Whether a required component holds a value the user has supplied.
8666
+ */
8667
+ isRequiredFieldFilled(component) {
8668
+ const value = this.formData[component.id];
8669
+ // Checkboxes are only "filled" when ticked. A BOOLEAN's default_value is
8670
+ // seeded into formData, so it needs no separate handling here.
8671
+ if (component.type === "BOOLEAN" || component.type === "LEGAL") {
8672
+ return value === "true";
8673
+ }
8674
+ return typeof value === "string" && value.trim() !== "";
8675
+ }
8676
+ /**
8677
+ * Whether the screen still has required fields the user has not filled in.
8678
+ * Used to hold the primary action button disabled: the widget submits via
8679
+ * its own handler rather than a native form submit, so the browser's
8680
+ * constraint validation never runs and an empty required field would
8681
+ * otherwise only surface as a server error after a round trip.
8682
+ */
8683
+ hasUnfilledRequiredFields(screen) {
8684
+ return (screen.components ?? []).some((component) => component.visible !== false &&
8685
+ "required" in component &&
8686
+ component.required === true &&
8687
+ FILLABLE_FIELD_TYPES.has(component.type) &&
8688
+ !this.isRequiredFieldFilled(component));
8689
+ }
8111
8690
  /**
8112
8691
  * Visible label of a choice button, used to filter searchable choice lists.
8113
8692
  */
@@ -8143,6 +8722,9 @@ class AuthheroWidget {
8143
8722
  const choiceButtons = fieldComponents.filter((c) => c.type === "NEXT_BUTTON");
8144
8723
  const isSearchableChoiceList = choiceButtons.length === fieldComponents.length &&
8145
8724
  choiceButtons.length > CHOICE_LIST_SEARCH_THRESHOLD;
8725
+ // Hold the primary action button disabled until every required field has
8726
+ // a value.
8727
+ const requiredFieldsMissing = this.hasUnfilledRequiredFields(screen);
8146
8728
  const filterQuery = this.listFilter.trim().toLowerCase();
8147
8729
  const visibleChoiceButtons = isSearchableChoiceList && filterQuery
8148
8730
  ? choiceButtons.filter((c) => this.getChoiceButtonText(c).toLowerCase().includes(filterQuery))
@@ -8177,7 +8759,7 @@ class AuthheroWidget {
8177
8759
  };
8178
8760
  // Get logo URL from theme.widget (takes precedence) or branding
8179
8761
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
8180
- return (hAsync("div", { class: "widget-container", part: "container", "data-authstack-container": true }, hAsync("header", { class: "widget-header", part: "header" }, logoUrl && (hAsync("div", { class: "logo-wrapper", part: "logo-wrapper" }, hAsync("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (hAsync("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (hAsync("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), hAsync("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (hAsync("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (hAsync("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), hAsync("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (hAsync("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), hAsync("div", { class: "form-content" }, socialComponents.length > 0 && (hAsync("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (hAsync("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
8762
+ return (hAsync("div", { class: "widget-container", part: "container", "data-authstack-container": true }, hAsync("header", { class: "widget-header", part: "header" }, logoUrl && (hAsync("div", { class: "logo-wrapper", part: "logo-wrapper" }, hAsync("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (hAsync("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (hAsync("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), hAsync("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (hAsync("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (hAsync("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), hAsync("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (hAsync("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), hAsync("div", { class: "form-content" }, socialComponents.length > 0 && (hAsync("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (hAsync("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
8181
8763
  fieldComponents.length > 0 &&
8182
8764
  hasDivider && (hAsync("div", { class: "divider", part: "divider" }, hAsync("span", { class: "divider-text" }, dividerText))), hAsync("div", { class: "fields-section", part: "fields-section" }, isSearchableChoiceList && (hAsync("input", { type: "text", class: "choice-list-search", part: "choice-list-search", placeholder: "Search", "aria-label": "Search", autocomplete: "off", value: this.listFilter, onInput: (e) => (this.listFilter = e.target.value), onKeyDown: (e) => {
8183
8765
  // The search box lives inside the form; Enter would
@@ -8190,7 +8772,9 @@ class AuthheroWidget {
8190
8772
  ? "fields-list fields-list-scroll"
8191
8773
  : "fields-list" }, (isSearchableChoiceList
8192
8774
  ? visibleChoiceButtons
8193
- : fieldComponents).map((component) => (hAsync("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading }))), isSearchableChoiceList &&
8775
+ : fieldComponents).map((component) => (hAsync("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading ||
8776
+ (component.type === "NEXT_BUTTON" &&
8777
+ requiredFieldsMissing) }))), isSearchableChoiceList &&
8194
8778
  visibleChoiceButtons.length === 0 && (hAsync("div", { class: "choice-list-empty", part: "choice-list-empty" }, "No matches")))))), screen.links && screen.links.length > 0 && (hAsync("div", { class: "links", part: "links" }, screen.links.map((link) => (hAsync("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (hAsync("span", null, link.text, " ", hAsync("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
8195
8779
  id: link.id,
8196
8780
  href: link.href,
@@ -8236,6 +8820,7 @@ class AuthheroWidget {
8236
8820
  "loading": [1028],
8237
8821
  "autoSubmit": [4, "auto-submit"],
8238
8822
  "autoNavigate": [4, "auto-navigate"],
8823
+ "locale": [1],
8239
8824
  "_screen": [32],
8240
8825
  "_authParams": [32],
8241
8826
  "_branding": [32],