@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
@@ -369,6 +369,100 @@ input[type="date"].input-field+.input-label {
369
369
  left: 8px;
370
370
  }
371
371
 
372
+ /* ==========================================================================
373
+ Date Input - Segmented day / month / year
374
+ Part: date-input, date-segment, date-separator
375
+
376
+ The group is styled as one field: the outer box carries the border and
377
+ focus ring, the segments inside are borderless so three inputs read as a
378
+ single control.
379
+ ========================================================================== */
380
+
381
+ .date-input {
382
+ display: flex;
383
+ align-items: center;
384
+ width: 100%;
385
+ padding: 16px;
386
+ box-sizing: border-box;
387
+ background-color: var(--ah-color-input-bg, #ffffff);
388
+ border: var(--ah-input-border-width, 1px) solid
389
+ var(--ah-color-border, #c9cace);
390
+ border-radius: var(--ah-input-radius, 3px);
391
+ transition:
392
+ border-color 0.15s ease-out,
393
+ box-shadow 0.15s ease-out;
394
+ }
395
+
396
+ .date-input:hover {
397
+ border-color: var(--ah-color-border-hover, #65676e);
398
+ }
399
+
400
+ .date-input:focus-within {
401
+ border-color: var(--ah-color-primary, #635dff);
402
+ box-shadow: inset 0 0 0 1px var(--ah-color-primary, #635dff);
403
+ }
404
+
405
+ .date-input.has-error {
406
+ border-color: var(--ah-color-error, #d03c38);
407
+ }
408
+
409
+ .date-input.has-error:focus-within {
410
+ box-shadow: inset 0 0 0 1px var(--ah-color-error, #d03c38);
411
+ }
412
+
413
+ /* Forced-colors mode drops box-shadow, which is the only thing marking the
414
+ segment group as focused — the border colour is overridden too. Draw an
415
+ outline in a system colour so keyboard focus stays visible. */
416
+ @media (forced-colors: active) {
417
+ .date-input:focus-within,
418
+ .date-input.has-error:focus-within {
419
+ outline: 2px solid Highlight;
420
+ outline-offset: 1px;
421
+ }
422
+ }
423
+
424
+ .date-segment {
425
+ /* Sized in ch so the boxes track the font: wide enough for the placeholder
426
+ tokens (DD/MM are wider than "00") without leaving a gap around a typed
427
+ value. The year box holds twice the digits. */
428
+ width: 3.25ch;
429
+ min-width: 0;
430
+ padding: 0;
431
+ font-size: 16px;
432
+ font-family: inherit;
433
+ text-align: center;
434
+ color: var(--ah-color-input-text, var(--ah-color-text, #1e212a));
435
+ background: transparent;
436
+ border: none;
437
+ outline: none;
438
+ }
439
+
440
+ .date-segment[data-date-segment="year"] {
441
+ width: 5.5ch;
442
+ }
443
+
444
+ /* Shown here, unlike other fields: the tokens are the field's format hint */
445
+ .date-segment::placeholder {
446
+ color: var(--ah-color-text-muted, #65676e);
447
+ opacity: 1;
448
+ }
449
+
450
+ .date-segment:disabled {
451
+ color: var(--ah-color-text-disabled, #a0a0a0);
452
+ cursor: not-allowed;
453
+ }
454
+
455
+ .date-input:has(.date-segment:disabled) {
456
+ background-color: var(--ah-color-bg-disabled, #f5f5f5);
457
+ }
458
+
459
+ .date-separator {
460
+ padding: 0 4px;
461
+ font-size: 16px;
462
+ color: var(--ah-color-text-muted, #65676e);
463
+ user-select: none;
464
+ }
465
+
372
466
  /* Social buttons container */
373
467
  .social-buttons {
374
468
  display: flex;
@@ -1,5 +1,6 @@
1
1
  import { h, } from "@stencil/core";
2
2
  import { countries, getCountryByCode, } from "../../utils/country-data";
3
+ import { expandTwoDigitYear, getDateLayout, parseIsoDate, resolveYearAnchor, toIsoDate, } from "../../utils/date-format";
3
4
  export class AuthheroNode {
4
5
  /**
5
6
  * The component configuration to render.
@@ -15,6 +16,12 @@ export class AuthheroNode {
15
16
  * Whether the component is disabled.
16
17
  */
17
18
  disabled = false;
19
+ /**
20
+ * BCP-47 locale used for locale-dependent field layout (currently the
21
+ * segment order of DATE fields). Resolved server-side so SSR and hydration
22
+ * agree; falls back to day-month-year when absent.
23
+ */
24
+ locale;
18
25
  /**
19
26
  * Whether the password field is visible.
20
27
  */
@@ -36,6 +43,15 @@ export class AuthheroNode {
36
43
  * When true, the value is emitted as-is without dial code prefix.
37
44
  */
38
45
  telEmailMode = false;
46
+ /**
47
+ * Segment values for the DATE input, each held as typed (unpadded while the
48
+ * user is mid-entry) and normalised on blur.
49
+ */
50
+ dateSegments = {
51
+ year: "",
52
+ month: "",
53
+ day: "",
54
+ };
39
55
  /**
40
56
  * Emitted when a field value changes.
41
57
  */
@@ -47,13 +63,16 @@ export class AuthheroNode {
47
63
  componentChanged() {
48
64
  this.initCountryFromConfig();
49
65
  this.initTelValue();
66
+ this.initDateValue();
50
67
  }
51
68
  valueChanged() {
52
69
  this.initTelValue();
70
+ this.initDateValue();
53
71
  }
54
72
  componentWillLoad() {
55
73
  this.initCountryFromConfig();
56
74
  this.initTelValue();
75
+ this.initDateValue();
57
76
  }
58
77
  componentDidLoad() {
59
78
  // Auto-focus a code field so the caret starts in the first (leftmost) box.
@@ -79,6 +98,20 @@ export class AuthheroNode {
79
98
  }
80
99
  }
81
100
  }
101
+ /**
102
+ * The last value this field emitted for a TEL component. The widget mirrors
103
+ * every emitted value back onto the `value` prop, and that echo must not be
104
+ * re-parsed: the wire format carries only a dial code, so a country picked
105
+ * from the list would be replaced by the first entry sharing that code — a
106
+ * user who chose Canada would be silently moved to the US on their next
107
+ * keystroke.
108
+ */
109
+ lastEmittedTel;
110
+ /** Emit a TEL value and remember it, so the echo can be recognised. */
111
+ emitTelValue(value) {
112
+ this.lastEmittedTel = value;
113
+ this.fieldChange.emit({ id: this.component.id, value });
114
+ }
82
115
  /**
83
116
  * Hydrate localPhoneNumber (and selectedCountry) from the effective value
84
117
  * for TEL fields. The full value is stored as `{dialCode}{localNumber}`,
@@ -87,6 +120,10 @@ export class AuthheroNode {
87
120
  initTelValue() {
88
121
  if (this.component?.type !== "TEL")
89
122
  return;
123
+ if (this.lastEmittedTel !== undefined &&
124
+ this.getEffectiveValue() === this.lastEmittedTel) {
125
+ return;
126
+ }
90
127
  const config = this.component.config;
91
128
  const allowEmail = config?.allow_email === true;
92
129
  const fullValue = this.getEffectiveValue();
@@ -121,11 +158,16 @@ export class AuthheroNode {
121
158
  handleCountryChange = (e) => {
122
159
  const target = e.target;
123
160
  this.selectedCountry = getCountryByCode(target.value);
161
+ // A half-typed "+xx" prefix is replaced by the picked country, not kept
162
+ // as a local number (it would emit "+46+4").
163
+ if (this.localPhoneNumber.startsWith("+")) {
164
+ this.localPhoneNumber = "";
165
+ }
124
166
  // Re-emit the full phone number with new dial code
125
167
  const fullNumber = this.localPhoneNumber
126
168
  ? `${this.selectedCountry.dialCode}${this.localPhoneNumber}`
127
169
  : "";
128
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
170
+ this.emitTelValue(fullNumber);
129
171
  };
130
172
  /**
131
173
  * Try to detect a dial code prefix in the raw input (e.g. "+46", "0046")
@@ -163,7 +205,21 @@ export class AuthheroNode {
163
205
  target.value = cleanedLocal;
164
206
  this.localPhoneNumber = cleanedLocal;
165
207
  const fullNumber = `${this.selectedCountry.dialCode}${cleanedLocal}`;
166
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
208
+ this.emitTelValue(fullNumber);
209
+ }
210
+ else if (value.startsWith("+")) {
211
+ // A leading "+" is an international prefix the user is still typing.
212
+ // Keep it in the field so the dial code can build up until it matches
213
+ // a country — stripping it per keystroke means "+46" never resolves to
214
+ // Sweden, since each character is discarded before the next arrives.
215
+ const cleaned = `+${value.slice(1).replace(/[^\d\s\-()]/g, "")}`;
216
+ if (cleaned !== value) {
217
+ target.value = cleaned;
218
+ }
219
+ this.localPhoneNumber = cleaned;
220
+ // Emit the partial prefix as-is; prefixing it with the (not yet chosen)
221
+ // country's dial code would produce nonsense like "+1+4".
222
+ this.emitTelValue(cleaned);
167
223
  }
168
224
  else {
169
225
  const cleaned = allowPlus
@@ -176,7 +232,7 @@ export class AuthheroNode {
176
232
  const fullNumber = cleaned
177
233
  ? `${this.selectedCountry.dialCode}${cleaned}`
178
234
  : "";
179
- this.fieldChange.emit({ id: this.component.id, value: fullNumber });
235
+ this.emitTelValue(fullNumber);
180
236
  }
181
237
  }
182
238
  handlePhoneInput = (e) => {
@@ -196,7 +252,7 @@ export class AuthheroNode {
196
252
  else {
197
253
  // Email or text — emit as-is
198
254
  this.localPhoneNumber = value;
199
- this.fieldChange.emit({ id: this.component.id, value });
255
+ this.emitTelValue(value);
200
256
  }
201
257
  return;
202
258
  }
@@ -207,6 +263,196 @@ export class AuthheroNode {
207
263
  const target = e.target;
208
264
  this.fieldChange.emit({ id: this.component.id, value: target.value });
209
265
  };
266
+ // ===========================================================================
267
+ // DATE segment handling
268
+ // ===========================================================================
269
+ /**
270
+ * The last ISO value this field emitted. The widget mirrors every emitted
271
+ * value back onto the `value` prop, and that echo must not be treated as an
272
+ * external change: it would re-format the segments under the caret while
273
+ * the user is still typing (a day of "3" comes back as "03", swallowing the
274
+ * next digit).
275
+ */
276
+ lastEmittedDate;
277
+ /** Hydrate the day/month/year segments from the effective ISO value. */
278
+ initDateValue() {
279
+ if (this.component?.type !== "DATE")
280
+ return;
281
+ if (this.lastEmittedDate !== undefined &&
282
+ this.getEffectiveValue() === this.lastEmittedDate) {
283
+ return;
284
+ }
285
+ const parsed = parseIsoDate(this.getEffectiveValue());
286
+ if (parsed) {
287
+ this.dateSegments = parsed;
288
+ return;
289
+ }
290
+ // No parseable value. Only clear when we currently hold a complete date,
291
+ // i.e. this is a genuine external reset: a half-typed date emits "" and
292
+ // the parent echoes that straight back as the value prop, which would
293
+ // otherwise wipe the segment the user just typed into.
294
+ if (toIsoDate(this.dateSegments) !== "") {
295
+ this.dateSegments = { year: "", month: "", day: "" };
296
+ }
297
+ }
298
+ dateConfig() {
299
+ return this.component.config;
300
+ }
301
+ getDateLayoutForField() {
302
+ const config = this.dateConfig();
303
+ return getDateLayout(config?.format, this.locale);
304
+ }
305
+ static SEGMENT_LENGTH = {
306
+ day: 2,
307
+ month: 2,
308
+ year: 4,
309
+ };
310
+ /** Birthdate autofill tokens — the common case for a typed date. */
311
+ static SEGMENT_AUTOCOMPLETE = {
312
+ day: "bday-day",
313
+ month: "bday-month",
314
+ year: "bday-year",
315
+ };
316
+ /**
317
+ * The submittable value for the current segments: an ISO date, or "" when
318
+ * it is incomplete, impossible, or outside the field's `min`/`max`. ISO
319
+ * strings compare chronologically, so the bounds need no parsing.
320
+ */
321
+ dateIsoValue(segments) {
322
+ const iso = toIsoDate(segments);
323
+ if (!iso)
324
+ return "";
325
+ const config = this.dateConfig();
326
+ const min = config?.min;
327
+ const max = config?.max;
328
+ if (typeof min === "string" && parseIsoDate(min) && iso < min)
329
+ return "";
330
+ if (typeof max === "string" && parseIsoDate(max) && iso > max)
331
+ return "";
332
+ return iso;
333
+ }
334
+ setDateSegments(segments) {
335
+ this.dateSegments = segments;
336
+ this.lastEmittedDate = this.dateIsoValue(segments);
337
+ this.fieldChange.emit({
338
+ id: this.component.id,
339
+ value: this.lastEmittedDate,
340
+ });
341
+ }
342
+ focusDateSegment(segment) {
343
+ const input = this.host.shadowRoot?.querySelector(`input[data-date-segment="${segment}"]`);
344
+ input?.focus();
345
+ input?.select();
346
+ }
347
+ /**
348
+ * Whether typing this digit completes the segment. A segment is complete at
349
+ * its full width, and also as soon as a further digit could not produce a
350
+ * valid number — no day starts with 4 and no month starts with 2.
351
+ */
352
+ isDateSegmentComplete(segment, value) {
353
+ if (value.length >= AuthheroNode.SEGMENT_LENGTH[segment])
354
+ return true;
355
+ if (value.length !== 1)
356
+ return false;
357
+ if (segment === "day")
358
+ return Number(value) > 3;
359
+ if (segment === "month")
360
+ return Number(value) > 1;
361
+ return false;
362
+ }
363
+ handleDateSegmentInput = (segment) => (e) => {
364
+ const target = e.target;
365
+ const digits = target.value
366
+ .replace(/\D/g, "")
367
+ .slice(0, AuthheroNode.SEGMENT_LENGTH[segment]);
368
+ if (digits !== target.value) {
369
+ target.value = digits;
370
+ }
371
+ this.setDateSegments({ ...this.dateSegments, [segment]: digits });
372
+ if (this.isDateSegmentComplete(segment, digits)) {
373
+ const order = this.getDateLayoutForField().order;
374
+ const next = order[order.indexOf(segment) + 1];
375
+ if (next)
376
+ this.focusDateSegment(next);
377
+ }
378
+ };
379
+ /**
380
+ * Pad a single digit ("5" -> "05") and expand a two-digit year against the
381
+ * field's upper bound ("85" -> 1985). Shared by blur and paste so a pasted
382
+ * date is read the same way as a typed one.
383
+ */
384
+ normalizeDateSegment(segment, value) {
385
+ if (segment !== "year")
386
+ return value.padStart(2, "0");
387
+ return expandTwoDigitYear(value, resolveYearAnchor(this.dateConfig()?.max, new Date()));
388
+ }
389
+ /**
390
+ * Normalise on blur: pad a single digit ("5" -> "05") and expand a two-digit
391
+ * year against the field's upper bound ("85" -> 1985).
392
+ */
393
+ handleDateSegmentBlur = (segment) => (e) => {
394
+ const target = e.target;
395
+ const value = target.value.replace(/\D/g, "");
396
+ if (!value)
397
+ return;
398
+ const normalized = this.normalizeDateSegment(segment, value);
399
+ if (normalized === value)
400
+ return;
401
+ target.value = normalized;
402
+ this.setDateSegments({ ...this.dateSegments, [segment]: normalized });
403
+ };
404
+ /**
405
+ * Backspace at the start of an empty segment moves to the previous one, so
406
+ * the whole date can be cleared without reaching for the mouse.
407
+ */
408
+ handleDateSegmentKeyDown = (segment) => (e) => {
409
+ if (e.key === "Enter") {
410
+ e.preventDefault();
411
+ this.buttonClick.emit({ id: "submit", type: "submit", value: "next" });
412
+ return;
413
+ }
414
+ if (e.key !== "Backspace")
415
+ return;
416
+ const target = e.target;
417
+ if (target.value.length > 0 || target.selectionStart !== 0)
418
+ return;
419
+ const order = this.getDateLayoutForField().order;
420
+ const previous = order[order.indexOf(segment) - 1];
421
+ if (previous) {
422
+ e.preventDefault();
423
+ this.focusDateSegment(previous);
424
+ }
425
+ };
426
+ /**
427
+ * Pasting a whole date into any segment fills all three. An ISO value is
428
+ * read year-first; anything else follows the field's displayed order. A
429
+ * two-digit year is expanded the same way a typed one is, so "15/03/85"
430
+ * pastes as 1985 rather than falling through to the browser's own paste.
431
+ */
432
+ handleDatePaste = (e) => {
433
+ const text = e.clipboardData?.getData("text") ?? "";
434
+ const groups = text.match(/\d+/g);
435
+ if (!groups || groups.length < 3)
436
+ return;
437
+ const [first, second, third] = groups;
438
+ const isoLike = first.length === 4;
439
+ const order = isoLike
440
+ ? ["year", "month", "day"]
441
+ : this.getDateLayoutForField().order;
442
+ const segments = {
443
+ year: "",
444
+ month: "",
445
+ day: "",
446
+ };
447
+ [first, second, third].forEach((group, index) => {
448
+ const segment = order[index];
449
+ segments[segment] = this.normalizeDateSegment(segment, group.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]));
450
+ });
451
+ if (!toIsoDate(segments))
452
+ return;
453
+ e.preventDefault();
454
+ this.setDateSegments(segments);
455
+ };
210
456
  handleKeyDown = (e) => {
211
457
  if (e.key === "Enter") {
212
458
  e.preventDefault();
@@ -219,7 +465,7 @@ export class AuthheroNode {
219
465
  if (config?.allow_email === true && target.value.length === 0) {
220
466
  this.telEmailMode = true;
221
467
  this.localPhoneNumber = "";
222
- this.fieldChange.emit({ id: this.component.id, value: "" });
468
+ this.emitTelValue("");
223
469
  }
224
470
  }
225
471
  };
@@ -494,17 +740,40 @@ export class AuthheroNode {
494
740
  const effectiveValue = this.getEffectiveValue();
495
741
  return (h("div", { class: "input-wrapper", part: "input-wrapper" }, this.renderLabel(component.label, inputId, component.required), 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)));
496
742
  }
743
+ /**
744
+ * DATE is rendered as three numeric segments rather than `input type=date`.
745
+ * A native date input renders differently in every browser (Safari adds a
746
+ * stepper), keeps its dd/mm/yyyy hint visible while half-filled, and asks
747
+ * for a calendar gesture to reach a year decades back — all wrong for a
748
+ * date typed from memory, which is what these fields collect.
749
+ */
497
750
  renderDateField(component) {
498
- const inputId = `input-${component.id}`;
499
751
  const errors = this.getErrors();
500
- const { min, max } = component.config ?? {};
501
- // Date fields always have a value (even if placeholder format), so always float the label
502
- const hasValue = true;
503
- const effectiveValue = this.getEffectiveValue();
504
- return (h("div", { class: "input-wrapper", part: "input-wrapper" }, h("div", { class: "input-container" }, 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)));
752
+ const { format } = component.config ?? {};
753
+ const layout = getDateLayout(format, this.locale);
754
+ const isoValue = this.dateIsoValue(this.dateSegments);
755
+ // Every segment filled but no ISO value means the date does not exist
756
+ // (31 February) or falls outside min/max. Flag it rather than silently
757
+ // submitting nothing — the widget posts JSON, so the browser's own
758
+ // constraint validation never runs.
759
+ const isIncomplete = layout.order.some((segment) => this.dateSegments[segment].length === 0);
760
+ const hasInvalidDate = !isoValue && !isIncomplete;
761
+ const hasError = errors.length > 0 || hasInvalidDate;
762
+ const firstInputId = `input-${component.id}`;
763
+ const segmentInput = (segment) => (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 }));
764
+ return (h("div", { class: "input-wrapper", part: "input-wrapper" }, h("div", { class: "input-container" }, h("div", { class: hasError ? "date-input has-error" : "date-input", part: "date-input", role: "group", "aria-label": component.label }, layout.order.map((segment, index) => [
765
+ index > 0 && (h("span", { key: `sep-${segment}`, class: "date-separator", part: "date-separator", "aria-hidden": "true" }, layout.separator)),
766
+ segmentInput(segment),
767
+ ])), this.renderFloatingLabel(component.label, firstInputId, component.required, true)), h("input", { type: "hidden", name: component.id, "data-input-name": component.id, value: isoValue }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
505
768
  }
506
769
  renderBooleanField(component) {
507
- return (h("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, 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 }), h("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
770
+ return (h("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, h("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
771
+ // Once the field has a value it is authoritative — falling back to
772
+ // the default here would keep a ticked-by-default box ticked after
773
+ // the user unticks it.
774
+ this.value === undefined
775
+ ? component.config?.default_value === true
776
+ : this.value === "true", required: component.required, disabled: this.disabled, onChange: this.handleCheckbox }), h("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
508
777
  }
509
778
  renderLegalField(component) {
510
779
  const text = component.config?.text ?? component.label ?? "";
@@ -750,6 +1019,25 @@ export class AuthheroNode {
750
1019
  "reflect": false,
751
1020
  "attribute": "disabled",
752
1021
  "defaultValue": "false"
1022
+ },
1023
+ "locale": {
1024
+ "type": "string",
1025
+ "mutable": false,
1026
+ "complexType": {
1027
+ "original": "string",
1028
+ "resolved": "string | undefined",
1029
+ "references": {}
1030
+ },
1031
+ "required": false,
1032
+ "optional": true,
1033
+ "docs": {
1034
+ "tags": [],
1035
+ "text": "BCP-47 locale used for locale-dependent field layout (currently the\nsegment order of DATE fields). Resolved server-side so SSR and hydration\nagree; falls back to day-month-year when absent."
1036
+ },
1037
+ "getter": false,
1038
+ "setter": false,
1039
+ "reflect": false,
1040
+ "attribute": "locale"
753
1041
  }
754
1042
  };
755
1043
  }
@@ -759,7 +1047,8 @@ export class AuthheroNode {
759
1047
  "selectedCountry": {},
760
1048
  "localPhoneNumber": {},
761
1049
  "countryDropdownOpen": {},
762
- "telEmailMode": {}
1050
+ "telEmailMode": {},
1051
+ "dateSegments": {}
763
1052
  };
764
1053
  }
765
1054
  static get events() {