@truenas/ui-components 0.2.1 → 0.2.2

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.
@@ -1,7 +1,7 @@
1
1
  import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
2
2
  import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
3
3
  import * as i0 from '@angular/core';
4
- import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, ViewContainerRef, output, viewChild, computed, signal, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, contentChildren, HostListener, contentChild, ChangeDetectorRef, DestroyRef, TemplateRef, isSignal, untracked, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
4
+ import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, ViewContainerRef, output, viewChild, computed, signal, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, contentChildren, HostListener, linkedSignal, contentChild, ChangeDetectorRef, DestroyRef, TemplateRef, isSignal, untracked, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
5
5
  import * as i1$3 from '@angular/forms';
6
6
  import { NG_VALUE_ACCESSOR, FormsModule, NgControl, Validators } from '@angular/forms';
7
7
  import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
@@ -2514,6 +2514,81 @@ var InputType;
2514
2514
  InputType["Size"] = "size";
2515
2515
  })(InputType || (InputType = {}));
2516
2516
 
2517
+ /**
2518
+ * Marks an icon name for inclusion in the TrueNAS UI sprite generation.
2519
+ *
2520
+ * This function serves two purposes:
2521
+ * 1. At runtime: Applies library-specific prefixes (e.g., mdi-, app-)
2522
+ * 2. At build time: Marker for the scanner to detect icons for sprite inclusion
2523
+ *
2524
+ * Use this when icon names are computed dynamically or come from variables,
2525
+ * to ensure they're included in the sprite at build time.
2526
+ *
2527
+ * @example
2528
+ * // Static icon name - automatically detected from template
2529
+ * <tn-icon name="folder"></tn-icon>
2530
+ *
2531
+ * @example
2532
+ * // Dynamic MDI icon
2533
+ * const iconName = condition ? tnIconMarker("pencil", "mdi") : tnIconMarker("delete", "mdi");
2534
+ * <tn-icon [name]="iconName"></tn-icon>
2535
+ *
2536
+ * @example
2537
+ * // Dynamic custom icon (consumer's own icon)
2538
+ * const logo = tnIconMarker("your-logo-name", "custom");
2539
+ * <tn-icon [name]="logo"></tn-icon>
2540
+ *
2541
+ * @example
2542
+ * // Array of dynamic icons
2543
+ * const actions = [
2544
+ * { name: "Save", icon: tnIconMarker("content-save", "mdi") },
2545
+ * { name: "Cancel", icon: tnIconMarker("close", "mdi") }
2546
+ * ];
2547
+ *
2548
+ * @param iconName - The icon name to mark for sprite inclusion
2549
+ * @param library - Optional library type: 'mdi', 'material', or 'custom'
2550
+ * @returns The icon name with appropriate prefix applied
2551
+ * @public
2552
+ */
2553
+ function tnIconMarker(iconName, library) {
2554
+ // Apply library-specific prefixes
2555
+ if (library === 'mdi' && !iconName.startsWith('mdi-')) {
2556
+ return `mdi-${iconName}`;
2557
+ }
2558
+ if (library === 'custom' && !iconName.startsWith('app-')) {
2559
+ return `app-${iconName}`;
2560
+ }
2561
+ if (library === 'material' && !iconName.startsWith('mat-')) {
2562
+ return `mat-${iconName}`;
2563
+ }
2564
+ return iconName;
2565
+ }
2566
+ /**
2567
+ * INTERNAL LIBRARY USE ONLY
2568
+ *
2569
+ * Marks an icon name for inclusion in the sprite generation with library namespace.
2570
+ * This function MUST be used by library component code for custom icons.
2571
+ *
2572
+ * The TypeScript type enforces that the icon name starts with 'tn-' prefix,
2573
+ * which reserves this namespace exclusively for library-provided custom icons.
2574
+ *
2575
+ * @example
2576
+ * ```typescript
2577
+ * // ✅ Correct - Library component code
2578
+ * const icon = libIconMarker('tn-dataset');
2579
+ *
2580
+ * // ❌ Wrong - Will cause TypeScript error
2581
+ * const icon = libIconMarker('dataset');
2582
+ * ```
2583
+ *
2584
+ * @param iconName - The icon name with 'tn-' prefix (enforced by TypeScript)
2585
+ * @returns The same icon name (identity function)
2586
+ * @internal
2587
+ */
2588
+ function libIconMarker(iconName) {
2589
+ return iconName;
2590
+ }
2591
+
2517
2592
  // Ordered binary/decimal prefixes; the array index doubles as the power applied
2518
2593
  // to the base (B = base^0, K = base^1, M = base^2, ...).
2519
2594
  const UNIT_PREFIXES = ['B', 'K', 'M', 'G', 'T', 'P', 'E'];
@@ -2687,6 +2762,14 @@ class TnInputComponent {
2687
2762
  * (e.g. `1.5 GiB`). Ignored unless `inputType` is `Size`.
2688
2763
  */
2689
2764
  sizeRound = input(2, ...(ngDevMode ? [{ debugName: "sizeRound" }] : []));
2765
+ /**
2766
+ * Whether a `Password` field renders the built-in visibility toggle — the eye
2767
+ * button that switches the field between masked and plain-text display.
2768
+ * Defaults to true; set false for secrets that must never be revealed.
2769
+ * Setting `suffixIcon` suppresses the toggle automatically (a custom suffix
2770
+ * control replaces it). Ignored unless `inputType` is `Password`.
2771
+ */
2772
+ showPasswordToggle = input(true, ...(ngDevMode ? [{ debugName: "showPasswordToggle" }] : []));
2690
2773
  // Icon inputs
2691
2774
  prefixIcon = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIcon" }] : []));
2692
2775
  prefixIconLibrary = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIconLibrary" }] : []));
@@ -2709,8 +2792,28 @@ class TnInputComponent {
2709
2792
  isSize = computed(() => this.inputType() === InputType.Size, ...(ngDevMode ? [{ debugName: "isSize" }] : []));
2710
2793
  /** True when the field only accepts whole numbers (i.e. `allowDecimals` is false). */
2711
2794
  integerOnly = computed(() => !this.allowDecimals(), ...(ngDevMode ? [{ debugName: "integerOnly" }] : []));
2712
- /** The `type` attribute actually rendered. Number and size modes use a text input: number avoids the native type="number" footguns, size must accept unit letters. */
2713
- resolvedType = computed(() => (this.isNumeric() || this.isSize() ? InputType.PlainText : this.inputType()), ...(ngDevMode ? [{ debugName: "resolvedType" }] : []));
2795
+ /** True when the field is a password field. */
2796
+ isPassword = computed(() => this.inputType() === InputType.Password, ...(ngDevMode ? [{ debugName: "isPassword" }] : []));
2797
+ /**
2798
+ * Whether the password is currently revealed. Not exposed as an input so a
2799
+ * reveal is always an explicit user gesture, and linked to the toggle's own
2800
+ * availability so a revealed value never outlives the control that revealed
2801
+ * it: whenever the toggle goes away (the field leaves password mode,
2802
+ * `showPasswordToggle` flips off, a `suffixIcon` replaces it) or the field
2803
+ * is disabled, the field snaps back to masked.
2804
+ */
2805
+ passwordVisible = linkedSignal({ ...(ngDevMode ? { debugName: "passwordVisible" } : {}), source: () => this.hasPasswordToggle() && !this.isDisabled(),
2806
+ computation: () => false });
2807
+ /** The `type` attribute actually rendered. Number and size modes use a text input: number avoids the native type="number" footguns, size must accept unit letters. A revealed password renders as text — flipping the type attribute is the standard reveal mechanism and preserves value and caret. */
2808
+ resolvedType = computed(() => {
2809
+ if (this.isNumeric() || this.isSize()) {
2810
+ return InputType.PlainText;
2811
+ }
2812
+ if (this.isPassword() && this.passwordVisible()) {
2813
+ return InputType.PlainText;
2814
+ }
2815
+ return this.inputType();
2816
+ }, ...(ngDevMode ? [{ debugName: "resolvedType" }] : []));
2714
2817
  /** `inputmode` hint: numeric keypad for integers, decimal keypad otherwise. Null (omitted) outside number mode — size fields accept unit letters, so they keep the full keyboard. */
2715
2818
  numericInputMode = computed(() => {
2716
2819
  if (!this.isNumeric()) {
@@ -2720,6 +2823,30 @@ class TnInputComponent {
2720
2823
  }, ...(ngDevMode ? [{ debugName: "numericInputMode" }] : []));
2721
2824
  /** Number and size modes are always single-line; they win over `multiline` if both are set. */
2722
2825
  showTextarea = computed(() => this.multiline() && !this.isNumeric() && !this.isSize(), ...(ngDevMode ? [{ debugName: "showTextarea" }] : []));
2826
+ /**
2827
+ * The visibility toggle renders only on single-line password fields (a
2828
+ * password+multiline combo renders a textarea, which has no `type` to flip),
2829
+ * and yields to a consumer-provided `suffixIcon` — a custom suffix control
2830
+ * replaces the built-in toggle rather than stacking a second button.
2831
+ */
2832
+ hasPasswordToggle = computed(() => this.isPassword() && this.showPasswordToggle() && !this.showTextarea() && !this.hasSuffixIcon(), ...(ngDevMode ? [{ debugName: "hasPasswordToggle" }] : []));
2833
+ /**
2834
+ * Icon for the visibility toggle, mirroring the field's current state: a
2835
+ * slashed eye while masked, an open eye while revealed. The literal
2836
+ * `tnIconMarker` calls double as build-time markers that pull both icons
2837
+ * into the sprite.
2838
+ */
2839
+ passwordToggleIcon = computed(() => this.passwordVisible() ? tnIconMarker('eye', 'mdi') : tnIconMarker('eye-off', 'mdi'), ...(ngDevMode ? [{ debugName: "passwordToggleIcon" }] : []));
2840
+ /**
2841
+ * Role-first test-id segments for the visibility toggle:
2842
+ * `button-toggle-password[-<testId>]`. The toggle is fixed field chrome, so
2843
+ * the role leads (matching the dialog-shell close/fullscreen convention) and
2844
+ * automation can target every password toggle with one prefix selector.
2845
+ */
2846
+ passwordToggleTestId = computed(() => {
2847
+ const base = this.testId();
2848
+ return ['toggle-password', ...(Array.isArray(base) ? base : [base])];
2849
+ }, ...(ngDevMode ? [{ debugName: "passwordToggleTestId" }] : []));
2723
2850
  // Unique per instance: a hard-coded id produced duplicate ids when multiple
2724
2851
  // tn-inputs share a page (invalid HTML, and it breaks any future label `for=`,
2725
2852
  // aria-describedby, or getElementById targeting this input).
@@ -2827,6 +2954,9 @@ class TnInputComponent {
2827
2954
  }
2828
2955
  this.onTouched();
2829
2956
  }
2957
+ togglePasswordVisibility() {
2958
+ this.passwordVisible.update((visible) => !visible);
2959
+ }
2830
2960
  /** Strips any character that can't appear in the current numeric mode (single leading '-', single '.' for decimals). */
2831
2961
  sanitizeNumeric(raw) {
2832
2962
  const allowDecimal = !this.integerOnly();
@@ -2855,13 +2985,13 @@ class TnInputComponent {
2855
2985
  return Number.isNaN(parsed) ? null : parsed;
2856
2986
  }
2857
2987
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2858
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnInputComponent, isStandalone: true, selector: "tn-input", inputs: { inputType: { classPropertyName: "inputType", publicName: "inputType", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, allowDecimals: { classPropertyName: "allowDecimals", publicName: "allowDecimals", isSignal: true, isRequired: false, transformFunction: null }, sizeStandard: { classPropertyName: "sizeStandard", publicName: "sizeStandard", isSignal: true, isRequired: false, transformFunction: null }, sizeDefaultUnit: { classPropertyName: "sizeDefaultUnit", publicName: "sizeDefaultUnit", isSignal: true, isRequired: false, transformFunction: null }, sizeRound: { classPropertyName: "sizeRound", publicName: "sizeRound", isSignal: true, isRequired: false, transformFunction: null }, prefixIcon: { classPropertyName: "prefixIcon", publicName: "prefixIcon", isSignal: true, isRequired: false, transformFunction: null }, prefixIconLibrary: { classPropertyName: "prefixIconLibrary", publicName: "prefixIconLibrary", isSignal: true, isRequired: false, transformFunction: null }, suffixIcon: { classPropertyName: "suffixIcon", publicName: "suffixIcon", isSignal: true, isRequired: false, transformFunction: null }, suffixIconLibrary: { classPropertyName: "suffixIconLibrary", publicName: "suffixIconLibrary", isSignal: true, isRequired: false, transformFunction: null }, suffixIconAriaLabel: { classPropertyName: "suffixIconAriaLabel", publicName: "suffixIconAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, suffixActionTestId: { classPropertyName: "suffixActionTestId", publicName: "suffixActionTestId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSuffixAction: "onSuffixAction" }, providers: [
2988
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnInputComponent, isStandalone: true, selector: "tn-input", inputs: { inputType: { classPropertyName: "inputType", publicName: "inputType", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, allowDecimals: { classPropertyName: "allowDecimals", publicName: "allowDecimals", isSignal: true, isRequired: false, transformFunction: null }, sizeStandard: { classPropertyName: "sizeStandard", publicName: "sizeStandard", isSignal: true, isRequired: false, transformFunction: null }, sizeDefaultUnit: { classPropertyName: "sizeDefaultUnit", publicName: "sizeDefaultUnit", isSignal: true, isRequired: false, transformFunction: null }, sizeRound: { classPropertyName: "sizeRound", publicName: "sizeRound", isSignal: true, isRequired: false, transformFunction: null }, showPasswordToggle: { classPropertyName: "showPasswordToggle", publicName: "showPasswordToggle", isSignal: true, isRequired: false, transformFunction: null }, prefixIcon: { classPropertyName: "prefixIcon", publicName: "prefixIcon", isSignal: true, isRequired: false, transformFunction: null }, prefixIconLibrary: { classPropertyName: "prefixIconLibrary", publicName: "prefixIconLibrary", isSignal: true, isRequired: false, transformFunction: null }, suffixIcon: { classPropertyName: "suffixIcon", publicName: "suffixIcon", isSignal: true, isRequired: false, transformFunction: null }, suffixIconLibrary: { classPropertyName: "suffixIconLibrary", publicName: "suffixIconLibrary", isSignal: true, isRequired: false, transformFunction: null }, suffixIconAriaLabel: { classPropertyName: "suffixIconAriaLabel", publicName: "suffixIconAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, suffixActionTestId: { classPropertyName: "suffixActionTestId", publicName: "suffixActionTestId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSuffixAction: "onSuffixAction" }, providers: [
2859
2989
  {
2860
2990
  provide: NG_VALUE_ACCESSOR,
2861
2991
  useExisting: forwardRef(() => TnInputComponent),
2862
2992
  multi: true
2863
2993
  }
2864
- ], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n tnTestIdType=\"input\"\n [id]=\"id\"\n [value]=\"value\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"testId()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n tnTestIdType=\"textarea\"\n [id]=\"id\"\n [value]=\"value\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"testId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n tnTestIdType=\"button\"\n [tnTestId]=\"suffixActionTestId()\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
2994
+ ], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon() || hasPasswordToggle()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n tnTestIdType=\"input\"\n [id]=\"id\"\n [value]=\"value\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"testId()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n tnTestIdType=\"textarea\"\n [id]=\"id\"\n [value]=\"value\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"testId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n tnTestIdType=\"button\"\n [tnTestId]=\"suffixActionTestId()\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n\n <!-- Password visibility toggle. State is conveyed via the swapping\n aria-label alone \u2014 no aria-pressed, which would contradict a label that\n already names the action (\"Hide password, pressed\" is ambiguous). -->\n @if (hasPasswordToggle()) {\n <button\n type=\"button\"\n class=\"tn-input__visibility-toggle\"\n tnTestIdType=\"button\"\n [tnTestId]=\"passwordToggleTestId()\"\n [attr.aria-label]=\"passwordVisible() ? 'Hide password' : 'Show password'\"\n [disabled]=\"isDisabled()\"\n (click)=\"togglePasswordVisibility()\"\n >\n <tn-icon size=\"sm\" aria-hidden=\"true\" [name]=\"passwordToggleIcon()\" />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action,.tn-input__visibility-toggle{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled),.tn-input__visibility-toggle:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible,.tn-input__visibility-toggle:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled,.tn-input__visibility-toggle:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action,.tn-input__visibility-toggle{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
2865
2995
  }
2866
2996
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnInputComponent, decorators: [{
2867
2997
  type: Component,
@@ -2871,8 +3001,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2871
3001
  useExisting: forwardRef(() => TnInputComponent),
2872
3002
  multi: true
2873
3003
  }
2874
- ], template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n tnTestIdType=\"input\"\n [id]=\"id\"\n [value]=\"value\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"testId()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n tnTestIdType=\"textarea\"\n [id]=\"id\"\n [value]=\"value\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"testId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n tnTestIdType=\"button\"\n [tnTestId]=\"suffixActionTestId()\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"] }]
2875
- }], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], inputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputType", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], allowDecimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDecimals", required: false }] }], sizeStandard: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeStandard", required: false }] }], sizeDefaultUnit: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeDefaultUnit", required: false }] }], sizeRound: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeRound", required: false }] }], prefixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIcon", required: false }] }], prefixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconLibrary", required: false }] }], suffixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIcon", required: false }] }], suffixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconLibrary", required: false }] }], suffixIconAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconAriaLabel", required: false }] }], suffixActionTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixActionTestId", required: false }] }], onSuffixAction: [{ type: i0.Output, args: ["onSuffixAction"] }] } });
3004
+ ], template: "<div\n class=\"tn-input-container\"\n [class.tn-input-container--has-prefix]=\"hasPrefixIcon()\"\n [class.tn-input-container--has-suffix]=\"hasSuffixIcon() || hasPasswordToggle()\"\n>\n <!-- Prefix icon -->\n @if (hasPrefixIcon()) {\n <tn-icon\n class=\"tn-input__prefix-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"prefixIcon()!\"\n [library]=\"prefixIconLibrary()\"\n />\n }\n\n <!-- Regular input field -->\n @if (!showTextarea()) {\n <input\n #inputEl\n class=\"tn-input\"\n tnTestIdType=\"input\"\n [id]=\"id\"\n [value]=\"value\"\n [type]=\"resolvedType()\"\n [attr.inputmode]=\"numericInputMode()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"testId()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n />\n }\n\n <!-- Textarea field -->\n @if (showTextarea()) {\n <textarea\n #inputEl\n class=\"tn-input tn-textarea\"\n tnTestIdType=\"textarea\"\n [id]=\"id\"\n [value]=\"value\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.placeholder]=\"placeholder()\"\n [attr.autocomplete]=\"autocomplete()\"\n [attr.name]=\"name()\"\n [tnTestId]=\"testId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n (input)=\"onValueChange($event)\"\n (blur)=\"onBlur()\"\n ></textarea>\n }\n\n <!-- Suffix icon action button -->\n @if (hasSuffixIcon()) {\n <button\n type=\"button\"\n class=\"tn-input__suffix-action\"\n tnTestIdType=\"button\"\n [tnTestId]=\"suffixActionTestId()\"\n [attr.aria-label]=\"suffixIconAriaLabel()\"\n [disabled]=\"isDisabled()\"\n (click)=\"onSuffixAction.emit($event)\"\n >\n <tn-icon\n size=\"sm\"\n [name]=\"suffixIcon()!\"\n [library]=\"suffixIconLibrary()\"\n />\n </button>\n }\n\n <!-- Password visibility toggle. State is conveyed via the swapping\n aria-label alone \u2014 no aria-pressed, which would contradict a label that\n already names the action (\"Hide password, pressed\" is ambiguous). -->\n @if (hasPasswordToggle()) {\n <button\n type=\"button\"\n class=\"tn-input__visibility-toggle\"\n tnTestIdType=\"button\"\n [tnTestId]=\"passwordToggleTestId()\"\n [attr.aria-label]=\"passwordVisible() ? 'Hide password' : 'Show password'\"\n [disabled]=\"isDisabled()\"\n (click)=\"togglePasswordVisibility()\"\n >\n <tn-icon size=\"sm\" aria-hidden=\"true\" [name]=\"passwordToggleIcon()\" />\n </button>\n }\n</div>\n", styles: [".tn-input-container{position:relative;display:flex;align-items:center;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-input-container:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-input-container:has(.tn-input:disabled){background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-input-container:has(.tn-input.error){border-color:var(--tn-error, #dc3545)}.tn-input-container:has(.tn-input.error):focus-within{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-input{display:block;flex:1;min-width:0;min-height:2.5rem;padding:.5rem .75rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;border-radius:.375rem;outline:none;box-sizing:border-box}.tn-input::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-input:disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed}.tn-input-container--has-prefix .tn-input{padding-left:.75rem;border-left:1px solid var(--tn-lines, #d1d5db);border-top-left-radius:0;border-bottom-left-radius:0}.tn-input-container--has-suffix .tn-input{padding-right:.25rem}.tn-input__prefix-icon{flex-shrink:0;margin-left:.75rem;margin-right:.75rem;color:var(--tn-fg2, #6c757d);pointer-events:none}.tn-input__suffix-action,.tn-input__visibility-toggle{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:.375rem;padding:.25rem;border:none;border-radius:.25rem;background:transparent;color:var(--tn-fg2, #6c757d);cursor:pointer;transition:background-color .15s ease-in-out,color .15s ease-in-out}.tn-input__suffix-action:hover:not(:disabled),.tn-input__visibility-toggle:hover:not(:disabled){background-color:var(--tn-bg3, #f3f4f6);color:var(--tn-fg1, #1f2937)}.tn-input__suffix-action:focus-visible,.tn-input__visibility-toggle:focus-visible{outline:2px solid var(--tn-primary, #2563eb);outline-offset:1px}.tn-input__suffix-action:disabled,.tn-input__visibility-toggle:disabled{cursor:not-allowed;pointer-events:none}.tn-textarea{min-height:6rem;resize:vertical;line-height:1.4}@media(prefers-reduced-motion:reduce){.tn-input-container,.tn-input__suffix-action,.tn-input__visibility-toggle{transition:none}}@media(prefers-contrast:high){.tn-input-container{border-width:2px}}\n"] }]
3005
+ }], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], inputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputType", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], allowDecimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDecimals", required: false }] }], sizeStandard: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeStandard", required: false }] }], sizeDefaultUnit: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeDefaultUnit", required: false }] }], sizeRound: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeRound", required: false }] }], showPasswordToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPasswordToggle", required: false }] }], prefixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIcon", required: false }] }], prefixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIconLibrary", required: false }] }], suffixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIcon", required: false }] }], suffixIconLibrary: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconLibrary", required: false }] }], suffixIconAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIconAriaLabel", required: false }] }], suffixActionTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixActionTestId", required: false }] }], onSuffixAction: [{ type: i0.Output, args: ["onSuffixAction"] }] } });
2876
3006
 
2877
3007
  /**
2878
3008
  * Harness for interacting with tn-icon in tests.
@@ -3132,6 +3262,7 @@ class TnInputHarness extends ComponentHarness {
3132
3262
  prefixIconEl = this.locatorForOptional('tn-icon.tn-input__prefix-icon');
3133
3263
  suffixButton = this.locatorForOptional('.tn-input__suffix-action');
3134
3264
  suffixIconEl = this.locatorForOptional(TnIconHarness.with({ ancestor: '.tn-input__suffix-action' }));
3265
+ visibilityToggle = this.locatorForOptional('.tn-input__visibility-toggle');
3135
3266
  /**
3136
3267
  * Gets a `HarnessPredicate` that can be used to search for an input
3137
3268
  * with specific attributes.
@@ -3390,6 +3521,43 @@ class TnInputHarness extends ComponentHarness {
3390
3521
  }
3391
3522
  return button.click();
3392
3523
  }
3524
+ /**
3525
+ * Checks whether the password visibility toggle is present.
3526
+ *
3527
+ * @returns Promise resolving to true if the toggle button exists.
3528
+ */
3529
+ async hasPasswordToggle() {
3530
+ const toggle = await this.visibilityToggle();
3531
+ return toggle !== null;
3532
+ }
3533
+ /**
3534
+ * Whether the password is currently revealed (the field renders as plain text).
3535
+ *
3536
+ * Only meaningful on password-type inputs; resolves false while masked.
3537
+ *
3538
+ * @returns Promise resolving to true when the value is shown in plain text.
3539
+ */
3540
+ async isPasswordRevealed() {
3541
+ const toggle = await this.visibilityToggle();
3542
+ if (!toggle) {
3543
+ return false;
3544
+ }
3545
+ const input = await this.inputEl();
3546
+ return (await input.getProperty('type')) === 'text';
3547
+ }
3548
+ /**
3549
+ * Clicks the password visibility toggle, switching between masked and
3550
+ * plain-text display.
3551
+ *
3552
+ * @returns Promise that resolves when the click action is complete.
3553
+ */
3554
+ async togglePasswordVisibility() {
3555
+ const toggle = await this.visibilityToggle();
3556
+ if (!toggle) {
3557
+ throw new Error('No password visibility toggle found on this input.');
3558
+ }
3559
+ return toggle.click();
3560
+ }
3393
3561
  /**
3394
3562
  * Focuses the input element.
3395
3563
  *
@@ -7490,81 +7658,6 @@ class TnSelectHarness extends ComponentHarness {
7490
7658
  }
7491
7659
  }
7492
7660
 
7493
- /**
7494
- * Marks an icon name for inclusion in the TrueNAS UI sprite generation.
7495
- *
7496
- * This function serves two purposes:
7497
- * 1. At runtime: Applies library-specific prefixes (e.g., mdi-, app-)
7498
- * 2. At build time: Marker for the scanner to detect icons for sprite inclusion
7499
- *
7500
- * Use this when icon names are computed dynamically or come from variables,
7501
- * to ensure they're included in the sprite at build time.
7502
- *
7503
- * @example
7504
- * // Static icon name - automatically detected from template
7505
- * <tn-icon name="folder"></tn-icon>
7506
- *
7507
- * @example
7508
- * // Dynamic MDI icon
7509
- * const iconName = condition ? tnIconMarker("pencil", "mdi") : tnIconMarker("delete", "mdi");
7510
- * <tn-icon [name]="iconName"></tn-icon>
7511
- *
7512
- * @example
7513
- * // Dynamic custom icon (consumer's own icon)
7514
- * const logo = tnIconMarker("your-logo-name", "custom");
7515
- * <tn-icon [name]="logo"></tn-icon>
7516
- *
7517
- * @example
7518
- * // Array of dynamic icons
7519
- * const actions = [
7520
- * { name: "Save", icon: tnIconMarker("content-save", "mdi") },
7521
- * { name: "Cancel", icon: tnIconMarker("close", "mdi") }
7522
- * ];
7523
- *
7524
- * @param iconName - The icon name to mark for sprite inclusion
7525
- * @param library - Optional library type: 'mdi', 'material', or 'custom'
7526
- * @returns The icon name with appropriate prefix applied
7527
- * @public
7528
- */
7529
- function tnIconMarker(iconName, library) {
7530
- // Apply library-specific prefixes
7531
- if (library === 'mdi' && !iconName.startsWith('mdi-')) {
7532
- return `mdi-${iconName}`;
7533
- }
7534
- if (library === 'custom' && !iconName.startsWith('app-')) {
7535
- return `app-${iconName}`;
7536
- }
7537
- if (library === 'material' && !iconName.startsWith('mat-')) {
7538
- return `mat-${iconName}`;
7539
- }
7540
- return iconName;
7541
- }
7542
- /**
7543
- * INTERNAL LIBRARY USE ONLY
7544
- *
7545
- * Marks an icon name for inclusion in the sprite generation with library namespace.
7546
- * This function MUST be used by library component code for custom icons.
7547
- *
7548
- * The TypeScript type enforces that the icon name starts with 'tn-' prefix,
7549
- * which reserves this namespace exclusively for library-provided custom icons.
7550
- *
7551
- * @example
7552
- * ```typescript
7553
- * // ✅ Correct - Library component code
7554
- * const icon = libIconMarker('tn-dataset');
7555
- *
7556
- * // ❌ Wrong - Will cause TypeScript error
7557
- * const icon = libIconMarker('dataset');
7558
- * ```
7559
- *
7560
- * @param iconName - The icon name with 'tn-' prefix (enforced by TypeScript)
7561
- * @returns The same icon name (identity function)
7562
- * @internal
7563
- */
7564
- function libIconMarker(iconName) {
7565
- return iconName;
7566
- }
7567
-
7568
7661
  /// <reference types="jest" />
7569
7662
  /**
7570
7663
  * Creates default mock implementation of TnSpriteLoaderService.