@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
@@ -35,7 +35,7 @@ __decorateClass([
35
35
  ], ThemeableElement.prototype, "theme", 2);
36
36
 
37
37
  // src/components/slider/slider.ts
38
- import { customElement as customElement3, property as property4, query as query2 } from "lit/decorators.js";
38
+ import { customElement as customElement3, property as property4 } from "lit/decorators.js";
39
39
 
40
40
  // src/components/slider/slider.styles.ts
41
41
  import { css } from "lit";
@@ -65,17 +65,20 @@ var sliderStyles = css`
65
65
  width: 100%;
66
66
  }
67
67
 
68
- /* Position the tooltip above the thumb rather than the track center */
68
+ /* Position the tooltip above the thumb rather than the track center. --_fill is a unitless 0–1 ratio (set from
69
+ JS on :host) — computed in CSS, not from a JS-read offsetWidth, so it can never drift after a resize or when
70
+ first shown from display:none (SLD-1). The thumb travels only (100% - thumb size), offset by half the thumb
71
+ so it's centered on the value, matching a native range input's own thumb geometry. */
69
72
  lt-tooltip::part(tooltip) {
70
- left: var(--_thumb-left, 50%);
73
+ left: calc(var(--_fill, 0) * (100% - var(--lt-slider-thumb-size, 18px)) + var(--lt-slider-thumb-size, 18px) / 2);
71
74
  }
72
75
 
73
76
  input[type='range'] {
74
77
  appearance: none;
75
78
  background: linear-gradient(
76
79
  to right,
77
- var(--lt-interactive-primary-bg) var(--lt-slider-fill, 0%),
78
- var(--lt-border-strong) var(--lt-slider-fill, 0%)
80
+ var(--lt-interactive-primary-bg) calc(var(--_fill, 0) * 100%),
81
+ var(--lt-border-strong) calc(var(--_fill, 0) * 100%)
79
82
  );
80
83
  border-radius: 9999px;
81
84
  cursor: pointer;
@@ -162,17 +165,102 @@ var sliderStyles = css`
162
165
  --lt-slider-thumb-size: 22px;
163
166
  }
164
167
 
165
- /* Disabled */
166
- :host([disabled]) {
168
+ /* Disabled. :host(:disabled) — the browser matches this automatically for a form-associated element inside a
169
+ disabled <fieldset>, no JS required; :host([disabled]) covers the component's own explicit attribute. */
170
+ :host([disabled]),
171
+ :host(:disabled) {
167
172
  opacity: 0.5;
168
173
  }
169
174
 
170
- :host([disabled]) input[type='range'] {
175
+ :host([disabled]) input[type='range'],
176
+ :host(:disabled) input[type='range'] {
171
177
  cursor: not-allowed;
172
178
  pointer-events: none;
173
179
  }
180
+
181
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
182
+
183
+ @media (prefers-reduced-motion: reduce) {
184
+ input[type='range']::-webkit-slider-thumb,
185
+ input[type='range']::-moz-range-thumb {
186
+ transition: none;
187
+ }
188
+ }
189
+
190
+ /* ── Forced colors (Windows High Contrast) ─────────────────────────────────
191
+ Under forced-colors, the track's linear-gradient (WebKit/Chromium) and the Firefox ::-moz-range-track/
192
+ ::-moz-range-progress pair are all flattened to Canvas, with no border to fall back on — the whole track
193
+ disappears. Restoring a visible track boundary, plus a solid thumb fill, is enough: the thumb's own
194
+ position already conveys the value (the primary way a slider communicates it visually), matching how
195
+ native <input type="range"> itself renders under forced-colors in every engine — no attempt to also
196
+ recreate the fill-amount gradient with system colors, which native sliders don't do either. System color
197
+ keywords are exempt from the flattening and always resolve to the OS's own guaranteed-contrasting
198
+ palette. ─────────────────────────────────────────────────────────────────────────────────────────────── */
199
+
200
+ @media (forced-colors: active) {
201
+ input[type='range'] {
202
+ background: Canvas;
203
+ border: 1px solid ButtonText;
204
+ }
205
+
206
+ input[type='range']::-webkit-slider-thumb {
207
+ background: Highlight;
208
+ border-color: ButtonText;
209
+ }
210
+
211
+ input[type='range']::-moz-range-thumb {
212
+ background: Highlight;
213
+ border-color: ButtonText;
214
+ }
215
+
216
+ input[type='range']::-moz-range-track {
217
+ background: Canvas;
218
+ border: 1px solid ButtonText;
219
+ }
220
+
221
+ input[type='range']::-moz-range-progress {
222
+ background: Highlight;
223
+ }
224
+ }
174
225
  `;
175
226
 
227
+ // src/components/shared/form-default.ts
228
+ var FormDefaultController = class {
229
+ constructor(host, get) {
230
+ this._captured = false;
231
+ this._get = get;
232
+ host.addController(this);
233
+ }
234
+ hostConnected() {
235
+ if (this._captured) return;
236
+ this._captured = true;
237
+ this.value = this._get();
238
+ }
239
+ };
240
+
241
+ // src/components/shared/form-disabled.ts
242
+ var FormDisabledController = class {
243
+ constructor(host) {
244
+ this._fieldsetDisabled = false;
245
+ this._host = host;
246
+ host.addController(this);
247
+ }
248
+ // No-op — this controller has no connect-time work of its own (formDisabledCallback drives it instead), but
249
+ // ReactiveController's members are all optional, and a class satisfying an all-optional interface with no
250
+ // members in common is a TS error (TS2559); this gives it one.
251
+ hostConnected() {
252
+ }
253
+ /** Whether an ancestor `<fieldset>` is currently disabled. */
254
+ get fieldsetDisabled() {
255
+ return this._fieldsetDisabled;
256
+ }
257
+ setFieldsetDisabled(disabled) {
258
+ if (this._fieldsetDisabled === disabled) return;
259
+ this._fieldsetDisabled = disabled;
260
+ this._host.requestUpdate();
261
+ }
262
+ };
263
+
176
264
  // src/components/tooltip/tooltip.ts
177
265
  import { html as html2 } from "lit";
178
266
  import { customElement as customElement2, property as property3, query } from "lit/decorators.js";
@@ -187,20 +275,39 @@ var tooltipStyles = css2`
187
275
  --lt-tooltip-offset: 8px;
188
276
  }
189
277
 
278
+ /* The bubble is a "manual" popover so it renders in the top layer — above an open lt-dialog/lt-sidepanel
279
+ regardless of z-index, and never clipped by an overflow:hidden ancestor. Its position is set from JS (see
280
+ _reposition in tooltip.ts), since a top-layer element is positioned against the viewport, not the host.
281
+ display/overlay need allow-discrete so hidePopover() doesn't cut the exit fade short — same as lt-snackbar. */
190
282
  .tooltip {
191
283
  background: var(--lt-tooltip-bg, var(--lt-bg-inverse));
284
+ border: 0;
192
285
  border-radius: 4px;
193
286
  color: var(--lt-tooltip-color, var(--lt-text-inverse));
287
+ inset: auto;
288
+ margin: 0;
194
289
  opacity: 0;
290
+ overflow: visible;
195
291
  padding: var(--lt-spacing-1) var(--lt-spacing-2);
196
292
  pointer-events: none;
197
- position: absolute;
293
+ position: fixed;
198
294
  transition:
199
295
  opacity 150ms ease,
200
- visibility 150ms ease;
201
- visibility: hidden;
296
+ display 150ms allow-discrete,
297
+ overlay 150ms allow-discrete;
202
298
  white-space: nowrap;
203
- z-index: 1000;
299
+ }
300
+
301
+ .tooltip:popover-open {
302
+ opacity: 1;
303
+ }
304
+
305
+ /* A newly-shown popover has no earlier rendered frame to transition from — @starting-style supplies one so the
306
+ fade-in actually animates instead of snapping straight to the open state. */
307
+ @starting-style {
308
+ .tooltip:popover-open {
309
+ opacity: 0;
310
+ }
204
311
  }
205
312
 
206
313
  .arrow {
@@ -210,78 +317,60 @@ var tooltipStyles = css2`
210
317
  width: 0;
211
318
  }
212
319
 
213
- :host(:hover) .tooltip,
214
- :host(:focus-within) .tooltip {
215
- opacity: 1;
216
- visibility: visible;
217
- }
218
-
219
320
  :host([disabled]) .tooltip,
220
321
  :host(:not([content])) .tooltip,
221
322
  :host([content='']) .tooltip {
222
323
  display: none;
223
324
  }
224
325
 
225
- /* ── Top (default) ──────────────────────────── */
326
+ /* ── Arrows ─────────────────────────────────────────────────────────────── The bubble's own position is set
327
+ from JS; each arrow sits on the bubble edge facing the host. Centering is direction-symmetric, so
328
+ inset-inline-start:50% + translateX(-50%) is correct in both LTR and RTL. Start/end use logical properties, so
329
+ they flip with the page direction without a custom-property trick. ─────────────────────────── */
226
330
 
227
- :host(:not([placement])) .tooltip,
228
- :host([placement='top']) .tooltip {
229
- bottom: calc(100% + var(--lt-tooltip-offset));
230
- left: 50%;
231
- transform: translateX(-50%);
232
- }
331
+ /* ── Top (default) ──────────────────────────── */
233
332
 
234
333
  :host(:not([placement])) .arrow,
235
334
  :host([placement='top']) .arrow {
236
335
  border-top-color: var(--lt-tooltip-bg, var(--lt-color-neutral-900));
237
- left: 50%;
336
+ inset-inline-start: 50%;
238
337
  top: 100%;
239
338
  transform: translateX(-50%);
240
339
  }
241
340
 
242
341
  /* ── Bottom ─────────────────────────────────── */
243
342
 
244
- :host([placement='bottom']) .tooltip {
245
- left: 50%;
246
- top: calc(100% + var(--lt-tooltip-offset));
247
- transform: translateX(-50%);
248
- }
249
-
250
343
  :host([placement='bottom']) .arrow {
251
344
  border-bottom-color: var(--lt-tooltip-bg, var(--lt-color-neutral-900));
252
345
  bottom: 100%;
253
- left: 50%;
346
+ inset-inline-start: 50%;
254
347
  transform: translateX(-50%);
255
348
  }
256
349
 
257
- /* ── Left ───────────────────────────────────── */
350
+ /* ── Start ──────────────────────────────────── */
258
351
 
259
- :host([placement='left']) .tooltip {
260
- right: calc(100% + var(--lt-tooltip-offset));
352
+ :host([placement='start']) .arrow {
353
+ border-inline-start-color: var(--lt-tooltip-bg, var(--lt-color-neutral-900));
354
+ inset-inline-start: 100%;
261
355
  top: 50%;
262
356
  transform: translateY(-50%);
263
357
  }
264
358
 
265
- :host([placement='left']) .arrow {
266
- border-left-color: var(--lt-tooltip-bg, var(--lt-color-neutral-900));
267
- left: 100%;
268
- top: 50%;
269
- transform: translateY(-50%);
270
- }
271
-
272
- /* ── Right ──────────────────────────────────── */
359
+ /* ── End ────────────────────────────────────── */
273
360
 
274
- :host([placement='right']) .tooltip {
275
- left: calc(100% + var(--lt-tooltip-offset));
361
+ :host([placement='end']) .arrow {
362
+ border-inline-end-color: var(--lt-tooltip-bg, var(--lt-color-neutral-900));
363
+ inset-inline-end: 100%;
276
364
  top: 50%;
277
365
  transform: translateY(-50%);
278
366
  }
279
367
 
280
- :host([placement='right']) .arrow {
281
- border-right-color: var(--lt-tooltip-bg, var(--lt-color-neutral-900));
282
- right: 100%;
283
- top: 50%;
284
- transform: translateY(-50%);
368
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
369
+
370
+ @media (prefers-reduced-motion: reduce) {
371
+ .tooltip {
372
+ transition: none;
373
+ }
285
374
  }
286
375
  `;
287
376
 
@@ -457,6 +546,20 @@ var textStyles = css3`
457
546
  `;
458
547
 
459
548
  // src/components/text/text.ts
549
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
550
+ "h1",
551
+ "h2",
552
+ "h3",
553
+ "h4",
554
+ "h5",
555
+ "h6",
556
+ "p",
557
+ "span",
558
+ "div",
559
+ "label",
560
+ "strong",
561
+ "em"
562
+ ]);
460
563
  var VARIANT_TAG = {
461
564
  "display-2xl": "p",
462
565
  "display-xl": "p",
@@ -481,7 +584,8 @@ var Text = class extends ThemeableElement {
481
584
  this.as = "";
482
585
  }
483
586
  render() {
484
- const tag = unsafeStatic(this.as || VARIANT_TAG[this.variant] || "p");
587
+ const requested = this.as && ALLOWED_TAGS.has(this.as) ? this.as : "";
588
+ const tag = unsafeStatic(requested || VARIANT_TAG[this.variant] || "p");
485
589
  return html`<${tag} part="base"><slot></slot></${tag}>`;
486
590
  }
487
591
  };
@@ -502,17 +606,117 @@ var Tooltip = class extends ThemeableElement {
502
606
  constructor() {
503
607
  super(...arguments);
504
608
  this._tooltipId = `lt-tooltip-${++_tooltipIdCounter}`;
609
+ this._hovered = false;
610
+ this._focused = false;
505
611
  this.content = "";
506
612
  this.placement = "top";
507
613
  this.disabled = false;
508
614
  this.background = "";
509
615
  this.color = "";
510
- }
511
- firstUpdated() {
616
+ // A top-layer popover is positioned against the viewport, not the host, so the bubble's fixed coordinates are
617
+ // computed here from the host's rect. Fixed coordinates need a physical side, so the logical start/end placements
618
+ // are resolved against the host's computed direction (start is the left side in LTR, the right side in RTL).
619
+ this._reposition = () => {
620
+ if (!this._isShown()) return;
621
+ const host = this.getBoundingClientRect();
622
+ const { width, height } = this._tooltipEl.getBoundingClientRect();
623
+ const style = getComputedStyle(this);
624
+ const offset = parseFloat(style.getPropertyValue("--lt-tooltip-offset")) || 8;
625
+ const rtl = style.direction === "rtl";
626
+ let left;
627
+ let top;
628
+ switch (this.placement) {
629
+ case "bottom":
630
+ left = host.left + host.width / 2 - width / 2;
631
+ top = host.bottom + offset;
632
+ break;
633
+ case "start":
634
+ case "end": {
635
+ const onLeft = this.placement === "start" !== rtl;
636
+ left = onLeft ? host.left - offset - width : host.right + offset;
637
+ top = host.top + host.height / 2 - height / 2;
638
+ break;
639
+ }
640
+ default:
641
+ left = host.left + host.width / 2 - width / 2;
642
+ top = host.top - offset - height;
643
+ }
644
+ this._tooltipEl.style.left = `${left}px`;
645
+ this._tooltipEl.style.top = `${top}px`;
646
+ };
647
+ this._onPointerEnter = () => {
648
+ this._hovered = true;
649
+ this._sync();
650
+ };
651
+ this._onPointerLeave = () => {
652
+ this._hovered = false;
653
+ this._sync();
654
+ };
655
+ this._onFocusIn = () => {
656
+ this._focused = true;
657
+ this._sync();
658
+ };
659
+ this._onFocusOut = (e) => {
660
+ if (this.contains(e.relatedTarget)) return;
661
+ this._focused = false;
662
+ this._sync();
663
+ };
664
+ }
665
+ // aria-describedby's id lookup only works within one tree: the tooltip bubble lives in this element's own
666
+ // shadow root, but the trigger the id is set on is the slotted (light DOM) element — an id reference can't
667
+ // cross that boundary, so no AT ever actually announces it (verified: resolves to nothing in the accessibility
668
+ // tree). aria-description carries the text itself, so there's no id, and nothing to fail to resolve.
669
+ _describeTrigger() {
512
670
  const trigger = this._slot?.assignedElements()[0];
513
- if (trigger) {
514
- trigger.setAttribute("aria-describedby", this._tooltipId);
671
+ trigger?.setAttribute("aria-description", this.content);
672
+ }
673
+ _isShown() {
674
+ return this._tooltipEl?.matches(":popover-open") ?? false;
675
+ }
676
+ _sync() {
677
+ const shouldShow = (this._hovered || this._focused) && !this.disabled && !!this.content;
678
+ if (shouldShow) this._show();
679
+ else this._hide();
680
+ }
681
+ _show() {
682
+ if (!this._isShown()) {
683
+ this._tooltipEl.showPopover();
684
+ window.addEventListener("scroll", this._reposition, true);
685
+ window.addEventListener("resize", this._reposition);
515
686
  }
687
+ this._reposition();
688
+ }
689
+ _hide() {
690
+ if (!this._isShown()) return;
691
+ this._tooltipEl.hidePopover();
692
+ window.removeEventListener("scroll", this._reposition, true);
693
+ window.removeEventListener("resize", this._reposition);
694
+ }
695
+ connectedCallback() {
696
+ super.connectedCallback();
697
+ this.addEventListener("pointerenter", this._onPointerEnter);
698
+ this.addEventListener("pointerleave", this._onPointerLeave);
699
+ this.addEventListener("focusin", this._onFocusIn);
700
+ this.addEventListener("focusout", this._onFocusOut);
701
+ }
702
+ disconnectedCallback() {
703
+ super.disconnectedCallback();
704
+ this.removeEventListener("pointerenter", this._onPointerEnter);
705
+ this.removeEventListener("pointerleave", this._onPointerLeave);
706
+ this.removeEventListener("focusin", this._onFocusIn);
707
+ this.removeEventListener("focusout", this._onFocusOut);
708
+ this._hovered = false;
709
+ this._focused = false;
710
+ this._hide();
711
+ }
712
+ firstUpdated() {
713
+ this._describeTrigger();
714
+ }
715
+ updated(changed) {
716
+ super.updated(changed);
717
+ if (changed.has("content")) this._describeTrigger();
718
+ if (changed.has("content") || changed.has("disabled")) this._sync();
719
+ else if (changed.has("placement")) this._reposition();
516
720
  }
517
721
  render() {
518
722
  const tooltipStyle = styleMap({
@@ -521,7 +725,15 @@ var Tooltip = class extends ThemeableElement {
521
725
  });
522
726
  return html2`
523
727
  <slot></slot>
524
- <div class="tooltip" id=${this._tooltipId} role="tooltip" part="tooltip" aria-hidden="true" style=${tooltipStyle}>
728
+ <div
729
+ class="tooltip"
730
+ id=${this._tooltipId}
731
+ popover="manual"
732
+ role="tooltip"
733
+ part="tooltip"
734
+ aria-hidden="true"
735
+ style=${tooltipStyle}
736
+ >
525
737
  <lt-text variant="caption" as="span">${this.content}</lt-text>
526
738
  <span class="arrow" part="arrow"></span>
527
739
  </div>
@@ -532,6 +744,9 @@ Tooltip.styles = tooltipStyles;
532
744
  __decorateClass([
533
745
  query("slot")
534
746
  ], Tooltip.prototype, "_slot", 2);
747
+ __decorateClass([
748
+ query(".tooltip")
749
+ ], Tooltip.prototype, "_tooltipEl", 2);
535
750
  __decorateClass([
536
751
  property3({ reflect: true })
537
752
  ], Tooltip.prototype, "content", 2);
@@ -555,6 +770,8 @@ Tooltip = __decorateClass([
555
770
  var Slider = class extends ThemeableElement {
556
771
  constructor() {
557
772
  super();
773
+ this._default = new FormDefaultController(this, () => this.value);
774
+ this._fieldsetDisabled = new FormDisabledController(this);
558
775
  this.size = "md";
559
776
  this.disabled = false;
560
777
  this.tooltip = false;
@@ -566,26 +783,44 @@ var Slider = class extends ThemeableElement {
566
783
  this.name = "";
567
784
  this._internals = this.attachInternals();
568
785
  }
569
- get _fillPercent() {
570
- return (this.value - this.min) / (this.max - this.min) * 100;
786
+ // A unitless 0–1 ratio, not a percentage — CSS derives both the track's fill percentage and the tooltip's
787
+ // thumb-following offset from it via calc() (SLD-1), so there's only one number to keep in sync.
788
+ get _fillRatio() {
789
+ if (this.max === this.min) return 0;
790
+ return (this.value - this.min) / (this.max - this.min);
791
+ }
792
+ /** Clamps to [min, max] and snaps to the nearest step from min, matching how a native <input type="range">
793
+ * sanitizes its own value — including when `value`/`min`/`max`/`step` are set programmatically, which a native
794
+ * range input clamps on its own internal value but this component didn't mirror onto its own `value` property
795
+ * (SLD-1: `value` wasn't clamped/stepped, so e.g. `value=150` with `max=100` produced a 150% fill and
796
+ * submitted "150" to the form even though the visible thumb was pinned at the end). */
797
+ _sanitize(value) {
798
+ if (this.min === this.max) return this.min;
799
+ const clamped = Math.min(this.max, Math.max(this.min, value));
800
+ if (this.step <= 0) return clamped;
801
+ const steps = Math.round((clamped - this.min) / this.step);
802
+ return Math.min(this.max, Math.max(this.min, this.min + steps * this.step));
571
803
  }
572
- _thumbSizePx() {
573
- return { sm: 14, md: 18, lg: 22 }[this.size] ?? 18;
804
+ willUpdate(changed) {
805
+ if (changed.has("value") || changed.has("min") || changed.has("max") || changed.has("step")) {
806
+ const sanitized = this._sanitize(this.value);
807
+ if (sanitized !== this.value) this.value = sanitized;
808
+ }
574
809
  }
575
810
  updated() {
576
- const fill = this._fillPercent;
577
- this.style.setProperty("--_fill", `${fill}%`);
578
- const trackW = this.input?.offsetWidth ?? 0;
579
- if (trackW) {
580
- const thumbPx = this._thumbSizePx();
581
- const centerPx = fill / 100 * (trackW - thumbPx) + thumbPx / 2;
582
- this.style.setProperty("--_thumb-left", `${centerPx / trackW * 100}%`);
583
- }
811
+ this.style.setProperty("--_fill", String(this._fillRatio));
584
812
  this._internals.setFormValue(String(this.value));
585
813
  this._internals.setValidity({});
586
814
  }
587
815
  formResetCallback() {
588
- this.value = this.min;
816
+ this.value = this._default.value;
817
+ }
818
+ formDisabledCallback(disabled) {
819
+ this._fieldsetDisabled.setFieldsetDisabled(disabled);
820
+ }
821
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
822
+ get _isDisabled() {
823
+ return this.disabled || this._fieldsetDisabled.fieldsetDisabled;
589
824
  }
590
825
  checkValidity() {
591
826
  return this._internals.checkValidity();
@@ -626,7 +861,7 @@ var Slider = class extends ThemeableElement {
626
861
  max=${this.max}
627
862
  step=${this.step}
628
863
  .value=${String(this.value)}
629
- ?disabled=${this.disabled}
864
+ ?disabled=${this._isDisabled}
630
865
  name=${this.name}
631
866
  aria-label=${this.label || "slider"}
632
867
  aria-valuemin=${this.min}
@@ -665,7 +900,7 @@ __decorateClass([
665
900
  property4({ type: Number })
666
901
  ], Slider.prototype, "step", 2);
667
902
  __decorateClass([
668
- property4({ type: Number, reflect: true })
903
+ property4({ type: Number })
669
904
  ], Slider.prototype, "value", 2);
670
905
  __decorateClass([
671
906
  property4({ reflect: true })
@@ -673,9 +908,6 @@ __decorateClass([
673
908
  __decorateClass([
674
909
  property4({ reflect: true })
675
910
  ], Slider.prototype, "name", 2);
676
- __decorateClass([
677
- query2("input")
678
- ], Slider.prototype, "input", 2);
679
911
  Slider = __decorateClass([
680
912
  customElement3("lt-slider")
681
913
  ], Slider);
@@ -1,3 +1,4 @@
1
+ import { type PropertyValues } from 'lit';
1
2
  import { ThemeableElement } from '../../base';
2
3
  import type { SliderSize } from './slider.types';
3
4
  import '../tooltip/tooltip';
@@ -13,6 +14,8 @@ export declare class Slider extends ThemeableElement {
13
14
  static styles: import("lit").CSSResult;
14
15
  static formAssociated: boolean;
15
16
  private _internals;
17
+ private _default;
18
+ private _fieldsetDisabled;
16
19
  constructor();
17
20
  size: SliderSize;
18
21
  disabled: boolean;
@@ -21,16 +24,26 @@ export declare class Slider extends ThemeableElement {
21
24
  min: number;
22
25
  max: number;
23
26
  step: number;
27
+ /** Not reflected as an attribute — read the property, or the `change` event's `detail.value`, instead of
28
+ * `getAttribute('value')`/`[value]`. */
24
29
  value: number;
25
30
  /** Label displayed above the track */
26
31
  label: string;
27
32
  /** Name attribute for form submission */
28
33
  name: string;
29
- private input;
30
- private get _fillPercent();
31
- private _thumbSizePx;
34
+ private get _fillRatio();
35
+ /** Clamps to [min, max] and snaps to the nearest step from min, matching how a native <input type="range">
36
+ * sanitizes its own value — including when `value`/`min`/`max`/`step` are set programmatically, which a native
37
+ * range input clamps on its own internal value but this component didn't mirror onto its own `value` property
38
+ * (SLD-1: `value` wasn't clamped/stepped, so e.g. `value=150` with `max=100` produced a 150% fill and
39
+ * submitted "150" to the form even though the visible thumb was pinned at the end). */
40
+ private _sanitize;
41
+ willUpdate(changed: PropertyValues): void;
32
42
  updated(): void;
33
43
  formResetCallback(): void;
44
+ formDisabledCallback(disabled: boolean): void;
45
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
46
+ private get _isDisabled();
34
47
  checkValidity(): boolean;
35
48
  reportValidity(): boolean;
36
49
  private _onInput;