@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
@@ -10,7 +10,7 @@ var __decorateClass = (decorators, target, key, kind) => {
10
10
  };
11
11
 
12
12
  // src/components/button/button.ts
13
- import { html as html2, nothing } from "lit";
13
+ import { html as html2, nothing, LitElement as LitElement2 } from "lit";
14
14
 
15
15
  // src/base/themeable-element.ts
16
16
  import { LitElement } from "lit";
@@ -37,6 +37,7 @@ __decorateClass([
37
37
  // src/components/button/button.ts
38
38
  import { customElement as customElement2, property as property3 } from "lit/decorators.js";
39
39
  import { ifDefined } from "lit/directives/if-defined.js";
40
+ import { classMap } from "lit/directives/class-map.js";
40
41
 
41
42
  // src/components/button/button.styles.ts
42
43
  import { css } from "lit";
@@ -77,6 +78,7 @@ var buttonStyles = css`
77
78
  justify-content: center;
78
79
  line-height: 1;
79
80
  min-width: var(--lt-button-min-width, 88px);
81
+ position: relative;
80
82
  user-select: none;
81
83
  transition:
82
84
  background-color 120ms ease,
@@ -84,6 +86,25 @@ var buttonStyles = css`
84
86
  transform 50ms ease;
85
87
  }
86
88
 
89
+ /* Wraps the icons/slot so it can be hidden (opacity, not display/visibility — that would drop it from the
90
+ accessibility tree too) while loading without collapsing the button's width or its accessible name. */
91
+ .label {
92
+ align-items: center;
93
+ display: inline-flex;
94
+ gap: inherit;
95
+ }
96
+
97
+ .label.is-loading {
98
+ opacity: 0;
99
+ }
100
+
101
+ /* Centered over the (invisible but still laid-out) label, not laid out alongside it. */
102
+ .spinner {
103
+ inset: 0;
104
+ margin: auto;
105
+ position: absolute;
106
+ }
107
+
87
108
  [part='base']:active {
88
109
  transform: translateY(1px);
89
110
  }
@@ -139,10 +160,10 @@ var buttonStyles = css`
139
160
  --_active-bg: var(--lt-interactive-secondary-bg-active);
140
161
  }
141
162
  :host([variant='neutral']) {
142
- --_bg: var(--lt-color-neutral-500);
163
+ --_bg: var(--lt-interactive-neutral-bg);
143
164
  --_color: var(--lt-color-neutral-50);
144
- --_hover-bg: var(--lt-color-neutral-700);
145
- --_active-bg: var(--lt-color-neutral-800);
165
+ --_hover-bg: var(--lt-interactive-neutral-bg-hover);
166
+ --_active-bg: var(--lt-interactive-neutral-bg-active);
146
167
  }
147
168
  :host([variant='success']) {
148
169
  --_bg: var(--lt-interactive-success-bg);
@@ -329,6 +350,17 @@ var buttonStyles = css`
329
350
  letter-spacing: 0.05rem;
330
351
  text-transform: lowercase;
331
352
  }
353
+
354
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
355
+
356
+ @media (prefers-reduced-motion: reduce) {
357
+ [part='base'] {
358
+ transition: none;
359
+ }
360
+ .spinner {
361
+ animation: none;
362
+ }
363
+ }
332
364
  `;
333
365
 
334
366
  // src/components/shared/aria-forwarding.ts
@@ -337,7 +369,8 @@ var FORWARDED_ARIA_ATTRIBUTES = [
337
369
  "aria-expanded",
338
370
  "aria-haspopup",
339
371
  "aria-controls",
340
- "aria-current"
372
+ "aria-current",
373
+ "aria-description"
341
374
  ];
342
375
  var AriaForwardingController = class {
343
376
  constructor(host, getTarget) {
@@ -364,6 +397,29 @@ var AriaForwardingController = class {
364
397
  }
365
398
  };
366
399
 
400
+ // src/components/shared/form-disabled.ts
401
+ var FormDisabledController = class {
402
+ constructor(host) {
403
+ this._fieldsetDisabled = false;
404
+ this._host = host;
405
+ host.addController(this);
406
+ }
407
+ // No-op — this controller has no connect-time work of its own (formDisabledCallback drives it instead), but
408
+ // ReactiveController's members are all optional, and a class satisfying an all-optional interface with no
409
+ // members in common is a TS error (TS2559); this gives it one.
410
+ hostConnected() {
411
+ }
412
+ /** Whether an ancestor `<fieldset>` is currently disabled. */
413
+ get fieldsetDisabled() {
414
+ return this._fieldsetDisabled;
415
+ }
416
+ setFieldsetDisabled(disabled) {
417
+ if (this._fieldsetDisabled === disabled) return;
418
+ this._fieldsetDisabled = disabled;
419
+ this._host.requestUpdate();
420
+ }
421
+ };
422
+
367
423
  // src/components/spinner/spinner.ts
368
424
  import { html } from "lit";
369
425
  import { customElement, property as property2 } from "lit/decorators.js";
@@ -464,6 +520,13 @@ var Button = class extends ThemeableElement {
464
520
  constructor() {
465
521
  super();
466
522
  this._ariaForwarding = new AriaForwardingController(this, () => this.shadowRoot?.querySelector('[part="base"]'));
523
+ this._fieldsetDisabled = new FormDisabledController(this);
524
+ this._blockClickWhileBusy = (e) => {
525
+ if (this.disabled || this.loading || this._fieldsetDisabled.fieldsetDisabled) {
526
+ e.preventDefault();
527
+ e.stopImmediatePropagation();
528
+ }
529
+ };
467
530
  this.variant = "primary";
468
531
  this.appearance = "filled";
469
532
  this.bordered = false;
@@ -478,22 +541,69 @@ var Button = class extends ThemeableElement {
478
541
  this.target = "";
479
542
  this.rel = "";
480
543
  this.type = "button";
544
+ this.name = "";
545
+ this.value = "";
546
+ this.formAction = "";
547
+ // Delegates the actual submission to a hidden native <button>, kept in sync and attached to the resolved form
548
+ // only for the duration of the submission (BTN-3). Two platform constraints make this necessary rather than
549
+ // calling form.requestSubmit() directly on the host or passing it as the submitter: a form-associated custom
550
+ // element can't itself be `requestSubmit()`'s submitter (Chromium: "the specified element is not a submit
551
+ // button"), and name/value/formaction submitter semantics only apply to the specific button that triggered the
552
+ // submission — setting this element's own form value unconditionally would submit name/value even when a
553
+ // *different* button (or an Enter keypress) triggered the submit. This is the same workaround other
554
+ // form-associated custom element implementations use for the same gap.
555
+ this._proxySubmitBtn = null;
481
556
  this._internals = this.attachInternals();
482
557
  }
558
+ connectedCallback() {
559
+ super.connectedCallback();
560
+ this.addEventListener("click", this._blockClickWhileBusy, { capture: true });
561
+ }
562
+ disconnectedCallback() {
563
+ super.disconnectedCallback();
564
+ this.removeEventListener("click", this._blockClickWhileBusy, { capture: true });
565
+ }
566
+ formDisabledCallback(disabled) {
567
+ this._fieldsetDisabled.setFieldsetDisabled(disabled);
568
+ }
569
+ _getProxySubmitBtn() {
570
+ if (!this._proxySubmitBtn) {
571
+ this._proxySubmitBtn = document.createElement("button");
572
+ this._proxySubmitBtn.type = "submit";
573
+ this._proxySubmitBtn.hidden = true;
574
+ }
575
+ return this._proxySubmitBtn;
576
+ }
483
577
  _handleClick() {
484
- if (this.disabled || this.loading) return;
485
- const form = this.closest("form");
578
+ if (this.disabled || this.loading || this._fieldsetDisabled.fieldsetDisabled) return;
579
+ const form = this._internals.form;
486
580
  if (!form) return;
487
- if (this.type === "submit") form.requestSubmit();
488
- else if (this.type === "reset") form.reset();
581
+ if (this.type === "reset") {
582
+ form.reset();
583
+ return;
584
+ }
585
+ if (this.type !== "submit") return;
586
+ const proxy = this._getProxySubmitBtn();
587
+ if (this.name) proxy.name = this.name;
588
+ else proxy.removeAttribute("name");
589
+ if (this.value) proxy.value = this.value;
590
+ else proxy.removeAttribute("value");
591
+ if (this.formAction) proxy.formAction = this.formAction;
592
+ else proxy.removeAttribute("formaction");
593
+ form.appendChild(proxy);
594
+ form.requestSubmit(proxy);
595
+ proxy.remove();
489
596
  }
490
597
  render() {
491
- const isDisabled = this.disabled || this.loading;
492
- const inner = this.loading ? html2`<lt-spinner></lt-spinner>` : html2`
493
- ${this.iconStart ? html2`<lt-icon class="icon-start" name=${this.iconStart}></lt-icon>` : ""}
494
- <slot></slot>
495
- ${this.iconEnd ? html2`<lt-icon class="icon-end" name=${this.iconEnd}></lt-icon>` : ""}
496
- `;
598
+ const isDisabled = this.disabled || this.loading || this._fieldsetDisabled.fieldsetDisabled;
599
+ const inner = html2`
600
+ <span class=${classMap({ label: true, "is-loading": this.loading })} part="label">
601
+ ${this.iconStart ? html2`<lt-icon class="icon-start" name=${this.iconStart}></lt-icon>` : ""}
602
+ <slot></slot>
603
+ ${this.iconEnd ? html2`<lt-icon class="icon-end" name=${this.iconEnd}></lt-icon>` : ""}
604
+ </span>
605
+ ${this.loading ? html2`<lt-spinner class="spinner" part="spinner"></lt-spinner>` : ""}
606
+ `;
497
607
  if (this.href) {
498
608
  const rel = this.rel || (this.target === "_blank" ? "noopener noreferrer" : void 0);
499
609
  return html2`
@@ -523,6 +633,10 @@ var Button = class extends ThemeableElement {
523
633
  };
524
634
  Button.styles = buttonStyles;
525
635
  Button.formAssociated = true;
636
+ // Makes host.focus() move focus into the shadow root (the inner <button>/<a>) instead of being a no-op —
637
+ // without it, activeElement never leaves <body>, silently breaking autofocus and any focus restore that calls
638
+ // .focus() on this element (a closed dialog/dropdown returning focus to its trigger, for example).
639
+ Button.shadowRootOptions = { ...LitElement2.shadowRootOptions, delegatesFocus: true };
526
640
  __decorateClass([
527
641
  property3({ reflect: true })
528
642
  ], Button.prototype, "variant", 2);
@@ -565,6 +679,15 @@ __decorateClass([
565
679
  __decorateClass([
566
680
  property3({ reflect: true })
567
681
  ], Button.prototype, "type", 2);
682
+ __decorateClass([
683
+ property3({ reflect: true })
684
+ ], Button.prototype, "name", 2);
685
+ __decorateClass([
686
+ property3({ reflect: true })
687
+ ], Button.prototype, "value", 2);
688
+ __decorateClass([
689
+ property3({ attribute: "formaction" })
690
+ ], Button.prototype, "formAction", 2);
568
691
  Button = __decorateClass([
569
692
  customElement2("lt-button")
570
693
  ], Button);
@@ -1,15 +1,15 @@
1
1
  import { type PropertyValues } from 'lit';
2
2
  import { ThemeableElement } from '../../base';
3
3
  import '@latty-ds/icons';
4
- import type { CalendarMode, CalendarWeekStart } from './calendar.types';
4
+ import type { CalendarMode, CalendarWeekStart, CalendarLabels } from './calendar.types';
5
5
  /**
6
6
  * A visual calendar grid for single-date or date-range selection.
7
7
  *
8
8
  * @element lt-calendar
9
9
  *
10
- * @fires {CustomEvent<{value: string}>} change - Fired when a single date is selected (mode="single").
11
- * @fires {CustomEvent<{valueStart: string, valueEnd: string}>} change - Fired when a full range is selected (mode="range").
12
- * @fires {CustomEvent<{year: number, month: number}>} month-change - Fired when the viewed month changes.
10
+ * @fires {CustomEvent<{value: string} | {valueStart: string, valueEnd: string}>} change - Fired when a date is selected
11
+ * (mode="single", detail `{ value }`) or when a full range is selected (mode="range", detail `{ valueStart, valueEnd }`).
12
+ * @fires {CustomEvent<{year: number, month: number}>} monthchange - Fired when the viewed month changes.
13
13
  *
14
14
  * @example
15
15
  * ```html
@@ -42,16 +42,24 @@ export declare class Calendar extends ThemeableElement {
42
42
  max: string;
43
43
  /** BCP 47 locale used for day and month names. */
44
44
  locale: string;
45
- /** First day of the week. 0 = Sunday, 1 = Monday. */
46
- weekStart: CalendarWeekStart;
45
+ /**
46
+ * First day of the week (0 = Sunday … 6 = Saturday). Defaults to whatever `locale` implies (via
47
+ * `Intl.Locale#getWeekInfo`, falling back to Sunday) — set this explicitly only to override that.
48
+ */
49
+ weekStart?: CalendarWeekStart;
50
+ /** `weekStart` if explicitly set, otherwise derived from `locale`. Internal code should always read this, never `weekStart` directly. */
51
+ private get _weekStart();
47
52
  /** Whether to render filler days from the previous and next month. */
48
53
  showOutsideDays: boolean;
49
54
  /** Disables all interaction. */
50
55
  disabled: boolean;
51
56
  /** Number of month panels to display side-by-side. */
52
57
  months: number;
53
- /** Specific dates to disable, as an array of Date objects. Not reflected as an attribute. */
58
+ /** Specific dates to disable, as an array of Date objects. Set as a JS property (`.disabledDates`). */
54
59
  disabledDates: Date[];
60
+ /** Overridable UI text (nav button labels, the "Today" footer button, the month/year picker toggle), for i18n. English defaults — override only the keys you need. */
61
+ labels: Partial<CalendarLabels>;
62
+ private get _labels();
55
63
  private _viewYear;
56
64
  private _viewMonth;
57
65
  private _focusedDate;
@@ -1,4 +1,5 @@
1
- export type CalendarWeekStart = '0' | '1';
1
+ /** First day of the week, matching `Date#getDay()`'s convention: 0 = Sunday, 1 = Monday, … 6 = Saturday. */
2
+ export type CalendarWeekStart = '0' | '1' | '2' | '3' | '4' | '5' | '6';
2
3
  export type CalendarMode = 'single' | 'range';
3
4
  export interface CalendarDay {
4
5
  date: Date;
@@ -13,3 +14,18 @@ export interface CalendarDay {
13
14
  isRangeHoverEnd: boolean;
14
15
  isInHoverRange: boolean;
15
16
  }
17
+ /** Overridable UI text for `lt-calendar`, for i18n. */
18
+ export interface CalendarLabels {
19
+ /** Text of the footer button that jumps the view back to today. */
20
+ today: string;
21
+ /** Accessible label for the "view previous month" button. */
22
+ previousMonth: string;
23
+ /** Accessible label for the "view next month" button. */
24
+ nextMonth: string;
25
+ /** Accessible label for the "view previous year" button, shown while picking a month/year (single-month layout only). */
26
+ previousYear: string;
27
+ /** Accessible label for the "view next year" button, shown while picking a month/year (single-month layout only). */
28
+ nextYear: string;
29
+ /** Accessible label for the month/year picker toggle button (single-month layout only), given its current visible text. */
30
+ chooseMonthYear: (monthYear: string) => string;
31
+ }
@@ -10,7 +10,7 @@ var __decorateClass = (decorators, target, key, kind) => {
10
10
  };
11
11
 
12
12
  // src/components/calendar/calendar.ts
13
- import { html } from "lit";
13
+ import { html, nothing } from "lit";
14
14
  import { customElement, property as property2, state } from "lit/decorators.js";
15
15
 
16
16
  // src/base/themeable-element.ts
@@ -184,7 +184,9 @@ var calendarStyles = css`
184
184
  width: 100%;
185
185
  }
186
186
 
187
- .day:disabled {
187
+ /* aria-disabled, not the disabled attribute — a disabled day stays focusable, so keyboard users can arrow
188
+ through it instead of the roving tabindex skipping straight over it (it just can't be selected). */
189
+ .day[aria-disabled='true'] {
188
190
  cursor: not-allowed;
189
191
  }
190
192
 
@@ -207,7 +209,7 @@ var calendarStyles = css`
207
209
  width: 2.25rem;
208
210
  }
209
211
 
210
- .day:hover:not(:disabled) .day__inner {
212
+ .day:hover:not([aria-disabled='true']) .day__inner {
211
213
  background: var(--lt-bg-neutral-subtle);
212
214
  }
213
215
 
@@ -230,14 +232,14 @@ var calendarStyles = css`
230
232
  font-weight: 600;
231
233
  }
232
234
 
233
- .day--selected:hover:not(:disabled) .day__inner {
235
+ .day--selected:hover:not([aria-disabled='true']) .day__inner {
234
236
  background: var(--lt-interactive-primary-bg-hover);
235
237
  }
236
238
 
237
- /* Outside days */
239
+ /* Outside days — dimmed by the muted text color alone. They're enabled, clickable buttons, and a 0.5
240
+ opacity on top of muted text took them to ~2:1 (WCAG AA needs 4.5:1). */
238
241
  .day--outside {
239
242
  color: var(--lt-text-muted);
240
- opacity: 0.5;
241
243
  }
242
244
 
243
245
  /* Empty filler cell */
@@ -248,7 +250,7 @@ var calendarStyles = css`
248
250
  pointer-events: none;
249
251
  }
250
252
 
251
- .day:disabled .day__inner {
253
+ .day[aria-disabled='true'] .day__inner {
252
254
  color: var(--lt-text-disabled);
253
255
  opacity: 0.4;
254
256
  }
@@ -275,7 +277,7 @@ var calendarStyles = css`
275
277
  background: var(--lt-bg-primary-subtle);
276
278
  }
277
279
 
278
- .day--in-range:hover:not(:disabled) .day__inner {
280
+ .day--in-range:hover:not([aria-disabled='true']) .day__inner {
279
281
  background: var(--lt-bg-neutral-subtle);
280
282
  }
281
283
 
@@ -293,7 +295,7 @@ var calendarStyles = css`
293
295
  font-weight: 600;
294
296
  }
295
297
 
296
- .day--range-confirmed:hover:not(:disabled) .day__inner {
298
+ .day--range-confirmed:hover:not([aria-disabled='true']) .day__inner {
297
299
  background: var(--lt-interactive-primary-bg-hover);
298
300
  }
299
301
 
@@ -416,12 +418,42 @@ var calendarStyles = css`
416
418
  cursor: not-allowed;
417
419
  opacity: 0.5;
418
420
  }
421
+
422
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
423
+
424
+ @media (prefers-reduced-motion: reduce) {
425
+ .month-label-btn,
426
+ .picker-chevron,
427
+ .nav-btn,
428
+ .day,
429
+ .day__inner,
430
+ .picker-month-btn,
431
+ .today-btn {
432
+ transition: none;
433
+ }
434
+ }
419
435
  `;
420
436
 
421
437
  // src/utils/dispatch.ts
422
438
  var dispatch = (host, name, detail, init) => host.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true, ...init }));
423
439
 
424
440
  // src/components/calendar/calendar.ts
441
+ var DEFAULT_LABELS = {
442
+ today: "Today",
443
+ previousMonth: "Previous month",
444
+ nextMonth: "Next month",
445
+ previousYear: "Previous year",
446
+ nextYear: "Next year",
447
+ chooseMonthYear: (monthYear) => `${monthYear}, choose month and year`
448
+ };
449
+ function deriveWeekStart(locale) {
450
+ try {
451
+ const info = new Intl.Locale(locale).getWeekInfo?.();
452
+ if (info) return String(info.firstDay % 7);
453
+ } catch {
454
+ }
455
+ return "0";
456
+ }
425
457
  var Calendar = class extends ThemeableElement {
426
458
  constructor() {
427
459
  super(...arguments);
@@ -432,12 +464,11 @@ var Calendar = class extends ThemeableElement {
432
464
  this.min = "";
433
465
  this.max = "";
434
466
  this.locale = "en-US";
435
- this.weekStart = "0";
436
467
  this.showOutsideDays = true;
437
468
  this.disabled = false;
438
469
  this.months = 1;
439
- /** Specific dates to disable, as an array of Date objects. Not reflected as an attribute. */
440
470
  this.disabledDates = [];
471
+ this.labels = {};
441
472
  this._viewYear = (/* @__PURE__ */ new Date()).getFullYear();
442
473
  this._viewMonth = (/* @__PURE__ */ new Date()).getMonth();
443
474
  this._focusedDate = null;
@@ -464,10 +495,10 @@ var Calendar = class extends ThemeableElement {
464
495
  if (this.mode === "range") this._hoverDate = null;
465
496
  };
466
497
  this._handleKeyDown = (e) => {
467
- const today = /* @__PURE__ */ new Date();
468
- const focusedDate = this._focusedDate ?? this._parseDate(this.value) ?? new Date(this._viewYear, this._viewMonth, today.getDate() || 1);
498
+ const currentIso = e.currentTarget.dataset.date ?? "";
499
+ const focusedDate = this._parseDate(currentIso) ?? new Date(this._viewYear, this._viewMonth, 1);
469
500
  const next = new Date(focusedDate);
470
- const weekStart = Number(this.weekStart);
501
+ const weekStart = Number(this._weekStart);
471
502
  switch (e.key) {
472
503
  case "ArrowRight":
473
504
  next.setDate(next.getDate() + 1);
@@ -519,6 +550,13 @@ var Calendar = class extends ThemeableElement {
519
550
  this._focusCell(this._toIso(next));
520
551
  };
521
552
  }
553
+ /** `weekStart` if explicitly set, otherwise derived from `locale`. Internal code should always read this, never `weekStart` directly. */
554
+ get _weekStart() {
555
+ return this.weekStart ?? deriveWeekStart(this.locale);
556
+ }
557
+ get _labels() {
558
+ return { ...DEFAULT_LABELS, ...this.labels };
559
+ }
522
560
  willUpdate(changed) {
523
561
  super.willUpdate(changed);
524
562
  if (changed.has("value") && this.value) {
@@ -577,7 +615,7 @@ var Calendar = class extends ThemeableElement {
577
615
  _buildDays(year = this._viewYear, month = this._viewMonth) {
578
616
  const today = /* @__PURE__ */ new Date();
579
617
  const selected = this._parseDate(this.value);
580
- const weekStart = Number(this.weekStart);
618
+ const weekStart = Number(this._weekStart);
581
619
  const rangeStart = this.mode === "range" ? this._parseDate(this.valueStart) : null;
582
620
  const rangeEnd = this.mode === "range" ? this._parseDate(this.valueEnd) : null;
583
621
  let visHoverStart = null;
@@ -630,7 +668,7 @@ var Calendar = class extends ThemeableElement {
630
668
  return days;
631
669
  }
632
670
  _getWeekdayLabels() {
633
- const weekStart = Number(this.weekStart);
671
+ const weekStart = Number(this._weekStart);
634
672
  const fmt = new Intl.DateTimeFormat(this.locale, { weekday: "narrow" });
635
673
  const refSunday = new Date(2026, 0, 4);
636
674
  return Array.from({ length: 7 }, (_, i) => {
@@ -657,7 +695,7 @@ var Calendar = class extends ThemeableElement {
657
695
  this._viewMonth = month;
658
696
  this._viewYear = year;
659
697
  this._focusedDate = null;
660
- dispatch(this, "month-change", { year: this._viewYear, month: this._viewMonth });
698
+ dispatch(this, "monthchange", { year: this._viewYear, month: this._viewMonth });
661
699
  }
662
700
  _openPicker() {
663
701
  this._pickerYear = this._viewYear;
@@ -671,7 +709,7 @@ var Calendar = class extends ThemeableElement {
671
709
  this._viewMonth = month;
672
710
  this._pickingMonthYear = false;
673
711
  this._focusedDate = null;
674
- dispatch(this, "month-change", { year: this._viewYear, month: this._viewMonth });
712
+ dispatch(this, "monthchange", { year: this._viewYear, month: this._viewMonth });
675
713
  }
676
714
  _selectDay(day) {
677
715
  if (day.isDisabled || this.disabled) return;
@@ -757,7 +795,7 @@ var Calendar = class extends ThemeableElement {
757
795
  role="gridcell"
758
796
  class=${classes}
759
797
  data-date=${iso}
760
- ?disabled=${day.isDisabled}
798
+ aria-disabled=${day.isDisabled ? "true" : nothing}
761
799
  aria-selected=${isSelected ? "true" : "false"}
762
800
  aria-label=${this._dayAriaLabel(day.date)}
763
801
  tabindex=${this._isSameDay(day.date, rovingDate) ? 0 : -1}
@@ -791,11 +829,19 @@ var Calendar = class extends ThemeableElement {
791
829
  render() {
792
830
  const today = /* @__PURE__ */ new Date();
793
831
  const isMulti = this.months > 1;
832
+ const isInView = (date) => {
833
+ if (!isMulti) return date.getFullYear() === this._viewYear && date.getMonth() === this._viewMonth;
834
+ for (let i = 0; i < this.months; i++) {
835
+ const { year, month } = this._monthAtOffset(i);
836
+ if (date.getFullYear() === year && date.getMonth() === month) return true;
837
+ }
838
+ return false;
839
+ };
794
840
  const rovingAnchor = this._parseDate(this.mode === "range" ? this.valueStart : this.value);
795
- const rovingDate = this._focusedDate ?? rovingAnchor ?? (today.getFullYear() === this._viewYear && today.getMonth() === this._viewMonth ? today : null) ?? new Date(this._viewYear, this._viewMonth, 1);
841
+ 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);
796
842
  const footer = html`
797
843
  <div class="footer">
798
- <button class="today-btn" ?disabled=${this.disabled} @click=${this._goToToday}>Today</button>
844
+ <button class="today-btn" ?disabled=${this.disabled} @click=${this._goToToday}>${this._labels.today}</button>
799
845
  </div>
800
846
  `;
801
847
  if (isMulti) {
@@ -808,7 +854,7 @@ var Calendar = class extends ThemeableElement {
808
854
  <div class="header">
809
855
  <button
810
856
  class="nav-btn"
811
- aria-label="Previous month"
857
+ aria-label=${this._labels.previousMonth}
812
858
  ?disabled=${this.disabled}
813
859
  @click=${() => this._navigate(-1)}
814
860
  >
@@ -823,7 +869,7 @@ var Calendar = class extends ThemeableElement {
823
869
  </div>
824
870
  <button
825
871
  class="nav-btn"
826
- aria-label="Next month"
872
+ aria-label=${this._labels.nextMonth}
827
873
  ?disabled=${this.disabled}
828
874
  @click=${() => this._navigate(1)}
829
875
  >
@@ -852,8 +898,9 @@ var Calendar = class extends ThemeableElement {
852
898
  const monthLabel = new Intl.DateTimeFormat(this.locale, { month: "long", year: "numeric" }).format(
853
899
  new Date(this._viewYear, this._viewMonth)
854
900
  );
855
- const prevLabel = this._pickingMonthYear ? "Previous year" : "Previous month";
856
- const nextLabel = this._pickingMonthYear ? "Next year" : "Next month";
901
+ const prevLabel = this._pickingMonthYear ? this._labels.previousYear : this._labels.previousMonth;
902
+ const nextLabel = this._pickingMonthYear ? this._labels.nextYear : this._labels.nextMonth;
903
+ const monthYearVisibleText = this._pickingMonthYear ? String(this._pickerYear) : monthLabel;
857
904
  const header = html`
858
905
  <div class="header">
859
906
  <button class="nav-btn" aria-label=${prevLabel} ?disabled=${this.disabled} @click=${() => this._navigate(-1)}>
@@ -862,11 +909,11 @@ var Calendar = class extends ThemeableElement {
862
909
  <button
863
910
  class="month-label-btn ${this._pickingMonthYear ? "month-label-btn--open" : ""}"
864
911
  aria-expanded=${this._pickingMonthYear ? "true" : "false"}
865
- aria-label="Choose month and year"
912
+ aria-label=${this._labels.chooseMonthYear(monthYearVisibleText)}
866
913
  ?disabled=${this.disabled}
867
914
  @click=${() => this._pickingMonthYear ? this._closePicker() : this._openPicker()}
868
915
  >
869
- <span aria-live="polite" aria-atomic="true">${this._pickingMonthYear ? this._pickerYear : monthLabel}</span>
916
+ <span aria-live="polite" aria-atomic="true">${monthYearVisibleText}</span>
870
917
  <lt-icon name="arrow-down" size="sm" class="picker-chevron"></lt-icon>
871
918
  </button>
872
919
  <button class="nav-btn" aria-label=${nextLabel} ?disabled=${this.disabled} @click=${() => this._navigate(1)}>
@@ -932,6 +979,12 @@ __decorateClass([
932
979
  __decorateClass([
933
980
  property2({ type: Number, reflect: true })
934
981
  ], Calendar.prototype, "months", 2);
982
+ __decorateClass([
983
+ property2({ attribute: false })
984
+ ], Calendar.prototype, "disabledDates", 2);
985
+ __decorateClass([
986
+ property2({ type: Object })
987
+ ], Calendar.prototype, "labels", 2);
935
988
  __decorateClass([
936
989
  state()
937
990
  ], Calendar.prototype, "_viewYear", 2);
@@ -1,5 +1,5 @@
1
1
  import { ThemeableElement } from '../../base';
2
- import { CheckboxSize, CheckboxVariant, CheckboxLabelPosition } from './checkbox.types';
2
+ import { CheckboxSize, CheckboxVariant, CheckboxLabelPosition, type CheckboxLabels } from './checkbox.types';
3
3
  import '@latty-ds/icons';
4
4
  /**
5
5
  * A customizable checkbox component with support for multiple variants and sizes.
@@ -35,6 +35,8 @@ export declare class Checkbox extends ThemeableElement {
35
35
  static styles: import("lit").CSSResult;
36
36
  static formAssociated: boolean;
37
37
  private _internals;
38
+ private _default;
39
+ private _fieldsetDisabled;
38
40
  constructor();
39
41
  /**
40
42
  * Visual variant that determines the color when checked.
@@ -47,7 +49,10 @@ export declare class Checkbox extends ThemeableElement {
47
49
  */
48
50
  size: CheckboxSize;
49
51
  /**
50
- * Whether the checkbox is checked.
52
+ * Whether the checkbox is checked. Not reflected as an attribute — a native checkbox's `checked` _content_
53
+ * attribute stays fixed at the markup default and never tracks interaction (that's what `defaultChecked` is
54
+ * for); reflecting it live meant `formResetCallback` could no longer tell what the original default was. Read
55
+ * the property, or the `change` event's `detail.checked`, instead of `[checked]`/`hasAttribute('checked')`.
51
56
  * @default false
52
57
  */
53
58
  checked: boolean;
@@ -87,6 +92,9 @@ export declare class Checkbox extends ThemeableElement {
87
92
  * @default 'on'
88
93
  */
89
94
  value: string;
95
+ /** Overridable UI text (the required-field validation message), for i18n. English defaults — override only the keys you need. */
96
+ labels: Partial<CheckboxLabels>;
97
+ private get _labels();
90
98
  /**
91
99
  * Reference to the native input element.
92
100
  * @private
@@ -94,6 +102,9 @@ export declare class Checkbox extends ThemeableElement {
94
102
  private input;
95
103
  updated(changedProperties: Map<string, unknown>): void;
96
104
  formResetCallback(): void;
105
+ formDisabledCallback(disabled: boolean): void;
106
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
107
+ private get _isDisabled();
97
108
  checkValidity(): boolean;
98
109
  reportValidity(): boolean;
99
110
  /**
@@ -111,5 +122,6 @@ export declare class Checkbox extends ThemeableElement {
111
122
  * @private
112
123
  */
113
124
  private renderCheckmark;
125
+ protected firstUpdated(): void;
114
126
  render(): import("lit").TemplateResult<1>;
115
127
  }
@@ -19,3 +19,8 @@ export type CheckboxSize = 'sm' | 'md' | 'lg';
19
19
  */
20
20
  export type CheckboxVariant = 'primary' | 'secondary' | 'success' | 'error' | 'info';
21
21
  export type CheckboxLabelPosition = 'left' | 'right';
22
+ /** Overridable UI text for `lt-checkbox`, for i18n. */
23
+ export interface CheckboxLabels {
24
+ /** Validation message shown when a required checkbox is left unchecked. */
25
+ validationMessage: string;
26
+ }