@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
@@ -238,7 +238,184 @@ function getCountryByCode(code) {
238
238
  countries.find((c) => c.code === "US"));
239
239
  }
240
240
 
241
- 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)}`;
241
+ /**
242
+ * Locale-aware layout for the segmented DATE input.
243
+ *
244
+ * The segment order is resolved from a static table rather than from
245
+ * `Intl.DateTimeFormat`, deliberately: the widget is server-rendered and then
246
+ * hydrated in the browser, and the two runtimes do not ship the same ICU data
247
+ * (a Workers runtime commonly falls back to en-US ordering). A static table
248
+ * gives the server and the client the same answer, so the hydrated DOM matches
249
+ * the rendered one.
250
+ */
251
+ /** Languages that write dates big-endian: year, month, day. */
252
+ const YEAR_FIRST_LANGUAGES = new Set([
253
+ "bo",
254
+ "dz",
255
+ "hu",
256
+ "ja",
257
+ "ko",
258
+ "lt",
259
+ "mn",
260
+ "sv",
261
+ "ug",
262
+ "zh",
263
+ ]);
264
+ /**
265
+ * Regions that write month before day. Month-first is essentially a US
266
+ * convention plus the places that adopted it.
267
+ */
268
+ const MONTH_FIRST_REGIONS = new Set(["us", "ph", "fm", "mh", "pw", "as", "gu"]);
269
+ /**
270
+ * Locales whose language is normally year-first but which write dates
271
+ * day-first in a particular region — Finland-Swedish is d.m.yyyy, unlike
272
+ * Sweden-Swedish.
273
+ */
274
+ const DAY_FIRST_TAGS = new Set(["sv-fi"]);
275
+ const DEFAULT_TOKENS = {
276
+ day: "DD",
277
+ month: "MM",
278
+ year: "YYYY",
279
+ };
280
+ /**
281
+ * Split a BCP-47 tag into a lowercase language subtag and, when present, a
282
+ * two-letter region subtag. Script and variant subtags are ignored.
283
+ */
284
+ function parseTag(locale) {
285
+ const tag = (locale || "").toLowerCase().replace(/_/g, "-");
286
+ const parts = tag.split("-").filter(Boolean);
287
+ const language = parts[0] || "";
288
+ const region = parts.slice(1).find((part) => /^[a-z]{2}$/.test(part));
289
+ return { language, region };
290
+ }
291
+ /**
292
+ * Resolve the segment order for a locale. Falls back to day-month-year, which
293
+ * is what most of the world writes.
294
+ */
295
+ function getDateOrder(locale) {
296
+ const { language, region } = parseTag(locale);
297
+ if (region && MONTH_FIRST_REGIONS.has(region)) {
298
+ return ["month", "day", "year"];
299
+ }
300
+ if (region && DAY_FIRST_TAGS.has(`${language}-${region}`)) {
301
+ return ["day", "month", "year"];
302
+ }
303
+ if (YEAR_FIRST_LANGUAGES.has(language)) {
304
+ return ["year", "month", "day"];
305
+ }
306
+ // A bare "en" carries no region — treat it the way Intl does and assume
307
+ // en-US. Regional English (en-gb, en-au, …) falls through to day-first.
308
+ if (language === "en" && !region) {
309
+ return ["month", "day", "year"];
310
+ }
311
+ return ["day", "month", "year"];
312
+ }
313
+ /**
314
+ * Parse an explicit format string from the component config, e.g.
315
+ * "DD/MM/YYYY" or "YYYY-MM-DD". Returns null when the string does not name
316
+ * all three segments.
317
+ */
318
+ function parseFormat(format) {
319
+ const matches = format.match(/y+|m+|d+/gi);
320
+ if (!matches)
321
+ return null;
322
+ const order = [];
323
+ const tokens = { ...DEFAULT_TOKENS };
324
+ for (const match of matches) {
325
+ const initial = match[0].toLowerCase();
326
+ const segment = initial === "y" ? "year" : initial === "m" ? "month" : "day";
327
+ if (order.includes(segment))
328
+ continue;
329
+ order.push(segment);
330
+ // A "YY" format still collects a four-digit year — the segment holds four
331
+ // characters and `toIsoDate` rejects anything shorter, with a two-digit
332
+ // entry expanded on blur. Showing "YY" as the placeholder would promise an
333
+ // input the field does not accept, so short year tokens are widened.
334
+ tokens[segment] =
335
+ segment === "year" && match.length < 4 ? DEFAULT_TOKENS.year : match;
336
+ }
337
+ if (order.length !== 3)
338
+ return null;
339
+ const separator = format.match(/[^a-z\s]/i)?.[0] ?? "/";
340
+ return { order, tokens, separator };
341
+ }
342
+ /**
343
+ * Resolve the full layout for a DATE field. An explicit `config.format` wins;
344
+ * otherwise the locale decides the order.
345
+ */
346
+ function getDateLayout(format, locale) {
347
+ if (format) {
348
+ const parsed = parseFormat(format);
349
+ if (parsed)
350
+ return parsed;
351
+ }
352
+ const order = getDateOrder(locale);
353
+ return {
354
+ order,
355
+ tokens: DEFAULT_TOKENS,
356
+ separator: order[0] === "year" ? "-" : "/",
357
+ };
358
+ }
359
+ /** Number of days in a given month, honouring leap years. */
360
+ function daysInMonth(year, month) {
361
+ if (month < 1 || month > 12)
362
+ return 0;
363
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
364
+ }
365
+ /** Split an ISO "YYYY-MM-DD" value into its segments. */
366
+ function parseIsoDate(value) {
367
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec((value || "").trim());
368
+ if (!match)
369
+ return null;
370
+ return { year: match[1], month: match[2], day: match[3] };
371
+ }
372
+ /**
373
+ * Build an ISO "YYYY-MM-DD" string from segment values. Returns "" unless all
374
+ * three segments are complete and form a real calendar date — a half-filled or
375
+ * impossible date (31 February) is not a value worth submitting.
376
+ */
377
+ function toIsoDate(segments) {
378
+ const { year, month, day } = segments;
379
+ if (year.length !== 4 || month.length === 0 || day.length === 0)
380
+ return "";
381
+ const y = Number(year);
382
+ const m = Number(month);
383
+ const d = Number(day);
384
+ if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) {
385
+ return "";
386
+ }
387
+ if (m < 1 || m > 12)
388
+ return "";
389
+ if (d < 1 || d > daysInMonth(y, m))
390
+ return "";
391
+ return `${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
392
+ }
393
+ /**
394
+ * Expand a two-digit year into the most recent matching year at or before
395
+ * `anchorYear` — "85" becomes 1985, "05" becomes 2005. Anything that is not
396
+ * exactly two digits is returned unchanged, so a fully typed year is never
397
+ * second-guessed.
398
+ */
399
+ function expandTwoDigitYear(year, anchorYear) {
400
+ if (!/^\d{2}$/.test(year))
401
+ return year;
402
+ const century = Math.floor(anchorYear / 100) * 100;
403
+ const candidate = century + Number(year);
404
+ return String(candidate > anchorYear ? candidate - 100 : candidate);
405
+ }
406
+ /**
407
+ * The latest year a two-digit entry may expand to. `config.max` pins it when
408
+ * the field has an upper bound; otherwise we assume a date in the past (the
409
+ * birthdate case) and anchor on the current year.
410
+ */
411
+ function resolveYearAnchor(max, today) {
412
+ const parsed = parseIsoDate(max);
413
+ if (parsed)
414
+ return Number(parsed.year);
415
+ return today.getUTCFullYear();
416
+ }
417
+
418
+ 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)}`;
242
419
 
243
420
  const AuthheroNode = class {
244
421
  constructor(hostRef) {
@@ -256,6 +433,12 @@ const AuthheroNode = class {
256
433
  * Whether the component is disabled.
257
434
  */
258
435
  disabled = false;
436
+ /**
437
+ * BCP-47 locale used for locale-dependent field layout (currently the
438
+ * segment order of DATE fields). Resolved server-side so SSR and hydration
439
+ * agree; falls back to day-month-year when absent.
440
+ */
441
+ locale;
259
442
  /**
260
443
  * Whether the password field is visible.
261
444
  */
@@ -277,6 +460,15 @@ const AuthheroNode = class {
277
460
  * When true, the value is emitted as-is without dial code prefix.
278
461
  */
279
462
  telEmailMode = false;
463
+ /**
464
+ * Segment values for the DATE input, each held as typed (unpadded while the
465
+ * user is mid-entry) and normalised on blur.
466
+ */
467
+ dateSegments = {
468
+ year: "",
469
+ month: "",
470
+ day: "",
471
+ };
280
472
  /**
281
473
  * Emitted when a field value changes.
282
474
  */
@@ -288,13 +480,16 @@ const AuthheroNode = class {
288
480
  componentChanged() {
289
481
  this.initCountryFromConfig();
290
482
  this.initTelValue();
483
+ this.initDateValue();
291
484
  }
292
485
  valueChanged() {
293
486
  this.initTelValue();
487
+ this.initDateValue();
294
488
  }
295
489
  componentWillLoad() {
296
490
  this.initCountryFromConfig();
297
491
  this.initTelValue();
492
+ this.initDateValue();
298
493
  }
299
494
  componentDidLoad() {
300
495
  // Auto-focus a code field so the caret starts in the first (leftmost) box.
@@ -320,6 +515,20 @@ const AuthheroNode = class {
320
515
  }
321
516
  }
322
517
  }
518
+ /**
519
+ * The last value this field emitted for a TEL component. The widget mirrors
520
+ * every emitted value back onto the `value` prop, and that echo must not be
521
+ * re-parsed: the wire format carries only a dial code, so a country picked
522
+ * from the list would be replaced by the first entry sharing that code — a
523
+ * user who chose Canada would be silently moved to the US on their next
524
+ * keystroke.
525
+ */
526
+ lastEmittedTel;
527
+ /** Emit a TEL value and remember it, so the echo can be recognised. */
528
+ emitTelValue(value) {
529
+ this.lastEmittedTel = value;
530
+ this.fieldChange.emit({ id: this.component.id, value });
531
+ }
323
532
  /**
324
533
  * Hydrate localPhoneNumber (and selectedCountry) from the effective value
325
534
  * for TEL fields. The full value is stored as `{dialCode}{localNumber}`,
@@ -328,6 +537,10 @@ const AuthheroNode = class {
328
537
  initTelValue() {
329
538
  if (this.component?.type !== "TEL")
330
539
  return;
540
+ if (this.lastEmittedTel !== undefined &&
541
+ this.getEffectiveValue() === this.lastEmittedTel) {
542
+ return;
543
+ }
331
544
  const config = this.component.config;
332
545
  const allowEmail = config?.allow_email === true;
333
546
  const fullValue = this.getEffectiveValue();
@@ -362,11 +575,16 @@ const AuthheroNode = class {
362
575
  handleCountryChange = (e) => {
363
576
  const target = e.target;
364
577
  this.selectedCountry = getCountryByCode(target.value);
578
+ // A half-typed "+xx" prefix is replaced by the picked country, not kept
579
+ // as a local number (it would emit "+46+4").
580
+ if (this.localPhoneNumber.startsWith("+")) {
581
+ this.localPhoneNumber = "";
582
+ }
365
583
  // Re-emit the full phone number with new dial code
366
584
  const fullNumber = this.localPhoneNumber
367
585
  ? `${this.selectedCountry.dialCode}${this.localPhoneNumber}`
368
586
  : "";
369
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
587
+ this.emitTelValue(fullNumber);
370
588
  };
371
589
  /**
372
590
  * Try to detect a dial code prefix in the raw input (e.g. "+46", "0046")
@@ -404,7 +622,21 @@ const AuthheroNode = class {
404
622
  target.value = cleanedLocal;
405
623
  this.localPhoneNumber = cleanedLocal;
406
624
  const fullNumber = `${this.selectedCountry.dialCode}${cleanedLocal}`;
407
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
625
+ this.emitTelValue(fullNumber);
626
+ }
627
+ else if (value.startsWith("+")) {
628
+ // A leading "+" is an international prefix the user is still typing.
629
+ // Keep it in the field so the dial code can build up until it matches
630
+ // a country — stripping it per keystroke means "+46" never resolves to
631
+ // Sweden, since each character is discarded before the next arrives.
632
+ const cleaned = `+${value.slice(1).replace(/[^\d\s\-()]/g, "")}`;
633
+ if (cleaned !== value) {
634
+ target.value = cleaned;
635
+ }
636
+ this.localPhoneNumber = cleaned;
637
+ // Emit the partial prefix as-is; prefixing it with the (not yet chosen)
638
+ // country's dial code would produce nonsense like "+1+4".
639
+ this.emitTelValue(cleaned);
408
640
  }
409
641
  else {
410
642
  const cleaned = allowPlus
@@ -417,7 +649,7 @@ const AuthheroNode = class {
417
649
  const fullNumber = cleaned
418
650
  ? `${this.selectedCountry.dialCode}${cleaned}`
419
651
  : "";
420
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
652
+ this.emitTelValue(fullNumber);
421
653
  }
422
654
  }
423
655
  handlePhoneInput = (e) => {
@@ -437,7 +669,7 @@ const AuthheroNode = class {
437
669
  else {
438
670
  // Email or text — emit as-is
439
671
  this.localPhoneNumber = value;
440
- this.fieldChange.emit({ id: this.component.id, value });
672
+ this.emitTelValue(value);
441
673
  }
442
674
  return;
443
675
  }
@@ -448,6 +680,196 @@ const AuthheroNode = class {
448
680
  const target = e.target;
449
681
  this.fieldChange.emit({ id: this.component.id, value: target.value });
450
682
  };
683
+ // ===========================================================================
684
+ // DATE segment handling
685
+ // ===========================================================================
686
+ /**
687
+ * The last ISO value this field emitted. The widget mirrors every emitted
688
+ * value back onto the `value` prop, and that echo must not be treated as an
689
+ * external change: it would re-format the segments under the caret while
690
+ * the user is still typing (a day of "3" comes back as "03", swallowing the
691
+ * next digit).
692
+ */
693
+ lastEmittedDate;
694
+ /** Hydrate the day/month/year segments from the effective ISO value. */
695
+ initDateValue() {
696
+ if (this.component?.type !== "DATE")
697
+ return;
698
+ if (this.lastEmittedDate !== undefined &&
699
+ this.getEffectiveValue() === this.lastEmittedDate) {
700
+ return;
701
+ }
702
+ const parsed = parseIsoDate(this.getEffectiveValue());
703
+ if (parsed) {
704
+ this.dateSegments = parsed;
705
+ return;
706
+ }
707
+ // No parseable value. Only clear when we currently hold a complete date,
708
+ // i.e. this is a genuine external reset: a half-typed date emits "" and
709
+ // the parent echoes that straight back as the value prop, which would
710
+ // otherwise wipe the segment the user just typed into.
711
+ if (toIsoDate(this.dateSegments) !== "") {
712
+ this.dateSegments = { year: "", month: "", day: "" };
713
+ }
714
+ }
715
+ dateConfig() {
716
+ return this.component.config;
717
+ }
718
+ getDateLayoutForField() {
719
+ const config = this.dateConfig();
720
+ return getDateLayout(config?.format, this.locale);
721
+ }
722
+ static SEGMENT_LENGTH = {
723
+ day: 2,
724
+ month: 2,
725
+ year: 4,
726
+ };
727
+ /** Birthdate autofill tokens — the common case for a typed date. */
728
+ static SEGMENT_AUTOCOMPLETE = {
729
+ day: "bday-day",
730
+ month: "bday-month",
731
+ year: "bday-year",
732
+ };
733
+ /**
734
+ * The submittable value for the current segments: an ISO date, or "" when
735
+ * it is incomplete, impossible, or outside the field's `min`/`max`. ISO
736
+ * strings compare chronologically, so the bounds need no parsing.
737
+ */
738
+ dateIsoValue(segments) {
739
+ const iso = toIsoDate(segments);
740
+ if (!iso)
741
+ return "";
742
+ const config = this.dateConfig();
743
+ const min = config?.min;
744
+ const max = config?.max;
745
+ if (typeof min === "string" && parseIsoDate(min) && iso < min)
746
+ return "";
747
+ if (typeof max === "string" && parseIsoDate(max) && iso > max)
748
+ return "";
749
+ return iso;
750
+ }
751
+ setDateSegments(segments) {
752
+ this.dateSegments = segments;
753
+ this.lastEmittedDate = this.dateIsoValue(segments);
754
+ this.fieldChange.emit({
755
+ id: this.component.id,
756
+ value: this.lastEmittedDate,
757
+ });
758
+ }
759
+ focusDateSegment(segment) {
760
+ const input = this.host.shadowRoot?.querySelector(`input[data-date-segment="${segment}"]`);
761
+ input?.focus();
762
+ input?.select();
763
+ }
764
+ /**
765
+ * Whether typing this digit completes the segment. A segment is complete at
766
+ * its full width, and also as soon as a further digit could not produce a
767
+ * valid number — no day starts with 4 and no month starts with 2.
768
+ */
769
+ isDateSegmentComplete(segment, value) {
770
+ if (value.length >= AuthheroNode.SEGMENT_LENGTH[segment])
771
+ return true;
772
+ if (value.length !== 1)
773
+ return false;
774
+ if (segment === "day")
775
+ return Number(value) > 3;
776
+ if (segment === "month")
777
+ return Number(value) > 1;
778
+ return false;
779
+ }
780
+ handleDateSegmentInput = (segment) => (e) => {
781
+ const target = e.target;
782
+ const digits = target.value
783
+ .replace(/\D/g, "")
784
+ .slice(0, AuthheroNode.SEGMENT_LENGTH[segment]);
785
+ if (digits !== target.value) {
786
+ target.value = digits;
787
+ }
788
+ this.setDateSegments({ ...this.dateSegments, [segment]: digits });
789
+ if (this.isDateSegmentComplete(segment, digits)) {
790
+ const order = this.getDateLayoutForField().order;
791
+ const next = order[order.indexOf(segment) + 1];
792
+ if (next)
793
+ this.focusDateSegment(next);
794
+ }
795
+ };
796
+ /**
797
+ * Pad a single digit ("5" -> "05") and expand a two-digit year against the
798
+ * field's upper bound ("85" -> 1985). Shared by blur and paste so a pasted
799
+ * date is read the same way as a typed one.
800
+ */
801
+ normalizeDateSegment(segment, value) {
802
+ if (segment !== "year")
803
+ return value.padStart(2, "0");
804
+ return expandTwoDigitYear(value, resolveYearAnchor(this.dateConfig()?.max, new Date()));
805
+ }
806
+ /**
807
+ * Normalise on blur: pad a single digit ("5" -> "05") and expand a two-digit
808
+ * year against the field's upper bound ("85" -> 1985).
809
+ */
810
+ handleDateSegmentBlur = (segment) => (e) => {
811
+ const target = e.target;
812
+ const value = target.value.replace(/\D/g, "");
813
+ if (!value)
814
+ return;
815
+ const normalized = this.normalizeDateSegment(segment, value);
816
+ if (normalized === value)
817
+ return;
818
+ target.value = normalized;
819
+ this.setDateSegments({ ...this.dateSegments, [segment]: normalized });
820
+ };
821
+ /**
822
+ * Backspace at the start of an empty segment moves to the previous one, so
823
+ * the whole date can be cleared without reaching for the mouse.
824
+ */
825
+ handleDateSegmentKeyDown = (segment) => (e) => {
826
+ if (e.key === "Enter") {
827
+ e.preventDefault();
828
+ this.buttonClick.emit({ id: "submit", type: "submit", value: "next" });
829
+ return;
830
+ }
831
+ if (e.key !== "Backspace")
832
+ return;
833
+ const target = e.target;
834
+ if (target.value.length > 0 || target.selectionStart !== 0)
835
+ return;
836
+ const order = this.getDateLayoutForField().order;
837
+ const previous = order[order.indexOf(segment) - 1];
838
+ if (previous) {
839
+ e.preventDefault();
840
+ this.focusDateSegment(previous);
841
+ }
842
+ };
843
+ /**
844
+ * Pasting a whole date into any segment fills all three. An ISO value is
845
+ * read year-first; anything else follows the field's displayed order. A
846
+ * two-digit year is expanded the same way a typed one is, so "15/03/85"
847
+ * pastes as 1985 rather than falling through to the browser's own paste.
848
+ */
849
+ handleDatePaste = (e) => {
850
+ const text = e.clipboardData?.getData("text") ?? "";
851
+ const groups = text.match(/\d+/g);
852
+ if (!groups || groups.length < 3)
853
+ return;
854
+ const [first, second, third] = groups;
855
+ const isoLike = first.length === 4;
856
+ const order = isoLike
857
+ ? ["year", "month", "day"]
858
+ : this.getDateLayoutForField().order;
859
+ const segments = {
860
+ year: "",
861
+ month: "",
862
+ day: "",
863
+ };
864
+ [first, second, third].forEach((group, index) => {
865
+ const segment = order[index];
866
+ segments[segment] = this.normalizeDateSegment(segment, group.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]));
867
+ });
868
+ if (!toIsoDate(segments))
869
+ return;
870
+ e.preventDefault();
871
+ this.setDateSegments(segments);
872
+ };
451
873
  handleKeyDown = (e) => {
452
874
  if (e.key === "Enter") {
453
875
  e.preventDefault();
@@ -460,7 +882,7 @@ const AuthheroNode = class {
460
882
  if (config?.allow_email === true && target.value.length === 0) {
461
883
  this.telEmailMode = true;
462
884
  this.localPhoneNumber = "";
463
- this.fieldChange.emit({ id: this.component.id, value: "" });
885
+ this.emitTelValue("");
464
886
  }
465
887
  }
466
888
  };
@@ -735,17 +1157,40 @@ const AuthheroNode = class {
735
1157
  const effectiveValue = this.getEffectiveValue();
736
1158
  return (index.h("div", { class: "input-wrapper", part: "input-wrapper" }, this.renderLabel(component.label, inputId, component.required), index.h("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)));
737
1159
  }
1160
+ /**
1161
+ * DATE is rendered as three numeric segments rather than `input type=date`.
1162
+ * A native date input renders differently in every browser (Safari adds a
1163
+ * stepper), keeps its dd/mm/yyyy hint visible while half-filled, and asks
1164
+ * for a calendar gesture to reach a year decades back — all wrong for a
1165
+ * date typed from memory, which is what these fields collect.
1166
+ */
738
1167
  renderDateField(component) {
739
- const inputId = `input-${component.id}`;
740
1168
  const errors = this.getErrors();
741
- const { min, max } = component.config ?? {};
742
- // Date fields always have a value (even if placeholder format), so always float the label
743
- const hasValue = true;
744
- const effectiveValue = this.getEffectiveValue();
745
- return (index.h("div", { class: "input-wrapper", part: "input-wrapper" }, index.h("div", { class: "input-container" }, index.h("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)));
1169
+ const { format } = component.config ?? {};
1170
+ const layout = getDateLayout(format, this.locale);
1171
+ const isoValue = this.dateIsoValue(this.dateSegments);
1172
+ // Every segment filled but no ISO value means the date does not exist
1173
+ // (31 February) or falls outside min/max. Flag it rather than silently
1174
+ // submitting nothing — the widget posts JSON, so the browser's own
1175
+ // constraint validation never runs.
1176
+ const isIncomplete = layout.order.some((segment) => this.dateSegments[segment].length === 0);
1177
+ const hasInvalidDate = !isoValue && !isIncomplete;
1178
+ const hasError = errors.length > 0 || hasInvalidDate;
1179
+ const firstInputId = `input-${component.id}`;
1180
+ const segmentInput = (segment) => (index.h("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 }));
1181
+ return (index.h("div", { class: "input-wrapper", part: "input-wrapper" }, index.h("div", { class: "input-container" }, index.h("div", { class: hasError ? "date-input has-error" : "date-input", part: "date-input", role: "group", "aria-label": component.label }, layout.order.map((segment, index$1) => [
1182
+ index$1 > 0 && (index.h("span", { key: `sep-${segment}`, class: "date-separator", part: "date-separator", "aria-hidden": "true" }, layout.separator)),
1183
+ segmentInput(segment),
1184
+ ])), this.renderFloatingLabel(component.label, firstInputId, component.required, true)), index.h("input", { type: "hidden", name: component.id, "data-input-name": component.id, value: isoValue }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
746
1185
  }
747
1186
  renderBooleanField(component) {
748
- return (index.h("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, index.h("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 }), index.h("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
1187
+ return (index.h("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, index.h("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
1188
+ // Once the field has a value it is authoritative — falling back to
1189
+ // the default here would keep a ticked-by-default box ticked after
1190
+ // the user unticks it.
1191
+ this.value === undefined
1192
+ ? component.config?.default_value === true
1193
+ : this.value === "true", required: component.required, disabled: this.disabled, onChange: this.handleCheckbox }), index.h("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
749
1194
  }
750
1195
  renderLegalField(component) {
751
1196
  const text = component.config?.text ?? component.label ?? "";