@authhero/widget 0.37.0 → 0.38.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 (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-b5f14fa6.entry.js +1 -0
  4. package/dist/authhero-widget/p-f94037cd.entry.js +1 -0
  5. package/dist/cjs/authhero-node.cjs.entry.js +458 -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 +176 -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-C9ZfIQiS.js +1 -0
  18. package/dist/esm/authhero-node.entry.js +458 -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 +541 -24
  28. package/hydrate/index.mjs +541 -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.js CHANGED
@@ -5500,7 +5500,184 @@ function getCountryByCode(code) {
5500
5500
  countries.find((c) => c.code === "US"));
5501
5501
  }
5502
5502
 
5503
- 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)}`;
5503
+ /**
5504
+ * Locale-aware layout for the segmented DATE input.
5505
+ *
5506
+ * The segment order is resolved from a static table rather than from
5507
+ * `Intl.DateTimeFormat`, deliberately: the widget is server-rendered and then
5508
+ * hydrated in the browser, and the two runtimes do not ship the same ICU data
5509
+ * (a Workers runtime commonly falls back to en-US ordering). A static table
5510
+ * gives the server and the client the same answer, so the hydrated DOM matches
5511
+ * the rendered one.
5512
+ */
5513
+ /** Languages that write dates big-endian: year, month, day. */
5514
+ const YEAR_FIRST_LANGUAGES = new Set([
5515
+ "bo",
5516
+ "dz",
5517
+ "hu",
5518
+ "ja",
5519
+ "ko",
5520
+ "lt",
5521
+ "mn",
5522
+ "sv",
5523
+ "ug",
5524
+ "zh",
5525
+ ]);
5526
+ /**
5527
+ * Regions that write month before day. Month-first is essentially a US
5528
+ * convention plus the places that adopted it.
5529
+ */
5530
+ const MONTH_FIRST_REGIONS = new Set(["us", "ph", "fm", "mh", "pw", "as", "gu"]);
5531
+ /**
5532
+ * Locales whose language is normally year-first but which write dates
5533
+ * day-first in a particular region — Finland-Swedish is d.m.yyyy, unlike
5534
+ * Sweden-Swedish.
5535
+ */
5536
+ const DAY_FIRST_TAGS = new Set(["sv-fi"]);
5537
+ const DEFAULT_TOKENS = {
5538
+ day: "DD",
5539
+ month: "MM",
5540
+ year: "YYYY",
5541
+ };
5542
+ /**
5543
+ * Split a BCP-47 tag into a lowercase language subtag and, when present, a
5544
+ * two-letter region subtag. Script and variant subtags are ignored.
5545
+ */
5546
+ function parseTag(locale) {
5547
+ const tag = (locale || "").toLowerCase().replace(/_/g, "-");
5548
+ const parts = tag.split("-").filter(Boolean);
5549
+ const language = parts[0] || "";
5550
+ const region = parts.slice(1).find((part) => /^[a-z]{2}$/.test(part));
5551
+ return { language, region };
5552
+ }
5553
+ /**
5554
+ * Resolve the segment order for a locale. Falls back to day-month-year, which
5555
+ * is what most of the world writes.
5556
+ */
5557
+ function getDateOrder(locale) {
5558
+ const { language, region } = parseTag(locale);
5559
+ if (region && MONTH_FIRST_REGIONS.has(region)) {
5560
+ return ["month", "day", "year"];
5561
+ }
5562
+ if (region && DAY_FIRST_TAGS.has(`${language}-${region}`)) {
5563
+ return ["day", "month", "year"];
5564
+ }
5565
+ if (YEAR_FIRST_LANGUAGES.has(language)) {
5566
+ return ["year", "month", "day"];
5567
+ }
5568
+ // A bare "en" carries no region — treat it the way Intl does and assume
5569
+ // en-US. Regional English (en-gb, en-au, …) falls through to day-first.
5570
+ if (language === "en" && !region) {
5571
+ return ["month", "day", "year"];
5572
+ }
5573
+ return ["day", "month", "year"];
5574
+ }
5575
+ /**
5576
+ * Parse an explicit format string from the component config, e.g.
5577
+ * "DD/MM/YYYY" or "YYYY-MM-DD". Returns null when the string does not name
5578
+ * all three segments.
5579
+ */
5580
+ function parseFormat(format) {
5581
+ const matches = format.match(/y+|m+|d+/gi);
5582
+ if (!matches)
5583
+ return null;
5584
+ const order = [];
5585
+ const tokens = { ...DEFAULT_TOKENS };
5586
+ for (const match of matches) {
5587
+ const initial = match[0].toLowerCase();
5588
+ const segment = initial === "y" ? "year" : initial === "m" ? "month" : "day";
5589
+ if (order.includes(segment))
5590
+ continue;
5591
+ order.push(segment);
5592
+ // A "YY" format still collects a four-digit year — the segment holds four
5593
+ // characters and `toIsoDate` rejects anything shorter, with a two-digit
5594
+ // entry expanded on blur. Showing "YY" as the placeholder would promise an
5595
+ // input the field does not accept, so short year tokens are widened.
5596
+ tokens[segment] =
5597
+ segment === "year" && match.length < 4 ? DEFAULT_TOKENS.year : match;
5598
+ }
5599
+ if (order.length !== 3)
5600
+ return null;
5601
+ const separator = format.match(/[^a-z\s]/i)?.[0] ?? "/";
5602
+ return { order, tokens, separator };
5603
+ }
5604
+ /**
5605
+ * Resolve the full layout for a DATE field. An explicit `config.format` wins;
5606
+ * otherwise the locale decides the order.
5607
+ */
5608
+ function getDateLayout(format, locale) {
5609
+ if (format) {
5610
+ const parsed = parseFormat(format);
5611
+ if (parsed)
5612
+ return parsed;
5613
+ }
5614
+ const order = getDateOrder(locale);
5615
+ return {
5616
+ order,
5617
+ tokens: DEFAULT_TOKENS,
5618
+ separator: order[0] === "year" ? "-" : "/",
5619
+ };
5620
+ }
5621
+ /** Number of days in a given month, honouring leap years. */
5622
+ function daysInMonth(year, month) {
5623
+ if (month < 1 || month > 12)
5624
+ return 0;
5625
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
5626
+ }
5627
+ /** Split an ISO "YYYY-MM-DD" value into its segments. */
5628
+ function parseIsoDate(value) {
5629
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec((value || "").trim());
5630
+ if (!match)
5631
+ return null;
5632
+ return { year: match[1], month: match[2], day: match[3] };
5633
+ }
5634
+ /**
5635
+ * Build an ISO "YYYY-MM-DD" string from segment values. Returns "" unless all
5636
+ * three segments are complete and form a real calendar date — a half-filled or
5637
+ * impossible date (31 February) is not a value worth submitting.
5638
+ */
5639
+ function toIsoDate(segments) {
5640
+ const { year, month, day } = segments;
5641
+ if (year.length !== 4 || month.length === 0 || day.length === 0)
5642
+ return "";
5643
+ const y = Number(year);
5644
+ const m = Number(month);
5645
+ const d = Number(day);
5646
+ if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) {
5647
+ return "";
5648
+ }
5649
+ if (m < 1 || m > 12)
5650
+ return "";
5651
+ if (d < 1 || d > daysInMonth(y, m))
5652
+ return "";
5653
+ return `${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
5654
+ }
5655
+ /**
5656
+ * Expand a two-digit year into the most recent matching year at or before
5657
+ * `anchorYear` — "85" becomes 1985, "05" becomes 2005. Anything that is not
5658
+ * exactly two digits is returned unchanged, so a fully typed year is never
5659
+ * second-guessed.
5660
+ */
5661
+ function expandTwoDigitYear(year, anchorYear) {
5662
+ if (!/^\d{2}$/.test(year))
5663
+ return year;
5664
+ const century = Math.floor(anchorYear / 100) * 100;
5665
+ const candidate = century + Number(year);
5666
+ return String(candidate > anchorYear ? candidate - 100 : candidate);
5667
+ }
5668
+ /**
5669
+ * The latest year a two-digit entry may expand to. `config.max` pins it when
5670
+ * the field has an upper bound; otherwise we assume a date in the past (the
5671
+ * birthdate case) and anchor on the current year.
5672
+ */
5673
+ function resolveYearAnchor(max, today) {
5674
+ const parsed = parseIsoDate(max);
5675
+ if (parsed)
5676
+ return Number(parsed.year);
5677
+ return today.getUTCFullYear();
5678
+ }
5679
+
5680
+ 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)}`;
5504
5681
 
5505
5682
  class AuthheroNode {
5506
5683
  constructor(hostRef) {
@@ -5518,6 +5695,12 @@ class AuthheroNode {
5518
5695
  * Whether the component is disabled.
5519
5696
  */
5520
5697
  disabled = false;
5698
+ /**
5699
+ * BCP-47 locale used for locale-dependent field layout (currently the
5700
+ * segment order of DATE fields). Resolved server-side so SSR and hydration
5701
+ * agree; falls back to day-month-year when absent.
5702
+ */
5703
+ locale;
5521
5704
  /**
5522
5705
  * Whether the password field is visible.
5523
5706
  */
@@ -5539,6 +5722,15 @@ class AuthheroNode {
5539
5722
  * When true, the value is emitted as-is without dial code prefix.
5540
5723
  */
5541
5724
  telEmailMode = false;
5725
+ /**
5726
+ * Segment values for the DATE input, each held as typed (unpadded while the
5727
+ * user is mid-entry) and normalised on blur.
5728
+ */
5729
+ dateSegments = {
5730
+ year: "",
5731
+ month: "",
5732
+ day: "",
5733
+ };
5542
5734
  /**
5543
5735
  * Emitted when a field value changes.
5544
5736
  */
@@ -5550,13 +5742,16 @@ class AuthheroNode {
5550
5742
  componentChanged() {
5551
5743
  this.initCountryFromConfig();
5552
5744
  this.initTelValue();
5745
+ this.initDateValue();
5553
5746
  }
5554
5747
  valueChanged() {
5555
5748
  this.initTelValue();
5749
+ this.initDateValue();
5556
5750
  }
5557
5751
  componentWillLoad() {
5558
5752
  this.initCountryFromConfig();
5559
5753
  this.initTelValue();
5754
+ this.initDateValue();
5560
5755
  }
5561
5756
  componentDidLoad() {
5562
5757
  // Auto-focus a code field so the caret starts in the first (leftmost) box.
@@ -5582,6 +5777,20 @@ class AuthheroNode {
5582
5777
  }
5583
5778
  }
5584
5779
  }
5780
+ /**
5781
+ * The last value this field emitted for a TEL component. The widget mirrors
5782
+ * every emitted value back onto the `value` prop, and that echo must not be
5783
+ * re-parsed: the wire format carries only a dial code, so a country picked
5784
+ * from the list would be replaced by the first entry sharing that code — a
5785
+ * user who chose Canada would be silently moved to the US on their next
5786
+ * keystroke.
5787
+ */
5788
+ lastEmittedTel;
5789
+ /** Emit a TEL value and remember it, so the echo can be recognised. */
5790
+ emitTelValue(value) {
5791
+ this.lastEmittedTel = value;
5792
+ this.fieldChange.emit({ id: this.component.id, value });
5793
+ }
5585
5794
  /**
5586
5795
  * Hydrate localPhoneNumber (and selectedCountry) from the effective value
5587
5796
  * for TEL fields. The full value is stored as `{dialCode}{localNumber}`,
@@ -5590,6 +5799,10 @@ class AuthheroNode {
5590
5799
  initTelValue() {
5591
5800
  if (this.component?.type !== "TEL")
5592
5801
  return;
5802
+ if (this.lastEmittedTel !== undefined &&
5803
+ this.getEffectiveValue() === this.lastEmittedTel) {
5804
+ return;
5805
+ }
5593
5806
  const config = this.component.config;
5594
5807
  const allowEmail = config?.allow_email === true;
5595
5808
  const fullValue = this.getEffectiveValue();
@@ -5624,11 +5837,16 @@ class AuthheroNode {
5624
5837
  handleCountryChange = (e) => {
5625
5838
  const target = e.target;
5626
5839
  this.selectedCountry = getCountryByCode(target.value);
5840
+ // A half-typed "+xx" prefix is replaced by the picked country, not kept
5841
+ // as a local number (it would emit "+46+4").
5842
+ if (this.localPhoneNumber.startsWith("+")) {
5843
+ this.localPhoneNumber = "";
5844
+ }
5627
5845
  // Re-emit the full phone number with new dial code
5628
5846
  const fullNumber = this.localPhoneNumber
5629
5847
  ? `${this.selectedCountry.dialCode}${this.localPhoneNumber}`
5630
5848
  : "";
5631
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
5849
+ this.emitTelValue(fullNumber);
5632
5850
  };
5633
5851
  /**
5634
5852
  * Try to detect a dial code prefix in the raw input (e.g. "+46", "0046")
@@ -5666,7 +5884,21 @@ class AuthheroNode {
5666
5884
  target.value = cleanedLocal;
5667
5885
  this.localPhoneNumber = cleanedLocal;
5668
5886
  const fullNumber = `${this.selectedCountry.dialCode}${cleanedLocal}`;
5669
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
5887
+ this.emitTelValue(fullNumber);
5888
+ }
5889
+ else if (value.startsWith("+")) {
5890
+ // A leading "+" is an international prefix the user is still typing.
5891
+ // Keep it in the field so the dial code can build up until it matches
5892
+ // a country — stripping it per keystroke means "+46" never resolves to
5893
+ // Sweden, since each character is discarded before the next arrives.
5894
+ const cleaned = `+${value.slice(1).replace(/[^\d\s\-()]/g, "")}`;
5895
+ if (cleaned !== value) {
5896
+ target.value = cleaned;
5897
+ }
5898
+ this.localPhoneNumber = cleaned;
5899
+ // Emit the partial prefix as-is; prefixing it with the (not yet chosen)
5900
+ // country's dial code would produce nonsense like "+1+4".
5901
+ this.emitTelValue(cleaned);
5670
5902
  }
5671
5903
  else {
5672
5904
  const cleaned = allowPlus
@@ -5679,7 +5911,7 @@ class AuthheroNode {
5679
5911
  const fullNumber = cleaned
5680
5912
  ? `${this.selectedCountry.dialCode}${cleaned}`
5681
5913
  : "";
5682
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
5914
+ this.emitTelValue(fullNumber);
5683
5915
  }
5684
5916
  }
5685
5917
  handlePhoneInput = (e) => {
@@ -5699,7 +5931,7 @@ class AuthheroNode {
5699
5931
  else {
5700
5932
  // Email or text — emit as-is
5701
5933
  this.localPhoneNumber = value;
5702
- this.fieldChange.emit({ id: this.component.id, value });
5934
+ this.emitTelValue(value);
5703
5935
  }
5704
5936
  return;
5705
5937
  }
@@ -5710,6 +5942,196 @@ class AuthheroNode {
5710
5942
  const target = e.target;
5711
5943
  this.fieldChange.emit({ id: this.component.id, value: target.value });
5712
5944
  };
5945
+ // ===========================================================================
5946
+ // DATE segment handling
5947
+ // ===========================================================================
5948
+ /**
5949
+ * The last ISO value this field emitted. The widget mirrors every emitted
5950
+ * value back onto the `value` prop, and that echo must not be treated as an
5951
+ * external change: it would re-format the segments under the caret while
5952
+ * the user is still typing (a day of "3" comes back as "03", swallowing the
5953
+ * next digit).
5954
+ */
5955
+ lastEmittedDate;
5956
+ /** Hydrate the day/month/year segments from the effective ISO value. */
5957
+ initDateValue() {
5958
+ if (this.component?.type !== "DATE")
5959
+ return;
5960
+ if (this.lastEmittedDate !== undefined &&
5961
+ this.getEffectiveValue() === this.lastEmittedDate) {
5962
+ return;
5963
+ }
5964
+ const parsed = parseIsoDate(this.getEffectiveValue());
5965
+ if (parsed) {
5966
+ this.dateSegments = parsed;
5967
+ return;
5968
+ }
5969
+ // No parseable value. Only clear when we currently hold a complete date,
5970
+ // i.e. this is a genuine external reset: a half-typed date emits "" and
5971
+ // the parent echoes that straight back as the value prop, which would
5972
+ // otherwise wipe the segment the user just typed into.
5973
+ if (toIsoDate(this.dateSegments) !== "") {
5974
+ this.dateSegments = { year: "", month: "", day: "" };
5975
+ }
5976
+ }
5977
+ dateConfig() {
5978
+ return this.component.config;
5979
+ }
5980
+ getDateLayoutForField() {
5981
+ const config = this.dateConfig();
5982
+ return getDateLayout(config?.format, this.locale);
5983
+ }
5984
+ static SEGMENT_LENGTH = {
5985
+ day: 2,
5986
+ month: 2,
5987
+ year: 4,
5988
+ };
5989
+ /** Birthdate autofill tokens — the common case for a typed date. */
5990
+ static SEGMENT_AUTOCOMPLETE = {
5991
+ day: "bday-day",
5992
+ month: "bday-month",
5993
+ year: "bday-year",
5994
+ };
5995
+ /**
5996
+ * The submittable value for the current segments: an ISO date, or "" when
5997
+ * it is incomplete, impossible, or outside the field's `min`/`max`. ISO
5998
+ * strings compare chronologically, so the bounds need no parsing.
5999
+ */
6000
+ dateIsoValue(segments) {
6001
+ const iso = toIsoDate(segments);
6002
+ if (!iso)
6003
+ return "";
6004
+ const config = this.dateConfig();
6005
+ const min = config?.min;
6006
+ const max = config?.max;
6007
+ if (typeof min === "string" && parseIsoDate(min) && iso < min)
6008
+ return "";
6009
+ if (typeof max === "string" && parseIsoDate(max) && iso > max)
6010
+ return "";
6011
+ return iso;
6012
+ }
6013
+ setDateSegments(segments) {
6014
+ this.dateSegments = segments;
6015
+ this.lastEmittedDate = this.dateIsoValue(segments);
6016
+ this.fieldChange.emit({
6017
+ id: this.component.id,
6018
+ value: this.lastEmittedDate,
6019
+ });
6020
+ }
6021
+ focusDateSegment(segment) {
6022
+ const input = this.host.shadowRoot?.querySelector(`input[data-date-segment="${segment}"]`);
6023
+ input?.focus();
6024
+ input?.select();
6025
+ }
6026
+ /**
6027
+ * Whether typing this digit completes the segment. A segment is complete at
6028
+ * its full width, and also as soon as a further digit could not produce a
6029
+ * valid number — no day starts with 4 and no month starts with 2.
6030
+ */
6031
+ isDateSegmentComplete(segment, value) {
6032
+ if (value.length >= AuthheroNode.SEGMENT_LENGTH[segment])
6033
+ return true;
6034
+ if (value.length !== 1)
6035
+ return false;
6036
+ if (segment === "day")
6037
+ return Number(value) > 3;
6038
+ if (segment === "month")
6039
+ return Number(value) > 1;
6040
+ return false;
6041
+ }
6042
+ handleDateSegmentInput = (segment) => (e) => {
6043
+ const target = e.target;
6044
+ const digits = target.value
6045
+ .replace(/\D/g, "")
6046
+ .slice(0, AuthheroNode.SEGMENT_LENGTH[segment]);
6047
+ if (digits !== target.value) {
6048
+ target.value = digits;
6049
+ }
6050
+ this.setDateSegments({ ...this.dateSegments, [segment]: digits });
6051
+ if (this.isDateSegmentComplete(segment, digits)) {
6052
+ const order = this.getDateLayoutForField().order;
6053
+ const next = order[order.indexOf(segment) + 1];
6054
+ if (next)
6055
+ this.focusDateSegment(next);
6056
+ }
6057
+ };
6058
+ /**
6059
+ * Pad a single digit ("5" -> "05") and expand a two-digit year against the
6060
+ * field's upper bound ("85" -> 1985). Shared by blur and paste so a pasted
6061
+ * date is read the same way as a typed one.
6062
+ */
6063
+ normalizeDateSegment(segment, value) {
6064
+ if (segment !== "year")
6065
+ return value.padStart(2, "0");
6066
+ return expandTwoDigitYear(value, resolveYearAnchor(this.dateConfig()?.max, new Date()));
6067
+ }
6068
+ /**
6069
+ * Normalise on blur: pad a single digit ("5" -> "05") and expand a two-digit
6070
+ * year against the field's upper bound ("85" -> 1985).
6071
+ */
6072
+ handleDateSegmentBlur = (segment) => (e) => {
6073
+ const target = e.target;
6074
+ const value = target.value.replace(/\D/g, "");
6075
+ if (!value)
6076
+ return;
6077
+ const normalized = this.normalizeDateSegment(segment, value);
6078
+ if (normalized === value)
6079
+ return;
6080
+ target.value = normalized;
6081
+ this.setDateSegments({ ...this.dateSegments, [segment]: normalized });
6082
+ };
6083
+ /**
6084
+ * Backspace at the start of an empty segment moves to the previous one, so
6085
+ * the whole date can be cleared without reaching for the mouse.
6086
+ */
6087
+ handleDateSegmentKeyDown = (segment) => (e) => {
6088
+ if (e.key === "Enter") {
6089
+ e.preventDefault();
6090
+ this.buttonClick.emit({ id: "submit", type: "submit", value: "next" });
6091
+ return;
6092
+ }
6093
+ if (e.key !== "Backspace")
6094
+ return;
6095
+ const target = e.target;
6096
+ if (target.value.length > 0 || target.selectionStart !== 0)
6097
+ return;
6098
+ const order = this.getDateLayoutForField().order;
6099
+ const previous = order[order.indexOf(segment) - 1];
6100
+ if (previous) {
6101
+ e.preventDefault();
6102
+ this.focusDateSegment(previous);
6103
+ }
6104
+ };
6105
+ /**
6106
+ * Pasting a whole date into any segment fills all three. An ISO value is
6107
+ * read year-first; anything else follows the field's displayed order. A
6108
+ * two-digit year is expanded the same way a typed one is, so "15/03/85"
6109
+ * pastes as 1985 rather than falling through to the browser's own paste.
6110
+ */
6111
+ handleDatePaste = (e) => {
6112
+ const text = e.clipboardData?.getData("text") ?? "";
6113
+ const groups = text.match(/\d+/g);
6114
+ if (!groups || groups.length < 3)
6115
+ return;
6116
+ const [first, second, third] = groups;
6117
+ const isoLike = first.length === 4;
6118
+ const order = isoLike
6119
+ ? ["year", "month", "day"]
6120
+ : this.getDateLayoutForField().order;
6121
+ const segments = {
6122
+ year: "",
6123
+ month: "",
6124
+ day: "",
6125
+ };
6126
+ [first, second, third].forEach((group, index) => {
6127
+ const segment = order[index];
6128
+ segments[segment] = this.normalizeDateSegment(segment, group.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]));
6129
+ });
6130
+ if (!toIsoDate(segments))
6131
+ return;
6132
+ e.preventDefault();
6133
+ this.setDateSegments(segments);
6134
+ };
5713
6135
  handleKeyDown = (e) => {
5714
6136
  if (e.key === "Enter") {
5715
6137
  e.preventDefault();
@@ -5722,7 +6144,7 @@ class AuthheroNode {
5722
6144
  if (config?.allow_email === true && target.value.length === 0) {
5723
6145
  this.telEmailMode = true;
5724
6146
  this.localPhoneNumber = "";
5725
- this.fieldChange.emit({ id: this.component.id, value: "" });
6147
+ this.emitTelValue("");
5726
6148
  }
5727
6149
  }
5728
6150
  };
@@ -5997,17 +6419,40 @@ class AuthheroNode {
5997
6419
  const effectiveValue = this.getEffectiveValue();
5998
6420
  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)));
5999
6421
  }
6422
+ /**
6423
+ * DATE is rendered as three numeric segments rather than `input type=date`.
6424
+ * A native date input renders differently in every browser (Safari adds a
6425
+ * stepper), keeps its dd/mm/yyyy hint visible while half-filled, and asks
6426
+ * for a calendar gesture to reach a year decades back — all wrong for a
6427
+ * date typed from memory, which is what these fields collect.
6428
+ */
6000
6429
  renderDateField(component) {
6001
- const inputId = `input-${component.id}`;
6002
6430
  const errors = this.getErrors();
6003
- const { min, max } = component.config ?? {};
6004
- // Date fields always have a value (even if placeholder format), so always float the label
6005
- const hasValue = true;
6006
- const effectiveValue = this.getEffectiveValue();
6007
- 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)));
6431
+ const { format } = component.config ?? {};
6432
+ const layout = getDateLayout(format, this.locale);
6433
+ const isoValue = this.dateIsoValue(this.dateSegments);
6434
+ // Every segment filled but no ISO value means the date does not exist
6435
+ // (31 February) or falls outside min/max. Flag it rather than silently
6436
+ // submitting nothing — the widget posts JSON, so the browser's own
6437
+ // constraint validation never runs.
6438
+ const isIncomplete = layout.order.some((segment) => this.dateSegments[segment].length === 0);
6439
+ const hasInvalidDate = !isoValue && !isIncomplete;
6440
+ const hasError = errors.length > 0 || hasInvalidDate;
6441
+ const firstInputId = `input-${component.id}`;
6442
+ 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 }));
6443
+ 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) => [
6444
+ index > 0 && (hAsync("span", { key: `sep-${segment}`, class: "date-separator", part: "date-separator", "aria-hidden": "true" }, layout.separator)),
6445
+ segmentInput(segment),
6446
+ ])), 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)));
6008
6447
  }
6009
6448
  renderBooleanField(component) {
6010
- 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)));
6449
+ return (hAsync("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, hAsync("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
6450
+ // Once the field has a value it is authoritative — falling back to
6451
+ // the default here would keep a ticked-by-default box ticked after
6452
+ // the user unticks it.
6453
+ this.value === undefined
6454
+ ? component.config?.default_value === true
6455
+ : this.value === "true", required: component.required, disabled: this.disabled, onChange: this.handleCheckbox }), hAsync("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
6011
6456
  }
6012
6457
  renderLegalField(component) {
6013
6458
  const text = component.config?.text ?? component.label ?? "";
@@ -6187,11 +6632,13 @@ class AuthheroNode {
6187
6632
  "component": [16],
6188
6633
  "value": [1],
6189
6634
  "disabled": [4],
6635
+ "locale": [1],
6190
6636
  "passwordVisible": [32],
6191
6637
  "selectedCountry": [32],
6192
6638
  "localPhoneNumber": [32],
6193
6639
  "countryDropdownOpen": [32],
6194
- "telEmailMode": [32]
6640
+ "telEmailMode": [32],
6641
+ "dateSegments": [32]
6195
6642
  },
6196
6643
  "$listeners$": undefined,
6197
6644
  "$lazyBundleId$": "-",
@@ -6722,6 +7169,27 @@ const authheroWidgetCss = () => `:host{display:block;font-family:var(--ah-font-f
6722
7169
  * buttons. Used by long pick-one screens such as tenant selection.
6723
7170
  */
6724
7171
  const CHOICE_LIST_SEARCH_THRESHOLD = 5;
7172
+ /**
7173
+ * Component types the widget renders as something the user can actually fill
7174
+ * in. Only these gate the primary action button: types that render nothing
7175
+ * (CARDS, FILE, RECAPTCHA, …) can carry `required` in the schema too, and
7176
+ * gating on them would leave Continue disabled with no way to satisfy it.
7177
+ */
7178
+ const FILLABLE_FIELD_TYPES = new Set([
7179
+ "TEXT",
7180
+ "EMAIL",
7181
+ "CODE",
7182
+ "PASSWORD",
7183
+ "NUMBER",
7184
+ "TEL",
7185
+ "URL",
7186
+ "DATE",
7187
+ "BOOLEAN",
7188
+ "LEGAL",
7189
+ "COUNTRY",
7190
+ "DROPDOWN",
7191
+ "CHOICE",
7192
+ ]);
6725
7193
  class AuthheroWidget {
6726
7194
  constructor(hostRef) {
6727
7195
  registerInstance(this, hostRef);
@@ -6814,6 +7282,13 @@ class AuthheroWidget {
6814
7282
  * @default false (same as autoSubmit when not specified)
6815
7283
  */
6816
7284
  autoNavigate;
7285
+ /**
7286
+ * BCP-47 locale for locale-dependent field layout, e.g. whether a DATE
7287
+ * field reads DD/MM/YYYY, MM/DD/YYYY or YYYY-MM-DD. Resolve it server-side
7288
+ * and pass it in so the server-rendered markup and the hydrated one agree;
7289
+ * screen text itself is already localized by the server.
7290
+ */
7291
+ locale;
6817
7292
  /**
6818
7293
  * Internal parsed screen state.
6819
7294
  */
@@ -6914,14 +7389,19 @@ class AuthheroWidget {
6914
7389
  initFormDataFromDefaults(screen) {
6915
7390
  const defaults = {};
6916
7391
  for (const comp of screen.components || []) {
6917
- if ("config" in comp &&
6918
- comp.config &&
6919
- "default_value" in comp.config &&
6920
- comp.config.default_value) {
6921
- const val = comp.config.default_value;
6922
- if (typeof val === "string" && val !== "") {
6923
- defaults[comp.id] = val;
6924
- }
7392
+ if (!("config" in comp) || !comp.config)
7393
+ continue;
7394
+ if (!("default_value" in comp.config))
7395
+ continue;
7396
+ const val = comp.config.default_value;
7397
+ if (typeof val === "string" && val !== "") {
7398
+ defaults[comp.id] = val;
7399
+ }
7400
+ else if (typeof val === "boolean") {
7401
+ // A BOOLEAN's default decides whether the checkbox renders ticked, so
7402
+ // seed the matching value — otherwise a field the user never touches
7403
+ // submits nothing and the screen's state is lost.
7404
+ defaults[comp.id] = val ? "true" : "false";
6925
7405
  }
6926
7406
  }
6927
7407
  if (Object.keys(defaults).length > 0) {
@@ -7922,6 +8402,11 @@ class AuthheroWidget {
7922
8402
  handleButtonClick = (detail) => {
7923
8403
  // If this is a submit button click, trigger form submission
7924
8404
  if (detail.type === "submit") {
8405
+ // Enter in a text field submits too, so the required-field gate has to
8406
+ // live here and not only on the button's disabled state.
8407
+ if (this._screen && this.hasUnfilledRequiredFields(this._screen)) {
8408
+ return;
8409
+ }
7925
8410
  // For GET screens (or missing method), navigate directly — no form submission needed
7926
8411
  if ((!this._screen?.method ||
7927
8412
  this._screen.method.toUpperCase() === "GET") &&
@@ -8110,6 +8595,32 @@ class AuthheroWidget {
8110
8595
  isDividerComponent(component) {
8111
8596
  return component.type === "DIVIDER";
8112
8597
  }
8598
+ /**
8599
+ * Whether a required component holds a value the user has supplied.
8600
+ */
8601
+ isRequiredFieldFilled(component) {
8602
+ const value = this.formData[component.id];
8603
+ // Checkboxes are only "filled" when ticked. A BOOLEAN's default_value is
8604
+ // seeded into formData, so it needs no separate handling here.
8605
+ if (component.type === "BOOLEAN" || component.type === "LEGAL") {
8606
+ return value === "true";
8607
+ }
8608
+ return typeof value === "string" && value.trim() !== "";
8609
+ }
8610
+ /**
8611
+ * Whether the screen still has required fields the user has not filled in.
8612
+ * Used to hold the primary action button disabled: the widget submits via
8613
+ * its own handler rather than a native form submit, so the browser's
8614
+ * constraint validation never runs and an empty required field would
8615
+ * otherwise only surface as a server error after a round trip.
8616
+ */
8617
+ hasUnfilledRequiredFields(screen) {
8618
+ return (screen.components ?? []).some((component) => component.visible !== false &&
8619
+ "required" in component &&
8620
+ component.required === true &&
8621
+ FILLABLE_FIELD_TYPES.has(component.type) &&
8622
+ !this.isRequiredFieldFilled(component));
8623
+ }
8113
8624
  /**
8114
8625
  * Visible label of a choice button, used to filter searchable choice lists.
8115
8626
  */
@@ -8145,6 +8656,9 @@ class AuthheroWidget {
8145
8656
  const choiceButtons = fieldComponents.filter((c) => c.type === "NEXT_BUTTON");
8146
8657
  const isSearchableChoiceList = choiceButtons.length === fieldComponents.length &&
8147
8658
  choiceButtons.length > CHOICE_LIST_SEARCH_THRESHOLD;
8659
+ // Hold the primary action button disabled until every required field has
8660
+ // a value.
8661
+ const requiredFieldsMissing = this.hasUnfilledRequiredFields(screen);
8148
8662
  const filterQuery = this.listFilter.trim().toLowerCase();
8149
8663
  const visibleChoiceButtons = isSearchableChoiceList && filterQuery
8150
8664
  ? choiceButtons.filter((c) => this.getChoiceButtonText(c).toLowerCase().includes(filterQuery))
@@ -8179,7 +8693,7 @@ class AuthheroWidget {
8179
8693
  };
8180
8694
  // Get logo URL from theme.widget (takes precedence) or branding
8181
8695
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
8182
- 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 &&
8696
+ 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 &&
8183
8697
  fieldComponents.length > 0 &&
8184
8698
  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) => {
8185
8699
  // The search box lives inside the form; Enter would
@@ -8192,7 +8706,9 @@ class AuthheroWidget {
8192
8706
  ? "fields-list fields-list-scroll"
8193
8707
  : "fields-list" }, (isSearchableChoiceList
8194
8708
  ? visibleChoiceButtons
8195
- : 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 &&
8709
+ : 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 ||
8710
+ (component.type === "NEXT_BUTTON" &&
8711
+ requiredFieldsMissing) }))), isSearchableChoiceList &&
8196
8712
  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, {
8197
8713
  id: link.id,
8198
8714
  href: link.href,
@@ -8238,6 +8754,7 @@ class AuthheroWidget {
8238
8754
  "loading": [1028],
8239
8755
  "autoSubmit": [4, "auto-submit"],
8240
8756
  "autoNavigate": [4, "auto-navigate"],
8757
+ "locale": [1],
8241
8758
  "_screen": [32],
8242
8759
  "_authParams": [32],
8243
8760
  "_branding": [32],