@truenas/ui-components 0.3.7 → 0.3.9

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.
@@ -1760,6 +1760,23 @@ class TnIconComponent {
1760
1760
  }
1761
1761
  return null;
1762
1762
  }, ...(ngDevMode ? [{ debugName: "hostDimension" }] : []));
1763
+ /**
1764
+ * Inline min-width/min-height override for the custom/full-size paths.
1765
+ * - customSize: pin the minimum to the custom value so flex layouts can't
1766
+ * squish it (mirrors the preset min-* rules in the stylesheet).
1767
+ * - fullSize: relax the minimum to 0 so the icon can shrink with its
1768
+ * container instead of being clamped by the `size` attribute's preset min.
1769
+ */
1770
+ hostMinDimension = computed(() => {
1771
+ const custom = this.customSize();
1772
+ if (custom) {
1773
+ return custom;
1774
+ }
1775
+ if (this.fullSize()) {
1776
+ return '0';
1777
+ }
1778
+ return null;
1779
+ }, ...(ngDevMode ? [{ debugName: "hostMinDimension" }] : []));
1763
1780
  hostFontSize = computed(() => {
1764
1781
  const custom = this.customSize();
1765
1782
  if (custom) {
@@ -1951,7 +1968,7 @@ class TnIconComponent {
1951
1968
  `To include it, add ${markerExample} to your source and run 'npx truenas-icons generate'.`);
1952
1969
  }
1953
1970
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1954
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnIconComponent, isStandalone: true, selector: "tn-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, library: { classPropertyName: "library", publicName: "library", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null }, customSize: { classPropertyName: "customSize", publicName: "customSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.name": "name()", "attr.library": "library()", "attr.size": "size()", "attr.color": "color()", "attr.full-size": "fullSize() || null", "attr.custom-size": "customSize() || null", "style.width": "hostDimension()", "style.height": "hostDimension()", "style.font-size": "hostFontSize()", "style.color": "color() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n tnTestIdType=\"icon\"\n [tnTestId]=\"testId()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: ["tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}\n"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1971
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnIconComponent, isStandalone: true, selector: "tn-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, library: { classPropertyName: "library", publicName: "library", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null }, customSize: { classPropertyName: "customSize", publicName: "customSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.name": "name()", "attr.library": "library()", "attr.size": "size()", "attr.color": "color()", "attr.full-size": "fullSize() || null", "attr.custom-size": "customSize() || null", "style.width": "hostDimension()", "style.height": "hostDimension()", "style.min-width": "hostMinDimension()", "style.min-height": "hostMinDimension()", "style.font-size": "hostFontSize()", "style.color": "color() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n tnTestIdType=\"icon\"\n [tnTestId]=\"testId()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: ["tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));min-width:var(--tn-icon-size, var(--tn-icon-md));min-height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);min-width:var(--tn-icon-xs);min-height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);min-width:var(--tn-icon-sm);min-height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);min-width:var(--tn-icon-md);min-height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);min-width:var(--tn-icon-lg);min-height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);min-width:var(--tn-icon-xl);min-height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}\n"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1955
1972
  }
1956
1973
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, decorators: [{
1957
1974
  type: Component,
@@ -1964,9 +1981,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1964
1981
  '[attr.custom-size]': 'customSize() || null',
1965
1982
  '[style.width]': 'hostDimension()',
1966
1983
  '[style.height]': 'hostDimension()',
1984
+ '[style.min-width]': 'hostMinDimension()',
1985
+ '[style.min-height]': 'hostMinDimension()',
1967
1986
  '[style.font-size]': 'hostFontSize()',
1968
1987
  '[style.color]': 'color() || null',
1969
- }, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n tnTestIdType=\"icon\"\n [tnTestId]=\"testId()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: ["tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}\n"] }]
1988
+ }, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n tnTestIdType=\"icon\"\n [tnTestId]=\"testId()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: ["tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));min-width:var(--tn-icon-size, var(--tn-icon-md));min-height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);min-width:var(--tn-icon-xs);min-height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);min-width:var(--tn-icon-sm);min-height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);min-width:var(--tn-icon-md);min-height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);min-width:var(--tn-icon-lg);min-height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);min-width:var(--tn-icon-xl);min-height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}\n"] }]
1970
1989
  }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], library: [{ type: i0.Input, args: [{ isSignal: true, alias: "library", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], fullSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullSize", required: false }] }], customSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "customSize", required: false }] }], svgContainer: [{ type: i0.ViewChild, args: ['svgContainer', { isSignal: true }] }] } });
1971
1990
 
1972
1991
  /**
@@ -13544,11 +13563,33 @@ class TnSliderThumbDirective {
13544
13563
  slider; // Will be set by parent slider component
13545
13564
  onTouched = () => { };
13546
13565
  onChangeCallback = (_value) => { };
13566
+ // Pointer is held down on the thumb (set on mousedown/touchstart). Drives the
13567
+ // global move/up listeners.
13568
+ isPointerDown = false;
13569
+ // The pointer has actually moved since going down — i.e. a genuine drag. Only
13570
+ // set once movement occurs (onGlobalMouseMove/onGlobalTouchMove), never on the
13571
+ // initial press, so a plain click still commits through onInput. See onInput.
13547
13572
  isDragging = false;
13573
+ // Last value written by the form. Retained so the parent slider can pick it up
13574
+ // once it links the thumb (ngAfterContentInit) — writeValue can run before the
13575
+ // slider sets `this.slider`.
13576
+ currentValue = 0;
13577
+ // Whether the form has actually written a value to this thumb. Lets the parent
13578
+ // slider distinguish "form wrote 0" from "thumb never bound" so it doesn't
13579
+ // clobber its own form value with this default. See getValue()/hasFormValue().
13580
+ hasWrittenValue = false;
13548
13581
  elementRef = inject((ElementRef));
13582
+ // An accessible name set directly on the <input tnSliderThumb> (e.g.
13583
+ // aria-label="Volume"). Captured up front so the host's [attr.aria-label]
13584
+ // binding can fall back to it instead of clobbering it when the parent slider
13585
+ // doesn't supply one. See ariaLabel()/ariaLabelledby().
13586
+ fallbackAriaLabel = null;
13587
+ fallbackAriaLabelledby = null;
13549
13588
  ngOnInit() {
13550
13589
  // Make the native input visually hidden but still accessible
13551
13590
  const input = this.elementRef.nativeElement;
13591
+ this.fallbackAriaLabel = input.getAttribute('aria-label');
13592
+ this.fallbackAriaLabelledby = input.getAttribute('aria-labelledby');
13552
13593
  input.style.opacity = '0';
13553
13594
  input.style.position = 'absolute';
13554
13595
  input.style.width = '100%';
@@ -13561,10 +13602,46 @@ class TnSliderThumbDirective {
13561
13602
  ngOnDestroy() {
13562
13603
  this.cleanup();
13563
13604
  }
13605
+ /** Value last written by the form, for the slider to read once linked. */
13606
+ getValue() {
13607
+ return this.currentValue;
13608
+ }
13609
+ /**
13610
+ * Whether the form has written a value to this thumb. The slider checks this
13611
+ * before adopting getValue() so an unbound thumb's default 0 never overwrites
13612
+ * a value bound directly on the slider.
13613
+ */
13614
+ hasFormValue() {
13615
+ return this.hasWrittenValue;
13616
+ }
13564
13617
  // ControlValueAccessor implementation
13565
13618
  writeValue(value) {
13566
- if (this.elementRef.nativeElement) {
13567
- this.elementRef.nativeElement.value = value?.toString() || '0';
13619
+ const nextValue = value ?? 0;
13620
+ this.hasWrittenValue = true;
13621
+ // Propagate to the parent slider so its value signal (and thus the thumb
13622
+ // position / track fill) reflects the form value, not just the native input.
13623
+ // When linked, mirror the slider's clamped/stepped result so currentValue and
13624
+ // the native input agree with it; otherwise retain the raw value for the
13625
+ // slider to adopt (and clamp) once it links the thumb in ngAfterContentInit.
13626
+ if (this.slider) {
13627
+ this.slider.updateValue(nextValue);
13628
+ // Note: we don't emit the clamped result back to the form control, so an
13629
+ // out-of-range write (e.g. setValue(143) on a 0–100 slider) leaves the model
13630
+ // holding 143 while the slider renders 100. This mirrors native
13631
+ // <input type="range">, which also doesn't reconcile the bound value.
13632
+ this.currentValue = this.slider.value();
13633
+ // When linked, the host `[value]="slider?.value()"` binding writes the
13634
+ // native input on the next change-detection pass, so a manual write here
13635
+ // would only be re-overwritten — leave it to the binding to keep one source
13636
+ // of truth.
13637
+ }
13638
+ else {
13639
+ this.currentValue = nextValue;
13640
+ // Unlinked: no host binding value yet (slider?.value() is undefined), so set
13641
+ // the native input directly until the slider links and takes over.
13642
+ if (this.elementRef.nativeElement) {
13643
+ this.elementRef.nativeElement.value = this.currentValue.toString();
13644
+ }
13568
13645
  }
13569
13646
  }
13570
13647
  registerOnChange(fn) {
@@ -13580,21 +13657,45 @@ class TnSliderThumbDirective {
13580
13657
  }
13581
13658
  }
13582
13659
  onInput(event) {
13660
+ // Once a drag is underway, the global pointer handlers
13661
+ // (updateValueFromPosition) own value commits; the native range also fires
13662
+ // input here, so skip it to avoid a redundant double emit. A plain click sets
13663
+ // isPointerDown but not isDragging (no movement), so its native input commits
13664
+ // here; keyboard changes set neither and also flow through.
13665
+ if (this.isDragging) {
13666
+ return;
13667
+ }
13583
13668
  const input = event.target;
13584
13669
  const value = parseFloat(input.value);
13670
+ // An empty/garbage value parses to NaN, which clampValue can't sanitize
13671
+ // (Math.max/min with NaN stay NaN). Native range inputs shouldn't produce
13672
+ // this, but guard so a NaN never reaches the form or slider value.
13673
+ if (!Number.isFinite(value)) {
13674
+ return;
13675
+ }
13585
13676
  if (this.slider) {
13586
13677
  this.slider.updateValue(value);
13587
13678
  }
13588
13679
  this.onChangeCallback(value);
13589
13680
  }
13590
13681
  onChange(_event) {
13682
+ this.notifyTouched();
13683
+ }
13684
+ /**
13685
+ * Marks the bound control touched. Calls the thumb's own `onTouched` (for a
13686
+ * thumb-bound control) and forwards to the linked slider (for a
13687
+ * slider-host-bound control) — the thumb is the only interactive element, so
13688
+ * the slider relies on it to ever become touched.
13689
+ */
13690
+ notifyTouched() {
13591
13691
  this.onTouched();
13692
+ this.slider?.markTouched();
13592
13693
  }
13593
13694
  onMouseDown(event) {
13594
13695
  if (this.disabled()) {
13595
13696
  return;
13596
13697
  }
13597
- this.isDragging = true;
13698
+ this.isPointerDown = true;
13598
13699
  this.addGlobalListeners();
13599
13700
  event.stopPropagation(); // Prevent track click
13600
13701
  }
@@ -13602,7 +13703,7 @@ class TnSliderThumbDirective {
13602
13703
  if (this.disabled()) {
13603
13704
  return;
13604
13705
  }
13605
- this.isDragging = true;
13706
+ this.isPointerDown = true;
13606
13707
  this.addGlobalListeners();
13607
13708
  event.stopPropagation(); // Prevent track click
13608
13709
  }
@@ -13619,31 +13720,35 @@ class TnSliderThumbDirective {
13619
13720
  document.removeEventListener('touchend', this.onGlobalTouchEnd);
13620
13721
  }
13621
13722
  onGlobalMouseMove = (event) => {
13622
- if (!this.isDragging || this.disabled()) {
13723
+ if (!this.isPointerDown || this.disabled()) {
13623
13724
  return;
13624
13725
  }
13726
+ this.isDragging = true;
13625
13727
  event.preventDefault();
13626
13728
  this.updateValueFromPosition(event.clientX);
13627
13729
  };
13628
13730
  onGlobalMouseUp = () => {
13629
- if (this.isDragging) {
13731
+ if (this.isPointerDown) {
13732
+ this.isPointerDown = false;
13630
13733
  this.isDragging = false;
13631
- this.onTouched();
13734
+ this.notifyTouched();
13632
13735
  this.removeGlobalListeners();
13633
13736
  }
13634
13737
  };
13635
13738
  onGlobalTouchMove = (event) => {
13636
- if (!this.isDragging || this.disabled()) {
13739
+ if (!this.isPointerDown || this.disabled()) {
13637
13740
  return;
13638
13741
  }
13742
+ this.isDragging = true;
13639
13743
  event.preventDefault();
13640
13744
  const touch = event.touches[0];
13641
13745
  this.updateValueFromPosition(touch.clientX);
13642
13746
  };
13643
13747
  onGlobalTouchEnd = () => {
13644
- if (this.isDragging) {
13748
+ if (this.isPointerDown) {
13749
+ this.isPointerDown = false;
13645
13750
  this.isDragging = false;
13646
- this.onTouched();
13751
+ this.notifyTouched();
13647
13752
  this.removeGlobalListeners();
13648
13753
  }
13649
13754
  };
@@ -13657,12 +13762,58 @@ class TnSliderThumbDirective {
13657
13762
  const maxVal = this.slider.max();
13658
13763
  const newValue = minVal + (percentage * (maxVal - minVal));
13659
13764
  this.slider.updateValue(newValue);
13765
+ // updateValue only moves the visual thumb; commit the (clamped/stepped) value
13766
+ // to the form and native input so dragging actually emits changes — otherwise
13767
+ // only click (native input event) updates the model.
13768
+ this.commit(this.slider.value());
13769
+ }
13770
+ /**
13771
+ * Resolve the accessible name for the range input: the parent slider's
13772
+ * `aria-label`/`aria-labelledby` input when set, otherwise a value placed
13773
+ * directly on the `<input tnSliderThumb>`. Returning the fallback keeps the
13774
+ * host binding from wiping a directly-set label. Null removes the attribute.
13775
+ */
13776
+ ariaLabel() {
13777
+ return this.slider?.ariaLabel() ?? this.fallbackAriaLabel;
13778
+ }
13779
+ ariaLabelledby() {
13780
+ return this.slider?.ariaLabelledby() ?? this.fallbackAriaLabelledby;
13781
+ }
13782
+ /**
13783
+ * Commit a value that originated outside the native input (a thumb drag).
13784
+ * Syncs `currentValue`, the native input, and emits to the form. The slider's
13785
+ * own `onChange` only reaches a slider-bound control, so a thumb-bound control
13786
+ * relies on this to stay in sync. Expects an already clamped/stepped value
13787
+ * (slider.value()).
13788
+ */
13789
+ commit(value) {
13790
+ this.currentValue = value;
13791
+ if (this.elementRef.nativeElement) {
13792
+ this.elementRef.nativeElement.value = value.toString();
13793
+ }
13794
+ this.onChangeCallback(value);
13795
+ }
13796
+ /**
13797
+ * Builds an aria-valuetext when the slider has a label prefix/suffix so screen
13798
+ * readers announce "50 km/h" rather than the bare number. Returns null when
13799
+ * neither is set, letting the native range's valuenow announcement stand.
13800
+ */
13801
+ ariaValueText() {
13802
+ if (!this.slider) {
13803
+ return null;
13804
+ }
13805
+ const prefix = this.slider.labelPrefix();
13806
+ const suffix = this.slider.labelSuffix();
13807
+ if (!prefix && !suffix) {
13808
+ return null;
13809
+ }
13810
+ return `${prefix}${this.slider.value()}${suffix}`;
13660
13811
  }
13661
13812
  cleanup() {
13662
13813
  this.removeGlobalListeners();
13663
13814
  }
13664
13815
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSliderThumbDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
13665
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnSliderThumbDirective, isStandalone: true, selector: "input[tnSliderThumb]", host: { attributes: { "type": "range" }, listeners: { "input": "onInput($event)", "change": "onChange($event)", "blur": "onTouched()", "mousedown": "onMouseDown($event)", "touchstart": "onTouchStart($event)" }, properties: { "disabled": "slider?.isDisabled()", "attr.min": "slider?.min()", "attr.max": "slider?.max()", "attr.step": "slider?.step()", "value": "slider?.value()" }, classAttribute: "tn-slider-thumb" }, providers: [
13816
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnSliderThumbDirective, isStandalone: true, selector: "input[tnSliderThumb]", host: { attributes: { "type": "range" }, listeners: { "input": "onInput($event)", "change": "onChange($event)", "blur": "notifyTouched()", "mousedown": "onMouseDown($event)", "touchstart": "onTouchStart($event)" }, properties: { "disabled": "slider?.isDisabled()", "attr.min": "slider?.min()", "attr.max": "slider?.max()", "attr.step": "slider?.step()", "value": "slider?.value()", "attr.aria-valuetext": "ariaValueText()", "attr.aria-label": "ariaLabel()", "attr.aria-labelledby": "ariaLabelledby()" }, classAttribute: "tn-slider-thumb" }, providers: [
13666
13817
  {
13667
13818
  provide: NG_VALUE_ACCESSOR,
13668
13819
  useExisting: forwardRef(() => TnSliderThumbDirective),
@@ -13690,15 +13841,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
13690
13841
  '[attr.max]': 'slider?.max()',
13691
13842
  '[attr.step]': 'slider?.step()',
13692
13843
  '[value]': 'slider?.value()',
13844
+ '[attr.aria-valuetext]': 'ariaValueText()',
13845
+ '[attr.aria-label]': 'ariaLabel()',
13846
+ '[attr.aria-labelledby]': 'ariaLabelledby()',
13693
13847
  '(input)': 'onInput($event)',
13694
13848
  '(change)': 'onChange($event)',
13695
- '(blur)': 'onTouched()',
13849
+ '(blur)': 'notifyTouched()',
13696
13850
  '(mousedown)': 'onMouseDown($event)',
13697
13851
  '(touchstart)': 'onTouchStart($event)'
13698
13852
  }
13699
13853
  }]
13700
13854
  }] });
13701
13855
 
13856
+ /**
13857
+ * Range slider with an optional value label.
13858
+ *
13859
+ * Form binding: both this component and the inner `input[tnSliderThumb]`
13860
+ * directive are `NG_VALUE_ACCESSOR` providers, so a `formControl`/`ngModel` can
13861
+ * be attached to either element. Bind to the `tn-slider` host for the simplest
13862
+ * usage; binding to the inner thumb input also works and the slider adopts that
13863
+ * value on init (see {@link ngAfterContentInit}). Avoid binding to both at once —
13864
+ * pick one element per control to keep a single source of truth.
13865
+ */
13702
13866
  class TnSliderComponent {
13703
13867
  min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : []));
13704
13868
  max = input(100, ...(ngDevMode ? [{ debugName: "max" }] : []));
@@ -13712,9 +13876,18 @@ class TnSliderComponent {
13712
13876
  * is configured via `TN_TEST_ATTR` (default `data-testid`).
13713
13877
  */
13714
13878
  testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
13879
+ /**
13880
+ * Accessible name forwarded to the inner range input — the focusable element
13881
+ * screen readers actually announce. Set this (or `aria-labelledby`) when the
13882
+ * slider isn't already labelled by a `tn-form-field`/`<label>`, otherwise a
13883
+ * standalone `<tn-slider><input tnSliderThumb></tn-slider>` announces only
13884
+ * "slider". A label set directly on the `input[tnSliderThumb]` is used as a
13885
+ * fallback when neither is provided here.
13886
+ */
13887
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : {}), alias: 'aria-label' });
13888
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : {}), alias: 'aria-labelledby' });
13715
13889
  thumbDirective = contentChild.required(TnSliderThumbDirective);
13716
13890
  sliderContainer = viewChild.required('sliderContainer');
13717
- thumbVisual = viewChild.required('thumbVisual');
13718
13891
  onChange = (_value) => { };
13719
13892
  onTouched = () => { };
13720
13893
  value = signal(0, ...(ngDevMode ? [{ debugName: "value" }] : []));
@@ -13735,13 +13908,6 @@ class TnSliderComponent {
13735
13908
  fillScale = computed(() => {
13736
13909
  return this.fillPercentage() / 100;
13737
13910
  }, ...(ngDevMode ? [{ debugName: "fillScale" }] : []));
13738
- // Computed position for thumb (in pixels from left)
13739
- thumbPosition = computed(() => {
13740
- const containerWidth = this.sliderContainer()?.nativeElement?.offsetWidth || 0;
13741
- const percentage = this.fillPercentage();
13742
- // Center the thumb (20px width, so -10px offset)
13743
- return (containerWidth * percentage / 100) - 10;
13744
- }, ...(ngDevMode ? [{ debugName: "thumbPosition" }] : []));
13745
13911
  // Public signals for label management
13746
13912
  showLabel = this._showLabel.asReadonly();
13747
13913
  labelVisible = this._labelVisible.asReadonly();
@@ -13763,13 +13929,24 @@ class TnSliderComponent {
13763
13929
  }
13764
13930
  });
13765
13931
  }
13766
- ngAfterViewInit() {
13767
- // Initialize thumb directive if present
13932
+ ngAfterContentInit() {
13933
+ // Link the projected thumb directive. Done in AfterContentInit (not
13934
+ // AfterViewInit) so the link exists before the thumb's host bindings settle,
13935
+ // avoiding a null→value flip on its [disabled]/[min]/[value] bindings.
13768
13936
  const thumbDirective = this.thumbDirective();
13769
13937
  if (thumbDirective) {
13770
13938
  thumbDirective.slider = this;
13939
+ // When the form is bound to the inner thumb input, its initial writeValue()
13940
+ // may run before this link exists, so adopt the value the thumb received —
13941
+ // otherwise the slider keeps its default 0 and the thumb/fill render at the
13942
+ // wrong position. Only adopt when the thumb was actually written to, so a
13943
+ // value bound directly on the slider isn't clobbered by the thumb's default.
13944
+ if (thumbDirective.hasFormValue()) {
13945
+ this.value.set(this.clampValue(thumbDirective.getValue()));
13946
+ }
13771
13947
  }
13772
- this.updateThumbPosition();
13948
+ }
13949
+ ngAfterViewInit() {
13773
13950
  // Set up handle interaction listeners if labelType is handle or both
13774
13951
  const currentLabelType = this.labelType();
13775
13952
  if ((currentLabelType === 'handle' || currentLabelType === 'both') && this._showLabel()) {
@@ -13791,6 +13968,15 @@ class TnSliderComponent {
13791
13968
  registerOnTouched(fn) {
13792
13969
  this.onTouched = fn;
13793
13970
  }
13971
+ /**
13972
+ * Marks a slider-host-bound control as touched. The inner thumb is the only
13973
+ * interactive element, so it forwards its touch events here (on blur / pointer
13974
+ * release) — otherwise a control bound to the `tn-slider` host would never
13975
+ * transition to touched and touched-gated validation would never show.
13976
+ */
13977
+ markTouched() {
13978
+ this.onTouched();
13979
+ }
13794
13980
  setDisabledState(isDisabled) {
13795
13981
  this.formDisabled.set(isDisabled);
13796
13982
  }
@@ -13799,7 +13985,6 @@ class TnSliderComponent {
13799
13985
  const clampedValue = this.clampValue(newValue);
13800
13986
  this.value.set(clampedValue);
13801
13987
  this.onChange(clampedValue);
13802
- this.updateThumbPosition();
13803
13988
  }
13804
13989
  enableLabel() {
13805
13990
  this._showLabel.set(true);
@@ -13813,24 +13998,6 @@ class TnSliderComponent {
13813
13998
  getSliderRect() {
13814
13999
  return this.sliderContainer().nativeElement.getBoundingClientRect();
13815
14000
  }
13816
- onTrackClick(event) {
13817
- if (this.isDisabled()) {
13818
- return;
13819
- }
13820
- event.preventDefault();
13821
- const rect = this.getSliderRect();
13822
- const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
13823
- const percentage = (clientX - rect.left) / rect.width;
13824
- const minVal = this.min();
13825
- const maxVal = this.max();
13826
- const newValue = minVal + (percentage * (maxVal - minVal));
13827
- this.updateValue(newValue);
13828
- this.onTouched();
13829
- }
13830
- updateThumbPosition() {
13831
- // Thumb position is now handled by computed signal and template binding
13832
- // No manual DOM manipulation needed
13833
- }
13834
14001
  clampValue(value) {
13835
14002
  const minVal = this.min();
13836
14003
  const maxVal = this.max();
@@ -13888,13 +14055,13 @@ class TnSliderComponent {
13888
14055
  }
13889
14056
  };
13890
14057
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13891
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSliderComponent, isStandalone: true, selector: "tn-slider", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, labelPrefix: { classPropertyName: "labelPrefix", publicName: "labelPrefix", isSignal: true, isRequired: false, transformFunction: null }, labelSuffix: { classPropertyName: "labelSuffix", publicName: "labelSuffix", isSignal: true, isRequired: false, transformFunction: null }, labelType: { classPropertyName: "labelType", publicName: "labelType", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-disabled": "isDisabled()" }, classAttribute: "tn-slider" }, providers: [
14058
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSliderComponent, isStandalone: true, selector: "tn-slider", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, labelPrefix: { classPropertyName: "labelPrefix", publicName: "labelPrefix", isSignal: true, isRequired: false, transformFunction: null }, labelSuffix: { classPropertyName: "labelSuffix", publicName: "labelSuffix", isSignal: true, isRequired: false, transformFunction: null }, labelType: { classPropertyName: "labelType", publicName: "labelType", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-disabled": "isDisabled()" }, classAttribute: "tn-slider" }, providers: [
13892
14059
  {
13893
14060
  provide: NG_VALUE_ACCESSOR,
13894
14061
  useExisting: forwardRef(() => TnSliderComponent),
13895
14062
  multi: true
13896
14063
  }
13897
- ], queries: [{ propertyName: "thumbDirective", first: true, predicate: TnSliderThumbDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "sliderContainer", first: true, predicate: ["sliderContainer"], descendants: true, isSignal: true }, { propertyName: "thumbVisual", first: true, predicate: ["thumbVisual"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n #sliderContainer\n class=\"tn-slider-container\"\n tnTestIdType=\"slider\"\n [tnTestId]=\"testId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.data-disabled]=\"isDisabled()\"\n (mousedown)=\"onTrackClick($event)\"\n (touchstart)=\"onTrackClick($event)\">\n\n <div class=\"tn-slider-track\">\n <div class=\"tn-slider-track-inactive\"></div>\n <div class=\"tn-slider-track-active\">\n <div\n class=\"tn-slider-track-active-fill\"\n [style.transform]=\"'scaleX(' + fillScale() + ')'\">\n </div>\n </div>\n @if ((labelType() === 'track' || labelType() === 'both') && showLabel()) {\n <div class=\"tn-slider-track-label\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <div\n #thumbVisual\n class=\"tn-slider-thumb-visual\"\n [style.transform]=\"'translateX(' + thumbPosition() + 'px)'\">\n <div class=\"tn-slider-thumb-knob\"></div>\n @if ((labelType() === 'handle' || labelType() === 'both') && showLabel()) {\n <div\n class=\"tn-slider-thumb-label\"\n [class.visible]=\"labelVisible()\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <ng-content />\n</div>\n", styles: [":host{display:block;width:100%;height:48px;position:relative;touch-action:pan-x}.tn-slider-container{position:relative;width:100%;height:100%;display:flex;align-items:center;cursor:pointer}.tn-slider-container[data-disabled=true]{cursor:not-allowed;opacity:.6}.tn-slider-track{position:relative;width:100%;height:4px}.tn-slider-track-inactive{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-lines, #e0e0e0);border-radius:2px}.tn-slider-track-active{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-radius:2px}.tn-slider-track-active-fill{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-primary, #007bff);border-radius:2px;transform-origin:left center;transition:transform .1s ease-out}:host ::ng-deep .tn-slider-thumb{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;margin:0;padding:0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none;background:none;border:none;outline:none}:host ::ng-deep .tn-slider-thumb:disabled{cursor:not-allowed}:host ::ng-deep .tn-slider-thumb:focus{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible+.tn-slider-visual-thumb{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-slider-thumb-visual{position:absolute;top:50%;left:0;pointer-events:none;z-index:3;transition:transform .1s ease-out}.tn-slider-thumb-knob{width:20px;height:24px;background:var(--tn-primary, #007bff);border:2px solid var(--tn-bg1, white);border-radius:4px;box-shadow:0 2px 4px #0003;transition:box-shadow .15s ease;transform:translateY(-50%)}.tn-slider-container:hover .tn-slider-thumb-knob{box-shadow:0 4px 8px #0000004d}.tn-slider-container[data-disabled=true] .tn-slider-thumb-knob{background:var(--tn-fg2, #999);box-shadow:0 1px 2px #0000001a}.tn-slider-thumb-label{position:absolute;bottom:calc(100% + 16px);left:50%;transform:translate(-50%) translateY(-50%);padding:8px 12px;background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, white);border-radius:6px;font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;opacity:0;pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity .15s ease;z-index:4;box-shadow:0 2px 8px #00000026}.tn-slider-thumb-label:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid var(--tn-primary, #007bff)}.tn-slider-thumb-label.visible{opacity:1}.tn-slider-track-label{position:absolute;top:-28px;right:0;padding:4px 0;background:transparent;color:var(--tn-fg1, #000);font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none;z-index:2}@media(prefers-reduced-motion:reduce){.tn-slider-track-active-fill,.tn-slider-thumb-visual,.tn-slider-thumb-knob,.tn-slider-thumb-label{transition:none}}@media(prefers-contrast:high){.tn-slider-track-inactive{border:1px solid var(--tn-fg1, #000)}.tn-slider-thumb-knob{border-width:3px;border-color:var(--tn-fg1, #000)}}\n"], dependencies: [{ kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
14064
+ ], queries: [{ propertyName: "thumbDirective", first: true, predicate: TnSliderThumbDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "sliderContainer", first: true, predicate: ["sliderContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n Pointer interaction (click-to-position and press-drag, anywhere on the track)\n is handled by the projected native range input, which overlays the whole\n container (see TnSliderThumbDirective): a click jumps the thumb and fires\n `input` \u2192 onInput; a press-and-move runs the directive's global drag handlers.\n No container-level click handler is needed \u2014 and one wouldn't fire anyway, as\n the overlay calls stopPropagation() on its own pointer events.\n-->\n<div\n #sliderContainer\n class=\"tn-slider-container\"\n tnTestIdType=\"slider\"\n [tnTestId]=\"testId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.data-disabled]=\"isDisabled()\">\n\n <div class=\"tn-slider-track\">\n <div class=\"tn-slider-track-inactive\"></div>\n <div class=\"tn-slider-track-active\">\n <div\n class=\"tn-slider-track-active-fill\"\n [style.transform]=\"'scaleX(' + fillScale() + ')'\">\n </div>\n </div>\n @if ((labelType() === 'track' || labelType() === 'both') && showLabel()) {\n <div class=\"tn-slider-track-label\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <div\n class=\"tn-slider-thumb-visual\"\n [style.left.%]=\"fillPercentage()\">\n <div class=\"tn-slider-thumb-knob\"></div>\n @if ((labelType() === 'handle' || labelType() === 'both') && showLabel()) {\n <div\n class=\"tn-slider-thumb-label\"\n [class.visible]=\"labelVisible()\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <ng-content />\n</div>\n", styles: [":host{display:block;width:100%;height:48px;position:relative;touch-action:pan-x}.tn-slider-container{position:relative;width:100%;height:100%;display:flex;align-items:center;cursor:pointer}.tn-slider-container[data-disabled=true]{cursor:not-allowed;opacity:.6}.tn-slider-track{position:relative;width:100%;height:4px}.tn-slider-track-inactive{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-lines, #e0e0e0);border-radius:2px}.tn-slider-track-active{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-radius:2px}.tn-slider-track-active-fill{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-primary, #007bff);border-radius:2px;transform-origin:left center;transition:transform .1s ease-out}:host ::ng-deep .tn-slider-thumb{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;margin:0;padding:0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none;background:none;border:none;outline:none}:host ::ng-deep .tn-slider-thumb:disabled{cursor:not-allowed}:host ::ng-deep .tn-slider-thumb:focus{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible{outline:none}:host ::ng-deep .tn-slider-container:has(.tn-slider-thumb:focus-visible) .tn-slider-thumb-knob{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-slider-thumb-visual{position:absolute;top:50%;left:0;margin-left:-10px;pointer-events:none;z-index:3;transition:left .1s ease-out}.tn-slider-thumb-knob{width:20px;height:24px;background:var(--tn-primary, #007bff);border:2px solid var(--tn-bg1, white);border-radius:4px;box-shadow:0 2px 4px #0003;transition:box-shadow .15s ease;transform:translateY(-50%)}.tn-slider-container:hover .tn-slider-thumb-knob{box-shadow:0 4px 8px #0000004d}.tn-slider-container[data-disabled=true] .tn-slider-thumb-knob{background:var(--tn-fg2, #999);box-shadow:0 1px 2px #0000001a}.tn-slider-thumb-label{position:absolute;bottom:calc(100% + 16px);left:50%;transform:translate(-50%) translateY(-50%);padding:8px 12px;background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, white);border-radius:6px;font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;opacity:0;pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity .15s ease;z-index:4;box-shadow:0 2px 8px #00000026}.tn-slider-thumb-label:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid var(--tn-primary, #007bff)}.tn-slider-thumb-label.visible{opacity:1}.tn-slider-track-label{position:absolute;top:-28px;right:0;padding:4px 0;background:transparent;color:var(--tn-fg1, #000);font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none;z-index:2}@media(prefers-reduced-motion:reduce){.tn-slider-track-active-fill,.tn-slider-thumb-visual,.tn-slider-thumb-knob,.tn-slider-thumb-label{transition:none}}@media(prefers-contrast:high){.tn-slider-track-inactive{border:1px solid var(--tn-fg1, #000)}.tn-slider-thumb-knob{border-width:3px;border-color:var(--tn-fg1, #000)}}\n"], dependencies: [{ kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
13898
14065
  }
13899
14066
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSliderComponent, decorators: [{
13900
14067
  type: Component,
@@ -13907,8 +14074,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
13907
14074
  ], host: {
13908
14075
  'class': 'tn-slider',
13909
14076
  '[attr.aria-disabled]': 'isDisabled()'
13910
- }, template: "<div\n #sliderContainer\n class=\"tn-slider-container\"\n tnTestIdType=\"slider\"\n [tnTestId]=\"testId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.data-disabled]=\"isDisabled()\"\n (mousedown)=\"onTrackClick($event)\"\n (touchstart)=\"onTrackClick($event)\">\n\n <div class=\"tn-slider-track\">\n <div class=\"tn-slider-track-inactive\"></div>\n <div class=\"tn-slider-track-active\">\n <div\n class=\"tn-slider-track-active-fill\"\n [style.transform]=\"'scaleX(' + fillScale() + ')'\">\n </div>\n </div>\n @if ((labelType() === 'track' || labelType() === 'both') && showLabel()) {\n <div class=\"tn-slider-track-label\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <div\n #thumbVisual\n class=\"tn-slider-thumb-visual\"\n [style.transform]=\"'translateX(' + thumbPosition() + 'px)'\">\n <div class=\"tn-slider-thumb-knob\"></div>\n @if ((labelType() === 'handle' || labelType() === 'both') && showLabel()) {\n <div\n class=\"tn-slider-thumb-label\"\n [class.visible]=\"labelVisible()\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <ng-content />\n</div>\n", styles: [":host{display:block;width:100%;height:48px;position:relative;touch-action:pan-x}.tn-slider-container{position:relative;width:100%;height:100%;display:flex;align-items:center;cursor:pointer}.tn-slider-container[data-disabled=true]{cursor:not-allowed;opacity:.6}.tn-slider-track{position:relative;width:100%;height:4px}.tn-slider-track-inactive{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-lines, #e0e0e0);border-radius:2px}.tn-slider-track-active{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-radius:2px}.tn-slider-track-active-fill{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-primary, #007bff);border-radius:2px;transform-origin:left center;transition:transform .1s ease-out}:host ::ng-deep .tn-slider-thumb{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;margin:0;padding:0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none;background:none;border:none;outline:none}:host ::ng-deep .tn-slider-thumb:disabled{cursor:not-allowed}:host ::ng-deep .tn-slider-thumb:focus{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible+.tn-slider-visual-thumb{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-slider-thumb-visual{position:absolute;top:50%;left:0;pointer-events:none;z-index:3;transition:transform .1s ease-out}.tn-slider-thumb-knob{width:20px;height:24px;background:var(--tn-primary, #007bff);border:2px solid var(--tn-bg1, white);border-radius:4px;box-shadow:0 2px 4px #0003;transition:box-shadow .15s ease;transform:translateY(-50%)}.tn-slider-container:hover .tn-slider-thumb-knob{box-shadow:0 4px 8px #0000004d}.tn-slider-container[data-disabled=true] .tn-slider-thumb-knob{background:var(--tn-fg2, #999);box-shadow:0 1px 2px #0000001a}.tn-slider-thumb-label{position:absolute;bottom:calc(100% + 16px);left:50%;transform:translate(-50%) translateY(-50%);padding:8px 12px;background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, white);border-radius:6px;font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;opacity:0;pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity .15s ease;z-index:4;box-shadow:0 2px 8px #00000026}.tn-slider-thumb-label:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid var(--tn-primary, #007bff)}.tn-slider-thumb-label.visible{opacity:1}.tn-slider-track-label{position:absolute;top:-28px;right:0;padding:4px 0;background:transparent;color:var(--tn-fg1, #000);font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none;z-index:2}@media(prefers-reduced-motion:reduce){.tn-slider-track-active-fill,.tn-slider-thumb-visual,.tn-slider-thumb-knob,.tn-slider-thumb-label{transition:none}}@media(prefers-contrast:high){.tn-slider-track-inactive{border:1px solid var(--tn-fg1, #000)}.tn-slider-thumb-knob{border-width:3px;border-color:var(--tn-fg1, #000)}}\n"] }]
13911
- }], ctorParameters: () => [], propDecorators: { min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], labelPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPrefix", required: false }] }], labelSuffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelSuffix", required: false }] }], labelType: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelType", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], thumbDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnSliderThumbDirective), { isSignal: true }] }], sliderContainer: [{ type: i0.ViewChild, args: ['sliderContainer', { isSignal: true }] }], thumbVisual: [{ type: i0.ViewChild, args: ['thumbVisual', { isSignal: true }] }] } });
14077
+ }, template: "<!--\n Pointer interaction (click-to-position and press-drag, anywhere on the track)\n is handled by the projected native range input, which overlays the whole\n container (see TnSliderThumbDirective): a click jumps the thumb and fires\n `input` \u2192 onInput; a press-and-move runs the directive's global drag handlers.\n No container-level click handler is needed \u2014 and one wouldn't fire anyway, as\n the overlay calls stopPropagation() on its own pointer events.\n-->\n<div\n #sliderContainer\n class=\"tn-slider-container\"\n tnTestIdType=\"slider\"\n [tnTestId]=\"testId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.data-disabled]=\"isDisabled()\">\n\n <div class=\"tn-slider-track\">\n <div class=\"tn-slider-track-inactive\"></div>\n <div class=\"tn-slider-track-active\">\n <div\n class=\"tn-slider-track-active-fill\"\n [style.transform]=\"'scaleX(' + fillScale() + ')'\">\n </div>\n </div>\n @if ((labelType() === 'track' || labelType() === 'both') && showLabel()) {\n <div class=\"tn-slider-track-label\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <div\n class=\"tn-slider-thumb-visual\"\n [style.left.%]=\"fillPercentage()\">\n <div class=\"tn-slider-thumb-knob\"></div>\n @if ((labelType() === 'handle' || labelType() === 'both') && showLabel()) {\n <div\n class=\"tn-slider-thumb-label\"\n [class.visible]=\"labelVisible()\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <ng-content />\n</div>\n", styles: [":host{display:block;width:100%;height:48px;position:relative;touch-action:pan-x}.tn-slider-container{position:relative;width:100%;height:100%;display:flex;align-items:center;cursor:pointer}.tn-slider-container[data-disabled=true]{cursor:not-allowed;opacity:.6}.tn-slider-track{position:relative;width:100%;height:4px}.tn-slider-track-inactive{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-lines, #e0e0e0);border-radius:2px}.tn-slider-track-active{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-radius:2px}.tn-slider-track-active-fill{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-primary, #007bff);border-radius:2px;transform-origin:left center;transition:transform .1s ease-out}:host ::ng-deep .tn-slider-thumb{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;margin:0;padding:0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none;background:none;border:none;outline:none}:host ::ng-deep .tn-slider-thumb:disabled{cursor:not-allowed}:host ::ng-deep .tn-slider-thumb:focus{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible{outline:none}:host ::ng-deep .tn-slider-container:has(.tn-slider-thumb:focus-visible) .tn-slider-thumb-knob{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-slider-thumb-visual{position:absolute;top:50%;left:0;margin-left:-10px;pointer-events:none;z-index:3;transition:left .1s ease-out}.tn-slider-thumb-knob{width:20px;height:24px;background:var(--tn-primary, #007bff);border:2px solid var(--tn-bg1, white);border-radius:4px;box-shadow:0 2px 4px #0003;transition:box-shadow .15s ease;transform:translateY(-50%)}.tn-slider-container:hover .tn-slider-thumb-knob{box-shadow:0 4px 8px #0000004d}.tn-slider-container[data-disabled=true] .tn-slider-thumb-knob{background:var(--tn-fg2, #999);box-shadow:0 1px 2px #0000001a}.tn-slider-thumb-label{position:absolute;bottom:calc(100% + 16px);left:50%;transform:translate(-50%) translateY(-50%);padding:8px 12px;background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, white);border-radius:6px;font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;opacity:0;pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity .15s ease;z-index:4;box-shadow:0 2px 8px #00000026}.tn-slider-thumb-label:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid var(--tn-primary, #007bff)}.tn-slider-thumb-label.visible{opacity:1}.tn-slider-track-label{position:absolute;top:-28px;right:0;padding:4px 0;background:transparent;color:var(--tn-fg1, #000);font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none;z-index:2}@media(prefers-reduced-motion:reduce){.tn-slider-track-active-fill,.tn-slider-thumb-visual,.tn-slider-thumb-knob,.tn-slider-thumb-label{transition:none}}@media(prefers-contrast:high){.tn-slider-track-inactive{border:1px solid var(--tn-fg1, #000)}.tn-slider-thumb-knob{border-width:3px;border-color:var(--tn-fg1, #000)}}\n"] }]
14078
+ }], ctorParameters: () => [], propDecorators: { min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], labelPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPrefix", required: false }] }], labelSuffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelSuffix", required: false }] }], labelType: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelType", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], thumbDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnSliderThumbDirective), { isSignal: true }] }], sliderContainer: [{ type: i0.ViewChild, args: ['sliderContainer', { isSignal: true }] }] } });
13912
14079
 
13913
14080
  class TnSliderWithLabelDirective {
13914
14081
  enabled = input(true, { ...(ngDevMode ? { debugName: "enabled" } : {}), alias: 'tnSliderWithLabel' });