@latty-ds/web 0.10.3 → 1.0.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 (101) hide show
  1. package/custom-elements.json +1970 -229
  2. package/dist/components/accordion/index.js +36 -2
  3. package/dist/components/alert/alert.d.ts +29 -6
  4. package/dist/components/alert/alert.types.d.ts +5 -0
  5. package/dist/components/alert/index.js +127 -20
  6. package/dist/components/avatar/avatar.d.ts +2 -0
  7. package/dist/components/avatar/index.js +10 -3
  8. package/dist/components/badge/index.js +1 -1
  9. package/dist/components/breadcrumb/breadcrumb.d.ts +4 -0
  10. package/dist/components/breadcrumb/breadcrumb.types.d.ts +5 -0
  11. package/dist/components/breadcrumb/index.js +20 -3
  12. package/dist/components/button/button.d.ts +33 -0
  13. package/dist/components/button/index.js +138 -15
  14. package/dist/components/calendar/calendar.d.ts +15 -7
  15. package/dist/components/calendar/calendar.types.d.ts +17 -1
  16. package/dist/components/calendar/index.js +80 -27
  17. package/dist/components/checkbox/checkbox.d.ts +14 -2
  18. package/dist/components/checkbox/checkbox.types.d.ts +5 -0
  19. package/dist/components/checkbox/index.js +82 -5
  20. package/dist/components/chip/chip.d.ts +4 -1
  21. package/dist/components/chip/chip.types.d.ts +5 -0
  22. package/dist/components/chip/index.js +29 -1
  23. package/dist/components/color-input/color-input.d.ts +11 -2
  24. package/dist/components/color-input/color-input.types.d.ts +5 -0
  25. package/dist/components/color-input/index.js +92 -8
  26. package/dist/components/combobox/combobox.d.ts +18 -2
  27. package/dist/components/combobox/combobox.types.d.ts +7 -0
  28. package/dist/components/combobox/index.js +164 -23
  29. package/dist/components/date-input/date-input.d.ts +22 -7
  30. package/dist/components/date-input/date-input.types.d.ts +7 -0
  31. package/dist/components/date-input/index.js +218 -55
  32. package/dist/components/datepicker/datepicker.d.ts +9 -1
  33. package/dist/components/datepicker/index.js +108 -15
  34. package/dist/components/dialog/dialog.d.ts +39 -19
  35. package/dist/components/dialog/dialog.types.d.ts +5 -0
  36. package/dist/components/dialog/index.js +240 -93
  37. package/dist/components/divider/index.js +2 -2
  38. package/dist/components/dropdown/dropdown-item.d.ts +1 -1
  39. package/dist/components/dropdown/dropdown.d.ts +4 -0
  40. package/dist/components/dropdown/index.js +43 -3
  41. package/dist/components/icon-button/icon-button.d.ts +4 -0
  42. package/dist/components/icon-button/index.js +39 -4
  43. package/dist/components/image/image.d.ts +7 -7
  44. package/dist/components/image/index.js +6 -11
  45. package/dist/components/list/index.js +14 -4
  46. package/dist/components/nav/index.js +37 -17
  47. package/dist/components/nav/nav-item.d.ts +8 -8
  48. package/dist/components/pagination/index.js +57 -16
  49. package/dist/components/pagination/pagination.d.ts +8 -1
  50. package/dist/components/pagination/pagination.types.d.ts +15 -0
  51. package/dist/components/progress/index.js +44 -6
  52. package/dist/components/progress/progress.d.ts +12 -0
  53. package/dist/components/radio/index.js +148 -9
  54. package/dist/components/radio/radio.d.ts +34 -2
  55. package/dist/components/radio/radio.types.d.ts +5 -0
  56. package/dist/components/radio-group/index.js +120 -29
  57. package/dist/components/radio-group/radio-group.d.ts +27 -2
  58. package/dist/components/select/index.js +197 -44
  59. package/dist/components/select/select.d.ts +38 -4
  60. package/dist/components/select/select.types.d.ts +5 -0
  61. package/dist/components/shared/aria-forwarding.d.ts +1 -1
  62. package/dist/components/shared/form-default.d.ts +30 -0
  63. package/dist/components/shared/form-disabled.d.ts +40 -0
  64. package/dist/components/shared/form-field.d.ts +7 -0
  65. package/dist/components/shared/form-validity.d.ts +17 -0
  66. package/dist/components/shared/roving.d.ts +11 -0
  67. package/dist/components/shared/scroll-lock.d.ts +2 -0
  68. package/dist/components/sidepanel/index.js +278 -75
  69. package/dist/components/sidepanel/sidepanel.d.ts +32 -10
  70. package/dist/components/sidepanel/sidepanel.types.d.ts +10 -1
  71. package/dist/components/skeleton/index.js +3 -8
  72. package/dist/components/slider/index.js +307 -75
  73. package/dist/components/slider/slider.d.ts +16 -3
  74. package/dist/components/snackbar/index.js +82 -9
  75. package/dist/components/snackbar/snackbar.d.ts +17 -1
  76. package/dist/components/snackbar/snackbar.types.d.ts +5 -0
  77. package/dist/components/surface/index.js +8 -0
  78. package/dist/components/switch/index.js +101 -5
  79. package/dist/components/switch/switch.d.ts +13 -2
  80. package/dist/components/switch/switch.types.d.ts +5 -0
  81. package/dist/components/tab/index.js +15 -14
  82. package/dist/components/tab/tab.d.ts +0 -1
  83. package/dist/components/tab-group/index.js +102 -30
  84. package/dist/components/tab-group/tab-group.d.ts +17 -2
  85. package/dist/components/table/index.js +105 -61
  86. package/dist/components/table/table.d.ts +8 -2
  87. package/dist/components/table/table.types.d.ts +4 -3
  88. package/dist/components/text/index.js +16 -1
  89. package/dist/components/text/text.d.ts +1 -1
  90. package/dist/components/textfield/index.js +243 -35
  91. package/dist/components/textfield/textfield.d.ts +24 -2
  92. package/dist/components/textfield/textfield.types.d.ts +11 -0
  93. package/dist/components/tooltip/index.js +176 -49
  94. package/dist/components/tooltip/tooltip.d.ts +17 -0
  95. package/dist/components/tooltip/tooltip.types.d.ts +5 -1
  96. package/dist/index.cjs +2467 -692
  97. package/dist/index.js +2478 -699
  98. package/dist/manifest.json +202 -32
  99. package/dist/utils/dev-warn.d.ts +5 -0
  100. package/dist/utils/index.d.ts +1 -0
  101. package/package.json +4 -8
@@ -9,8 +9,13 @@ var __decorateClass = (decorators, target, key, kind) => {
9
9
  return result;
10
10
  };
11
11
 
12
+ // src/components/shared/form-field.ts
13
+ var fieldCount = 0;
14
+ var createFieldIds = () => ({ helper: `lt-field-${fieldCount++}-helper` });
15
+
12
16
  // src/components/date-input/date-input.ts
13
- import { html as html3, nothing } from "lit";
17
+ import { html as html3, nothing as nothing2 } from "lit";
18
+ import { ifDefined } from "lit/directives/if-defined.js";
14
19
  import { customElement as customElement3, property as property4, state as state2 } from "lit/decorators.js";
15
20
 
16
21
  // src/base/themeable-element.ts
@@ -39,7 +44,7 @@ __decorateClass([
39
44
  import "@latty-ds/icons";
40
45
 
41
46
  // src/components/calendar/calendar.ts
42
- import { html } from "lit";
47
+ import { html, nothing } from "lit";
43
48
  import { customElement, property as property2, state } from "lit/decorators.js";
44
49
  import "@latty-ds/icons";
45
50
 
@@ -189,7 +194,9 @@ var calendarStyles = css`
189
194
  width: 100%;
190
195
  }
191
196
 
192
- .day:disabled {
197
+ /* aria-disabled, not the disabled attribute — a disabled day stays focusable, so keyboard users can arrow
198
+ through it instead of the roving tabindex skipping straight over it (it just can't be selected). */
199
+ .day[aria-disabled='true'] {
193
200
  cursor: not-allowed;
194
201
  }
195
202
 
@@ -212,7 +219,7 @@ var calendarStyles = css`
212
219
  width: 2.25rem;
213
220
  }
214
221
 
215
- .day:hover:not(:disabled) .day__inner {
222
+ .day:hover:not([aria-disabled='true']) .day__inner {
216
223
  background: var(--lt-bg-neutral-subtle);
217
224
  }
218
225
 
@@ -235,14 +242,14 @@ var calendarStyles = css`
235
242
  font-weight: 600;
236
243
  }
237
244
 
238
- .day--selected:hover:not(:disabled) .day__inner {
245
+ .day--selected:hover:not([aria-disabled='true']) .day__inner {
239
246
  background: var(--lt-interactive-primary-bg-hover);
240
247
  }
241
248
 
242
- /* Outside days */
249
+ /* Outside days — dimmed by the muted text color alone. They're enabled, clickable buttons, and a 0.5
250
+ opacity on top of muted text took them to ~2:1 (WCAG AA needs 4.5:1). */
243
251
  .day--outside {
244
252
  color: var(--lt-text-muted);
245
- opacity: 0.5;
246
253
  }
247
254
 
248
255
  /* Empty filler cell */
@@ -253,7 +260,7 @@ var calendarStyles = css`
253
260
  pointer-events: none;
254
261
  }
255
262
 
256
- .day:disabled .day__inner {
263
+ .day[aria-disabled='true'] .day__inner {
257
264
  color: var(--lt-text-disabled);
258
265
  opacity: 0.4;
259
266
  }
@@ -280,7 +287,7 @@ var calendarStyles = css`
280
287
  background: var(--lt-bg-primary-subtle);
281
288
  }
282
289
 
283
- .day--in-range:hover:not(:disabled) .day__inner {
290
+ .day--in-range:hover:not([aria-disabled='true']) .day__inner {
284
291
  background: var(--lt-bg-neutral-subtle);
285
292
  }
286
293
 
@@ -298,7 +305,7 @@ var calendarStyles = css`
298
305
  font-weight: 600;
299
306
  }
300
307
 
301
- .day--range-confirmed:hover:not(:disabled) .day__inner {
308
+ .day--range-confirmed:hover:not([aria-disabled='true']) .day__inner {
302
309
  background: var(--lt-interactive-primary-bg-hover);
303
310
  }
304
311
 
@@ -421,6 +428,20 @@ var calendarStyles = css`
421
428
  cursor: not-allowed;
422
429
  opacity: 0.5;
423
430
  }
431
+
432
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
433
+
434
+ @media (prefers-reduced-motion: reduce) {
435
+ .month-label-btn,
436
+ .picker-chevron,
437
+ .nav-btn,
438
+ .day,
439
+ .day__inner,
440
+ .picker-month-btn,
441
+ .today-btn {
442
+ transition: none;
443
+ }
444
+ }
424
445
  `;
425
446
 
426
447
  // src/utils/dispatch.ts
@@ -437,7 +458,30 @@ var createClickOutsideHandler = (host, onOutside, options) => {
437
458
  return () => document.removeEventListener(eventName, handler, capture ? { capture: true } : void 0);
438
459
  };
439
460
 
461
+ // src/utils/dev-warn.ts
462
+ var devWarn = (tag, message) => {
463
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
464
+ console.warn(`[${tag}] ${message}`);
465
+ }
466
+ };
467
+
440
468
  // src/components/calendar/calendar.ts
469
+ var DEFAULT_LABELS = {
470
+ today: "Today",
471
+ previousMonth: "Previous month",
472
+ nextMonth: "Next month",
473
+ previousYear: "Previous year",
474
+ nextYear: "Next year",
475
+ chooseMonthYear: (monthYear) => `${monthYear}, choose month and year`
476
+ };
477
+ function deriveWeekStart(locale) {
478
+ try {
479
+ const info = new Intl.Locale(locale).getWeekInfo?.();
480
+ if (info) return String(info.firstDay % 7);
481
+ } catch {
482
+ }
483
+ return "0";
484
+ }
441
485
  var Calendar = class extends ThemeableElement {
442
486
  constructor() {
443
487
  super(...arguments);
@@ -448,12 +492,11 @@ var Calendar = class extends ThemeableElement {
448
492
  this.min = "";
449
493
  this.max = "";
450
494
  this.locale = "en-US";
451
- this.weekStart = "0";
452
495
  this.showOutsideDays = true;
453
496
  this.disabled = false;
454
497
  this.months = 1;
455
- /** Specific dates to disable, as an array of Date objects. Not reflected as an attribute. */
456
498
  this.disabledDates = [];
499
+ this.labels = {};
457
500
  this._viewYear = (/* @__PURE__ */ new Date()).getFullYear();
458
501
  this._viewMonth = (/* @__PURE__ */ new Date()).getMonth();
459
502
  this._focusedDate = null;
@@ -480,10 +523,10 @@ var Calendar = class extends ThemeableElement {
480
523
  if (this.mode === "range") this._hoverDate = null;
481
524
  };
482
525
  this._handleKeyDown = (e) => {
483
- const today = /* @__PURE__ */ new Date();
484
- const focusedDate = this._focusedDate ?? this._parseDate(this.value) ?? new Date(this._viewYear, this._viewMonth, today.getDate() || 1);
526
+ const currentIso = e.currentTarget.dataset.date ?? "";
527
+ const focusedDate = this._parseDate(currentIso) ?? new Date(this._viewYear, this._viewMonth, 1);
485
528
  const next = new Date(focusedDate);
486
- const weekStart = Number(this.weekStart);
529
+ const weekStart = Number(this._weekStart);
487
530
  switch (e.key) {
488
531
  case "ArrowRight":
489
532
  next.setDate(next.getDate() + 1);
@@ -535,6 +578,13 @@ var Calendar = class extends ThemeableElement {
535
578
  this._focusCell(this._toIso(next));
536
579
  };
537
580
  }
581
+ /** `weekStart` if explicitly set, otherwise derived from `locale`. Internal code should always read this, never `weekStart` directly. */
582
+ get _weekStart() {
583
+ return this.weekStart ?? deriveWeekStart(this.locale);
584
+ }
585
+ get _labels() {
586
+ return { ...DEFAULT_LABELS, ...this.labels };
587
+ }
538
588
  willUpdate(changed) {
539
589
  super.willUpdate(changed);
540
590
  if (changed.has("value") && this.value) {
@@ -593,7 +643,7 @@ var Calendar = class extends ThemeableElement {
593
643
  _buildDays(year = this._viewYear, month = this._viewMonth) {
594
644
  const today = /* @__PURE__ */ new Date();
595
645
  const selected = this._parseDate(this.value);
596
- const weekStart = Number(this.weekStart);
646
+ const weekStart = Number(this._weekStart);
597
647
  const rangeStart = this.mode === "range" ? this._parseDate(this.valueStart) : null;
598
648
  const rangeEnd = this.mode === "range" ? this._parseDate(this.valueEnd) : null;
599
649
  let visHoverStart = null;
@@ -646,7 +696,7 @@ var Calendar = class extends ThemeableElement {
646
696
  return days;
647
697
  }
648
698
  _getWeekdayLabels() {
649
- const weekStart = Number(this.weekStart);
699
+ const weekStart = Number(this._weekStart);
650
700
  const fmt = new Intl.DateTimeFormat(this.locale, { weekday: "narrow" });
651
701
  const refSunday = new Date(2026, 0, 4);
652
702
  return Array.from({ length: 7 }, (_, i) => {
@@ -673,7 +723,7 @@ var Calendar = class extends ThemeableElement {
673
723
  this._viewMonth = month;
674
724
  this._viewYear = year;
675
725
  this._focusedDate = null;
676
- dispatch(this, "month-change", { year: this._viewYear, month: this._viewMonth });
726
+ dispatch(this, "monthchange", { year: this._viewYear, month: this._viewMonth });
677
727
  }
678
728
  _openPicker() {
679
729
  this._pickerYear = this._viewYear;
@@ -687,7 +737,7 @@ var Calendar = class extends ThemeableElement {
687
737
  this._viewMonth = month;
688
738
  this._pickingMonthYear = false;
689
739
  this._focusedDate = null;
690
- dispatch(this, "month-change", { year: this._viewYear, month: this._viewMonth });
740
+ dispatch(this, "monthchange", { year: this._viewYear, month: this._viewMonth });
691
741
  }
692
742
  _selectDay(day) {
693
743
  if (day.isDisabled || this.disabled) return;
@@ -773,7 +823,7 @@ var Calendar = class extends ThemeableElement {
773
823
  role="gridcell"
774
824
  class=${classes}
775
825
  data-date=${iso}
776
- ?disabled=${day.isDisabled}
826
+ aria-disabled=${day.isDisabled ? "true" : nothing}
777
827
  aria-selected=${isSelected ? "true" : "false"}
778
828
  aria-label=${this._dayAriaLabel(day.date)}
779
829
  tabindex=${this._isSameDay(day.date, rovingDate) ? 0 : -1}
@@ -807,11 +857,19 @@ var Calendar = class extends ThemeableElement {
807
857
  render() {
808
858
  const today = /* @__PURE__ */ new Date();
809
859
  const isMulti = this.months > 1;
860
+ const isInView = (date) => {
861
+ if (!isMulti) return date.getFullYear() === this._viewYear && date.getMonth() === this._viewMonth;
862
+ for (let i = 0; i < this.months; i++) {
863
+ const { year, month } = this._monthAtOffset(i);
864
+ if (date.getFullYear() === year && date.getMonth() === month) return true;
865
+ }
866
+ return false;
867
+ };
810
868
  const rovingAnchor = this._parseDate(this.mode === "range" ? this.valueStart : this.value);
811
- const rovingDate = this._focusedDate ?? rovingAnchor ?? (today.getFullYear() === this._viewYear && today.getMonth() === this._viewMonth ? today : null) ?? new Date(this._viewYear, this._viewMonth, 1);
869
+ const rovingDate = (this._focusedDate && isInView(this._focusedDate) ? this._focusedDate : null) ?? (rovingAnchor && isInView(rovingAnchor) ? rovingAnchor : null) ?? (today.getFullYear() === this._viewYear && today.getMonth() === this._viewMonth ? today : null) ?? new Date(this._viewYear, this._viewMonth, 1);
812
870
  const footer = html`
813
871
  <div class="footer">
814
- <button class="today-btn" ?disabled=${this.disabled} @click=${this._goToToday}>Today</button>
872
+ <button class="today-btn" ?disabled=${this.disabled} @click=${this._goToToday}>${this._labels.today}</button>
815
873
  </div>
816
874
  `;
817
875
  if (isMulti) {
@@ -824,7 +882,7 @@ var Calendar = class extends ThemeableElement {
824
882
  <div class="header">
825
883
  <button
826
884
  class="nav-btn"
827
- aria-label="Previous month"
885
+ aria-label=${this._labels.previousMonth}
828
886
  ?disabled=${this.disabled}
829
887
  @click=${() => this._navigate(-1)}
830
888
  >
@@ -839,7 +897,7 @@ var Calendar = class extends ThemeableElement {
839
897
  </div>
840
898
  <button
841
899
  class="nav-btn"
842
- aria-label="Next month"
900
+ aria-label=${this._labels.nextMonth}
843
901
  ?disabled=${this.disabled}
844
902
  @click=${() => this._navigate(1)}
845
903
  >
@@ -868,8 +926,9 @@ var Calendar = class extends ThemeableElement {
868
926
  const monthLabel = new Intl.DateTimeFormat(this.locale, { month: "long", year: "numeric" }).format(
869
927
  new Date(this._viewYear, this._viewMonth)
870
928
  );
871
- const prevLabel = this._pickingMonthYear ? "Previous year" : "Previous month";
872
- const nextLabel = this._pickingMonthYear ? "Next year" : "Next month";
929
+ const prevLabel = this._pickingMonthYear ? this._labels.previousYear : this._labels.previousMonth;
930
+ const nextLabel = this._pickingMonthYear ? this._labels.nextYear : this._labels.nextMonth;
931
+ const monthYearVisibleText = this._pickingMonthYear ? String(this._pickerYear) : monthLabel;
873
932
  const header = html`
874
933
  <div class="header">
875
934
  <button class="nav-btn" aria-label=${prevLabel} ?disabled=${this.disabled} @click=${() => this._navigate(-1)}>
@@ -878,11 +937,11 @@ var Calendar = class extends ThemeableElement {
878
937
  <button
879
938
  class="month-label-btn ${this._pickingMonthYear ? "month-label-btn--open" : ""}"
880
939
  aria-expanded=${this._pickingMonthYear ? "true" : "false"}
881
- aria-label="Choose month and year"
940
+ aria-label=${this._labels.chooseMonthYear(monthYearVisibleText)}
882
941
  ?disabled=${this.disabled}
883
942
  @click=${() => this._pickingMonthYear ? this._closePicker() : this._openPicker()}
884
943
  >
885
- <span aria-live="polite" aria-atomic="true">${this._pickingMonthYear ? this._pickerYear : monthLabel}</span>
944
+ <span aria-live="polite" aria-atomic="true">${monthYearVisibleText}</span>
886
945
  <lt-icon name="arrow-down" size="sm" class="picker-chevron"></lt-icon>
887
946
  </button>
888
947
  <button class="nav-btn" aria-label=${nextLabel} ?disabled=${this.disabled} @click=${() => this._navigate(1)}>
@@ -948,6 +1007,12 @@ __decorateClass([
948
1007
  __decorateClass([
949
1008
  property2({ type: Number, reflect: true })
950
1009
  ], Calendar.prototype, "months", 2);
1010
+ __decorateClass([
1011
+ property2({ attribute: false })
1012
+ ], Calendar.prototype, "disabledDates", 2);
1013
+ __decorateClass([
1014
+ property2({ type: Object })
1015
+ ], Calendar.prototype, "labels", 2);
951
1016
  __decorateClass([
952
1017
  state()
953
1018
  ], Calendar.prototype, "_viewYear", 2);
@@ -1139,6 +1204,20 @@ var textStyles = css2`
1139
1204
  `;
1140
1205
 
1141
1206
  // src/components/text/text.ts
1207
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
1208
+ "h1",
1209
+ "h2",
1210
+ "h3",
1211
+ "h4",
1212
+ "h5",
1213
+ "h6",
1214
+ "p",
1215
+ "span",
1216
+ "div",
1217
+ "label",
1218
+ "strong",
1219
+ "em"
1220
+ ]);
1142
1221
  var VARIANT_TAG = {
1143
1222
  "display-2xl": "p",
1144
1223
  "display-xl": "p",
@@ -1163,7 +1242,8 @@ var Text = class extends ThemeableElement {
1163
1242
  this.as = "";
1164
1243
  }
1165
1244
  render() {
1166
- const tag = unsafeStatic(this.as || VARIANT_TAG[this.variant] || "p");
1245
+ const requested = this.as && ALLOWED_TAGS.has(this.as) ? this.as : "";
1246
+ const tag = unsafeStatic(requested || VARIANT_TAG[this.variant] || "p");
1167
1247
  return html2`<${tag} part="base"><slot></slot></${tag}>`;
1168
1248
  }
1169
1249
  };
@@ -2588,8 +2668,8 @@ var dateInputStyles = [
2588
2668
  }
2589
2669
 
2590
2670
  .required-indicator {
2591
- color: var(--lt-color-error-500);
2592
- margin-left: 2px;
2671
+ color: var(--lt-text-error);
2672
+ margin-inline-start: 2px;
2593
2673
  }
2594
2674
 
2595
2675
  /* ── Field ── */
@@ -2609,7 +2689,7 @@ var dateInputStyles = [
2609
2689
  font-family: inherit;
2610
2690
  justify-content: space-between;
2611
2691
  outline: none;
2612
- text-align: left;
2692
+ text-align: start;
2613
2693
  transition:
2614
2694
  border-color 150ms ease,
2615
2695
  box-shadow 150ms ease;
@@ -2642,7 +2722,7 @@ var dateInputStyles = [
2642
2722
  color: var(--lt-text-subtle);
2643
2723
  flex-shrink: 0;
2644
2724
  line-height: 0;
2645
- margin-left: var(--lt-spacing-2);
2725
+ margin-inline-start: var(--lt-spacing-2);
2646
2726
  }
2647
2727
 
2648
2728
  /* ── Sizes ── */
@@ -2685,7 +2765,7 @@ var dateInputStyles = [
2685
2765
  }
2686
2766
 
2687
2767
  :host([variant='error']) .field-btn {
2688
- border-color: var(--lt-color-error-500);
2768
+ border-color: var(--lt-text-error);
2689
2769
  }
2690
2770
 
2691
2771
  :host([variant='error']) .field-btn:focus-visible {
@@ -2708,29 +2788,87 @@ var dateInputStyles = [
2708
2788
  }
2709
2789
 
2710
2790
  :host([variant='error']) .helper-text {
2711
- color: var(--lt-color-error-600);
2791
+ color: var(--lt-text-error);
2712
2792
  }
2713
2793
 
2714
2794
  :host([variant='success']) .helper-text {
2715
- color: var(--lt-color-success-600);
2795
+ color: var(--lt-text-success);
2716
2796
  }
2717
2797
 
2718
2798
  :host([variant='warning']) .helper-text {
2719
- color: var(--lt-color-warning-700);
2799
+ color: var(--lt-text-warning);
2800
+ }
2801
+
2802
+ /* ── Reduced motion ───────────────────────────────────────────────────── */
2803
+
2804
+ @media (prefers-reduced-motion: reduce) {
2805
+ .field-btn {
2806
+ transition: none;
2807
+ }
2720
2808
  }
2721
2809
  `
2722
2810
  ];
2723
2811
 
2812
+ // src/components/shared/form-default.ts
2813
+ var FormDefaultController = class {
2814
+ constructor(host, get) {
2815
+ this._captured = false;
2816
+ this._get = get;
2817
+ host.addController(this);
2818
+ }
2819
+ hostConnected() {
2820
+ if (this._captured) return;
2821
+ this._captured = true;
2822
+ this.value = this._get();
2823
+ }
2824
+ };
2825
+
2826
+ // src/components/shared/form-disabled.ts
2827
+ var FormDisabledController = class {
2828
+ constructor(host) {
2829
+ this._fieldsetDisabled = false;
2830
+ this._host = host;
2831
+ host.addController(this);
2832
+ }
2833
+ // No-op — this controller has no connect-time work of its own (formDisabledCallback drives it instead), but
2834
+ // ReactiveController's members are all optional, and a class satisfying an all-optional interface with no
2835
+ // members in common is a TS error (TS2559); this gives it one.
2836
+ hostConnected() {
2837
+ }
2838
+ /** Whether an ancestor `<fieldset>` is currently disabled. */
2839
+ get fieldsetDisabled() {
2840
+ return this._fieldsetDisabled;
2841
+ }
2842
+ setFieldsetDisabled(disabled) {
2843
+ if (this._fieldsetDisabled === disabled) return;
2844
+ this._fieldsetDisabled = disabled;
2845
+ this._host.requestUpdate();
2846
+ }
2847
+ };
2848
+
2724
2849
  // src/components/date-input/date-input.ts
2850
+ var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
2851
+ var DEFAULT_LABELS2 = {
2852
+ validationMessage: "Please select a date",
2853
+ datePicker: "Date picker"
2854
+ };
2855
+ var parseIsoDate = (value) => {
2856
+ const match = ISO_DATE.exec(value);
2857
+ if (!match) return null;
2858
+ const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
2859
+ const date = new Date(year, month - 1, day);
2860
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day ? date : null;
2861
+ };
2725
2862
  var DateInput = class extends ThemeableElement {
2726
2863
  constructor() {
2727
2864
  super();
2865
+ this._default = new FormDefaultController(this, () => this.value);
2866
+ this._fieldsetDisabled = new FormDisabledController(this);
2728
2867
  this.value = "";
2729
2868
  this.min = "";
2730
2869
  this.max = "";
2731
2870
  this.locale = "en-US";
2732
2871
  this.format = "medium";
2733
- this.weekStart = "0";
2734
2872
  this.label = "";
2735
2873
  this.placeholder = "Select a date";
2736
2874
  this.helperText = "";
@@ -2739,21 +2877,28 @@ var DateInput = class extends ThemeableElement {
2739
2877
  this.disabled = false;
2740
2878
  this.required = false;
2741
2879
  this.name = "";
2742
- /** Individual dates to disable in the calendar, as Date objects. */
2743
2880
  this.disabledDates = [];
2881
+ this.labels = {};
2744
2882
  this._open = false;
2745
2883
  this._cleanupClickOutside = null;
2746
2884
  this._floatingCleanup = null;
2885
+ this._fieldIds = createFieldIds();
2747
2886
  this._internals = this.attachInternals();
2748
2887
  }
2888
+ get _labels() {
2889
+ return { ...DEFAULT_LABELS2, ...this.labels };
2890
+ }
2749
2891
  get _displayValue() {
2750
- if (!this.value) return "";
2751
- const [y, m, d] = this.value.split("-").map(Number);
2752
- const date = new Date(y, m - 1, d);
2753
- return new Intl.DateTimeFormat(this.locale, { dateStyle: this.format }).format(date);
2892
+ const date = parseIsoDate(this.value);
2893
+ if (!date) return "";
2894
+ try {
2895
+ return new Intl.DateTimeFormat(this.locale, { dateStyle: this.format }).format(date);
2896
+ } catch {
2897
+ return new Intl.DateTimeFormat(void 0, { dateStyle: this.format }).format(date);
2898
+ }
2754
2899
  }
2755
2900
  async _openDropdown() {
2756
- if (this.disabled || this._open) return;
2901
+ if (this._isDisabled || this._open) return;
2757
2902
  this._open = true;
2758
2903
  await this.updateComplete;
2759
2904
  const btn = this.shadowRoot?.querySelector("#field-btn");
@@ -2790,22 +2935,33 @@ var DateInput = class extends ThemeableElement {
2790
2935
  }
2791
2936
  _handleDropdownKeyDown(e) {
2792
2937
  if (e.key === "Escape") {
2938
+ e.preventDefault();
2793
2939
  e.stopPropagation();
2794
2940
  this._closeDropdown();
2795
2941
  this.shadowRoot?.querySelector(".field-btn")?.focus();
2796
2942
  }
2797
2943
  }
2798
- updated() {
2944
+ updated(changed) {
2945
+ if (changed.has("value") && this.value && !parseIsoDate(this.value)) {
2946
+ devWarn("lt-date-input", `value "${this.value}" isn't a plain YYYY-MM-DD date \u2014 showing the placeholder instead`);
2947
+ }
2799
2948
  this._internals.setFormValue(this.value || null);
2800
2949
  const btn = this.shadowRoot?.querySelector("#field-btn");
2801
2950
  if (this.required && !this.value && btn) {
2802
- this._internals.setValidity({ valueMissing: true }, "Please select a date", btn);
2951
+ this._internals.setValidity({ valueMissing: true }, this._labels.validationMessage, btn);
2803
2952
  } else {
2804
2953
  this._internals.setValidity({});
2805
2954
  }
2806
2955
  }
2807
2956
  formResetCallback() {
2808
- this.value = "";
2957
+ this.value = this._default.value;
2958
+ }
2959
+ formDisabledCallback(disabled) {
2960
+ this._fieldsetDisabled.setFieldsetDisabled(disabled);
2961
+ }
2962
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
2963
+ get _isDisabled() {
2964
+ return this.disabled || this._fieldsetDisabled.fieldsetDisabled;
2809
2965
  }
2810
2966
  checkValidity() {
2811
2967
  return this._internals.checkValidity();
@@ -2820,8 +2976,8 @@ var DateInput = class extends ThemeableElement {
2820
2976
  <div class="wrapper">
2821
2977
  ${this.label ? html3`<label for="field-btn">
2822
2978
  <lt-text variant="label" as="span">${this.label}</lt-text>
2823
- ${this.required ? html3`<span class="required-indicator" aria-hidden="true">*</span>` : nothing}
2824
- </label>` : nothing}
2979
+ ${this.required ? html3`<span class="required-indicator" aria-hidden="true">*</span>` : nothing2}
2980
+ </label>` : nothing2}
2825
2981
 
2826
2982
  <div class="field">
2827
2983
  <button
@@ -2829,8 +2985,9 @@ var DateInput = class extends ThemeableElement {
2829
2985
  type="button"
2830
2986
  class="field-btn"
2831
2987
  aria-haspopup="dialog"
2988
+ aria-describedby=${this.helperText ? this._fieldIds.helper : nothing2}
2832
2989
  aria-expanded=${this._open ? "true" : "false"}
2833
- ?disabled=${this.disabled}
2990
+ ?disabled=${this._isDisabled}
2834
2991
  ?required=${this.required}
2835
2992
  @click=${this._openDropdown}
2836
2993
  >
@@ -2846,7 +3003,7 @@ var DateInput = class extends ThemeableElement {
2846
3003
  class="dropdown"
2847
3004
  popover="manual"
2848
3005
  role="dialog"
2849
- aria-label="Date picker"
3006
+ aria-label=${this._labels.datePicker}
2850
3007
  @keydown=${this._handleDropdownKeyDown}
2851
3008
  >
2852
3009
  ${this._open ? html3`<lt-calendar
@@ -2854,14 +3011,14 @@ var DateInput = class extends ThemeableElement {
2854
3011
  min=${this.min}
2855
3012
  max=${this.max}
2856
3013
  locale=${this.locale}
2857
- week-start=${this.weekStart}
3014
+ week-start=${ifDefined(this.weekStart)}
2858
3015
  .disabledDates=${this.disabledDates}
2859
3016
  @change=${this._handleCalendarChange}
2860
- ></lt-calendar>` : nothing}
3017
+ ></lt-calendar>` : nothing2}
2861
3018
  </div>
2862
3019
  </div>
2863
3020
 
2864
- ${this.helperText ? html3`<span class="helper-text">${this.helperText}</span>` : nothing}
3021
+ ${this.helperText ? html3`<span id=${this._fieldIds.helper} class="helper-text">${this.helperText}</span>` : nothing2}
2865
3022
  </div>
2866
3023
  `;
2867
3024
  }
@@ -2869,7 +3026,7 @@ var DateInput = class extends ThemeableElement {
2869
3026
  DateInput.styles = dateInputStyles;
2870
3027
  DateInput.formAssociated = true;
2871
3028
  __decorateClass([
2872
- property4({ reflect: true })
3029
+ property4()
2873
3030
  ], DateInput.prototype, "value", 2);
2874
3031
  __decorateClass([
2875
3032
  property4({ reflect: true })
@@ -2910,6 +3067,12 @@ __decorateClass([
2910
3067
  __decorateClass([
2911
3068
  property4({ reflect: true })
2912
3069
  ], DateInput.prototype, "name", 2);
3070
+ __decorateClass([
3071
+ property4({ attribute: false })
3072
+ ], DateInput.prototype, "disabledDates", 2);
3073
+ __decorateClass([
3074
+ property4({ type: Object })
3075
+ ], DateInput.prototype, "labels", 2);
2913
3076
  __decorateClass([
2914
3077
  state2()
2915
3078
  ], DateInput.prototype, "_open", 2);
@@ -24,10 +24,14 @@ export declare class Datepicker extends ThemeableElement {
24
24
  static styles: import("lit").CSSResult;
25
25
  static formAssociated: boolean;
26
26
  private _internals;
27
+ private _default;
28
+ private _fieldsetDisabled;
27
29
  constructor();
28
30
  /** Native input type. */
29
31
  type: DatepickerType;
30
- /** Current value in the format required by the native input (`YYYY-MM-DD`, `HH:MM`, or `YYYY-MM-DDTHH:MM`). */
32
+ /** Current value in the format required by the native input (`YYYY-MM-DD`, `HH:MM`, or `YYYY-MM-DDTHH:MM`).
33
+ * Not reflected as an attribute — read the property, or the `change` event's `detail.value`, instead of
34
+ * `getAttribute('value')`/`[value]`. */
31
35
  value: string;
32
36
  /** Minimum allowed value. */
33
37
  min: string;
@@ -37,6 +41,7 @@ export declare class Datepicker extends ThemeableElement {
37
41
  label: string;
38
42
  /** Helper or error text displayed below the input. */
39
43
  helperText: string;
44
+ private readonly _fieldIds;
40
45
  /** Visual state variant. */
41
46
  variant: DatepickerVariant;
42
47
  /** Visual size. */
@@ -52,6 +57,9 @@ export declare class Datepicker extends ThemeableElement {
52
57
  private input;
53
58
  protected updated(changed: PropertyValues): void;
54
59
  formResetCallback(): void;
60
+ formDisabledCallback(disabled: boolean): void;
61
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
62
+ private get _isDisabled();
55
63
  checkValidity(): boolean;
56
64
  reportValidity(): boolean;
57
65
  private handleChange;