@truenas/ui-components 0.2.0 → 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,9 +1,9 @@
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
- import { NG_VALUE_ACCESSOR, FormsModule, NgControl } from '@angular/forms';
6
+ import { NG_VALUE_ACCESSOR, FormsModule, NgControl, Validators } from '@angular/forms';
7
7
  import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
8
8
  import * as i1 from '@angular/cdk/a11y';
9
9
  import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
@@ -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'];
@@ -2628,6 +2703,36 @@ class TnInputComponent {
2628
2703
  * the a11y tree.
2629
2704
  */
2630
2705
  ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
2706
+ /**
2707
+ * Native `autocomplete` hint rendered on the input/textarea. Pass the standard
2708
+ * autofill tokens (`'username'`, `'current-password'`, `'new-password'`,
2709
+ * `'one-time-code'`, ...) so browsers and password managers can identify and
2710
+ * fill the field.
2711
+ */
2712
+ autocomplete = input(undefined, ...(ngDevMode ? [{ debugName: "autocomplete" }] : []));
2713
+ /**
2714
+ * Native `name` attribute rendered on the input/textarea. Browsers and password
2715
+ * managers use it to identify the field; typically mirrors the form control name.
2716
+ */
2717
+ name = input(undefined, ...(ngDevMode ? [{ debugName: "name" }] : []));
2718
+ /**
2719
+ * Renders the native `readonly` attribute: the value is visible, focusable and
2720
+ * selectable but not editable. Unlike `disabled`, a readonly field stays in the
2721
+ * tab order and its form control stays enabled.
2722
+ */
2723
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : []));
2724
+ /**
2725
+ * Renders the native `required` attribute so browsers and assistive technology
2726
+ * expose the field as required. Validation itself stays with the consumer's form
2727
+ * validators (e.g. `Validators.required`); forms that fully own validation UX
2728
+ * should also set `novalidate` to suppress native submit blocking.
2729
+ *
2730
+ * Note: this drives only the native/a11y semantics. When wrapped in a
2731
+ * `tn-form-field`, the visual `*` indicator is inferred automatically from the
2732
+ * control's `Validators.required`; only validator-less setups need the form
2733
+ * field's own `required` input alongside this one.
2734
+ */
2735
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
2631
2736
  /**
2632
2737
  * Integer/decimal switch — only meaningful when `inputType` is `InputType.Number`.
2633
2738
  *
@@ -2657,12 +2762,27 @@ class TnInputComponent {
2657
2762
  * (e.g. `1.5 GiB`). Ignored unless `inputType` is `Size`.
2658
2763
  */
2659
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" }] : []));
2660
2773
  // Icon inputs
2661
2774
  prefixIcon = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIcon" }] : []));
2662
2775
  prefixIconLibrary = input(undefined, ...(ngDevMode ? [{ debugName: "prefixIconLibrary" }] : []));
2663
2776
  suffixIcon = input(undefined, ...(ngDevMode ? [{ debugName: "suffixIcon" }] : []));
2664
2777
  suffixIconLibrary = input(undefined, ...(ngDevMode ? [{ debugName: "suffixIconLibrary" }] : []));
2665
2778
  suffixIconAriaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "suffixIconAriaLabel" }] : []));
2779
+ /**
2780
+ * Semantic test-id base for the suffix-action button. The library prepends the
2781
+ * element type (`button`) and renders the result under whichever attribute name
2782
+ * is configured via `TN_TEST_ATTR` (default `data-testid`) — e.g.
2783
+ * `suffixActionTestId="toggle-password"` → `button-toggle-password`.
2784
+ */
2785
+ suffixActionTestId = input(undefined, ...(ngDevMode ? [{ debugName: "suffixActionTestId" }] : []));
2666
2786
  onSuffixAction = output();
2667
2787
  hasPrefixIcon = computed(() => !!this.prefixIcon(), ...(ngDevMode ? [{ debugName: "hasPrefixIcon" }] : []));
2668
2788
  hasSuffixIcon = computed(() => !!this.suffixIcon(), ...(ngDevMode ? [{ debugName: "hasSuffixIcon" }] : []));
@@ -2672,8 +2792,28 @@ class TnInputComponent {
2672
2792
  isSize = computed(() => this.inputType() === InputType.Size, ...(ngDevMode ? [{ debugName: "isSize" }] : []));
2673
2793
  /** True when the field only accepts whole numbers (i.e. `allowDecimals` is false). */
2674
2794
  integerOnly = computed(() => !this.allowDecimals(), ...(ngDevMode ? [{ debugName: "integerOnly" }] : []));
2675
- /** 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. */
2676
- 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" }] : []));
2677
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. */
2678
2818
  numericInputMode = computed(() => {
2679
2819
  if (!this.isNumeric()) {
@@ -2683,6 +2823,30 @@ class TnInputComponent {
2683
2823
  }, ...(ngDevMode ? [{ debugName: "numericInputMode" }] : []));
2684
2824
  /** Number and size modes are always single-line; they win over `multiline` if both are set. */
2685
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" }] : []));
2686
2850
  // Unique per instance: a hard-coded id produced duplicate ids when multiple
2687
2851
  // tn-inputs share a page (invalid HTML, and it breaks any future label `for=`,
2688
2852
  // aria-describedby, or getElementById targeting this input).
@@ -2790,6 +2954,9 @@ class TnInputComponent {
2790
2954
  }
2791
2955
  this.onTouched();
2792
2956
  }
2957
+ togglePasswordVisibility() {
2958
+ this.passwordVisible.update((visible) => !visible);
2959
+ }
2793
2960
  /** Strips any character that can't appear in the current numeric mode (single leading '-', single '.' for decimals). */
2794
2961
  sanitizeNumeric(raw) {
2795
2962
  const allowDecimal = !this.integerOnly();
@@ -2818,13 +2985,13 @@ class TnInputComponent {
2818
2985
  return Number.isNaN(parsed) ? null : parsed;
2819
2986
  }
2820
2987
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2821
- 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 }, 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 } }, 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: [
2822
2989
  {
2823
2990
  provide: NG_VALUE_ACCESSOR,
2824
2991
  useExisting: forwardRef(() => TnInputComponent),
2825
2992
  multi: true
2826
2993
  }
2827
- ], 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 [tnTestId]=\"testId()\"\n [disabled]=\"isDisabled()\"\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 [tnTestId]=\"testId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\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 [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"] }] });
2828
2995
  }
2829
2996
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnInputComponent, decorators: [{
2830
2997
  type: Component,
@@ -2834,8 +3001,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2834
3001
  useExisting: forwardRef(() => TnInputComponent),
2835
3002
  multi: true
2836
3003
  }
2837
- ], 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 [tnTestId]=\"testId()\"\n [disabled]=\"isDisabled()\"\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 [tnTestId]=\"testId()\"\n [rows]=\"rows()\"\n [disabled]=\"isDisabled()\"\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 [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"] }]
2838
- }], 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 }] }], 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 }] }], 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"] }] } });
2839
3006
 
2840
3007
  /**
2841
3008
  * Harness for interacting with tn-icon in tests.
@@ -3095,6 +3262,7 @@ class TnInputHarness extends ComponentHarness {
3095
3262
  prefixIconEl = this.locatorForOptional('tn-icon.tn-input__prefix-icon');
3096
3263
  suffixButton = this.locatorForOptional('.tn-input__suffix-action');
3097
3264
  suffixIconEl = this.locatorForOptional(TnIconHarness.with({ ancestor: '.tn-input__suffix-action' }));
3265
+ visibilityToggle = this.locatorForOptional('.tn-input__visibility-toggle');
3098
3266
  /**
3099
3267
  * Gets a `HarnessPredicate` that can be used to search for an input
3100
3268
  * with specific attributes.
@@ -3106,6 +3274,9 @@ class TnInputHarness extends ComponentHarness {
3106
3274
  return new HarnessPredicate(TnInputHarness, options)
3107
3275
  .addOption('placeholder', options.placeholder, async (harness, placeholder) => {
3108
3276
  return (await harness.getPlaceholder()) === placeholder;
3277
+ })
3278
+ .addOption('name', options.name, async (harness, name) => {
3279
+ return (await harness.getName()) === name;
3109
3280
  });
3110
3281
  }
3111
3282
  /**
@@ -3206,6 +3377,58 @@ class TnInputHarness extends ComponentHarness {
3206
3377
  const input = await this.inputEl();
3207
3378
  return input.getAttribute('aria-label');
3208
3379
  }
3380
+ /**
3381
+ * Gets the native `name` attribute (set via the `name` input).
3382
+ *
3383
+ * @returns Promise resolving to the name string, or null if unset.
3384
+ */
3385
+ async getName() {
3386
+ if (await this.isMultiline()) {
3387
+ const textarea = await this.textareaEl();
3388
+ return textarea.getAttribute('name');
3389
+ }
3390
+ const input = await this.inputEl();
3391
+ return input.getAttribute('name');
3392
+ }
3393
+ /**
3394
+ * Gets the native `autocomplete` attribute (set via the `autocomplete` input).
3395
+ *
3396
+ * @returns Promise resolving to the autocomplete token, or null if unset.
3397
+ */
3398
+ async getAutocomplete() {
3399
+ if (await this.isMultiline()) {
3400
+ const textarea = await this.textareaEl();
3401
+ return textarea.getAttribute('autocomplete');
3402
+ }
3403
+ const input = await this.inputEl();
3404
+ return input.getAttribute('autocomplete');
3405
+ }
3406
+ /**
3407
+ * Checks whether the input is readonly (set via the `readonly` input).
3408
+ *
3409
+ * @returns Promise resolving to true if the input is readonly.
3410
+ */
3411
+ async isReadonly() {
3412
+ if (await this.isMultiline()) {
3413
+ const textarea = await this.textareaEl();
3414
+ return (await textarea.getProperty('readOnly')) ?? false;
3415
+ }
3416
+ const input = await this.inputEl();
3417
+ return (await input.getProperty('readOnly')) ?? false;
3418
+ }
3419
+ /**
3420
+ * Checks whether the input is marked required (set via the `required` input).
3421
+ *
3422
+ * @returns Promise resolving to true if the input is required.
3423
+ */
3424
+ async isRequired() {
3425
+ if (await this.isMultiline()) {
3426
+ const textarea = await this.textareaEl();
3427
+ return (await textarea.getProperty('required')) ?? false;
3428
+ }
3429
+ const input = await this.inputEl();
3430
+ return (await input.getProperty('required')) ?? false;
3431
+ }
3209
3432
  /**
3210
3433
  * Gets the placeholder text.
3211
3434
  *
@@ -3298,6 +3521,43 @@ class TnInputHarness extends ComponentHarness {
3298
3521
  }
3299
3522
  return button.click();
3300
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
+ }
3301
3561
  /**
3302
3562
  * Focuses the input element.
3303
3563
  *
@@ -6196,6 +6456,17 @@ function activeErrorKey(errors) {
6196
6456
  class TnFormFieldComponent {
6197
6457
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
6198
6458
  hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
6459
+ /**
6460
+ * Forces the visual `*` required indicator next to the label. Usually
6461
+ * unnecessary: the indicator is inferred automatically when the projected
6462
+ * control carries `Validators.required`. Set this only when inference can't
6463
+ * see the requirement — e.g. a validator wrapped in `Validators.compose(...)`
6464
+ * or a custom validator that emits a `required`-style error.
6465
+ *
6466
+ * The indicator is purely visual — for native/a11y semantics pair it with the
6467
+ * projected control's own `required` input (e.g. `tn-input`'s, which renders
6468
+ * the native attribute).
6469
+ */
6199
6470
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
6200
6471
  testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
6201
6472
  subscriptSizing = input('dynamic', ...(ngDevMode ? [{ debugName: "subscriptSizing" }] : []));
@@ -6227,7 +6498,15 @@ class TnFormFieldComponent {
6227
6498
  invalid: false,
6228
6499
  interacted: false,
6229
6500
  errors: null,
6501
+ required: false,
6230
6502
  }, ...(ngDevMode ? [{ debugName: "controlState" }] : []));
6503
+ /**
6504
+ * Whether the required indicator renders: forced via the `required` input, or
6505
+ * inferred from the projected control's validators (mirrors Angular Material's
6506
+ * `hasValidator(Validators.required)` approach — reference equality, so composed
6507
+ * or custom required-like validators need the explicit input).
6508
+ */
6509
+ showRequired = computed(() => this.required() || this.controlState().required, ...(ngDevMode ? [{ debugName: "showRequired" }] : []));
6231
6510
  hasError = computed(() => {
6232
6511
  const state = this.controlState();
6233
6512
  return state.invalid && state.interacted;
@@ -6260,6 +6539,7 @@ class TnFormFieldComponent {
6260
6539
  invalid: !!control.invalid,
6261
6540
  interacted: !!(control.dirty || control.touched),
6262
6541
  errors: control.errors ?? null,
6542
+ required: !!control.control?.hasValidator(Validators.required),
6263
6543
  });
6264
6544
  }
6265
6545
  }
@@ -6331,11 +6611,11 @@ class TnFormFieldComponent {
6331
6611
  return this.subscriptSizing() === 'fixed' || this.showError() || this.showHint();
6332
6612
  }, ...(ngDevMode ? [{ debugName: "showSubscript" }] : []));
6333
6613
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6334
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFormFieldComponent, isStandalone: true, selector: "tn-form-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, subscriptSizing: { classPropertyName: "subscriptSizing", publicName: "subscriptSizing", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipPosition: { classPropertyName: "tooltipPosition", publicName: "tooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-form-field\" tnTestIdType=\"form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label() || tooltip()) {\n <div class=\"tn-form-field-label-row\">\n @if (label()) {\n <label class=\"tn-form-field-label\" [class.required]=\"required()\">\n {{ label() }}\n @if (required()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n </label>\n }\n @if (tooltip()) {\n <button\n type=\"button\"\n class=\"tn-form-field-tooltip\"\n [attr.aria-label]=\"tooltip()\"\n [tnTooltip]=\"tooltip()\"\n [tnTooltipPosition]=\"tooltipPosition()\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" />\n </button>\n }\n </div>\n }\n\n <!-- Form Control Content -->\n <div class=\"tn-form-field-wrapper\">\n <ng-content />\n </div>\n\n <!-- Hint or Error Message -->\n @if (showSubscript()) {\n <div class=\"tn-form-field-subscript\" [class.tn-form-field-subscript-dynamic]=\"subscriptSizing() === 'dynamic'\">\n @if (showError()) {\n <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n aria-live=\"polite\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\">\n {{ hint() }}\n </div>\n }\n </div>\n }\n</div>\n", styles: [".tn-form-field{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label-row{display:flex;align-items:center;gap:.375rem;margin-bottom:.5rem}.tn-form-field-label{display:block;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-form-field-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-form-field-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-form-field-tooltip:hover,.tn-form-field-tooltip:focus-visible{color:var(--tn-primary, #007bff)}.tn-form-field-tooltip:focus-visible{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;border-radius:50%}.tn-form-field-wrapper{position:relative;width:100%;overflow:visible}.tn-form-field-wrapper :ng-deep .tn-select-container,.tn-form-field-wrapper :ng-deep .tn-input-container{margin-bottom:0}.tn-form-field-wrapper :ng-deep .tn-select-label,.tn-form-field-wrapper :ng-deep .tn-input-label{display:none}.tn-form-field-wrapper :ng-deep .tn-select-error,.tn-form-field-wrapper :ng-deep .tn-input-error{display:none}.tn-form-field-wrapper :ng-deep .tn-select-dropdown{z-index:1000}.tn-form-field-subscript{min-height:1.25rem;margin-top:.25rem;font-size:.75rem;line-height:1.4}.tn-form-field-subscript-dynamic{min-height:0}.tn-form-field-error{color:var(--tn-error, #dc3545);margin:0}.tn-form-field-hint{color:var(--tn-fg2, #6c757d);margin:0}.tn-form-field-wrapper:has(:focus-visible) .tn-form-field-label{color:var(--tn-primary, #007bff)}.tn-form-field-wrapper:has(.error) .tn-form-field-label{color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-form-field-label{transition:none}}@media(prefers-contrast:high){.tn-form-field-label,.tn-form-field-error{font-weight:600}}\n"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
6614
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFormFieldComponent, isStandalone: true, selector: "tn-form-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, subscriptSizing: { classPropertyName: "subscriptSizing", publicName: "subscriptSizing", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipPosition: { classPropertyName: "tooltipPosition", publicName: "tooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-form-field\" tnTestIdType=\"form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label() || tooltip()) {\n <div class=\"tn-form-field-label-row\">\n @if (label()) {\n <label class=\"tn-form-field-label\" [class.required]=\"showRequired()\">\n {{ label() }}\n @if (showRequired()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n </label>\n }\n @if (tooltip()) {\n <button\n type=\"button\"\n class=\"tn-form-field-tooltip\"\n [attr.aria-label]=\"tooltip()\"\n [tnTooltip]=\"tooltip()\"\n [tnTooltipPosition]=\"tooltipPosition()\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" />\n </button>\n }\n </div>\n }\n\n <!-- Form Control Content -->\n <div class=\"tn-form-field-wrapper\">\n <ng-content />\n </div>\n\n <!-- Hint or Error Message -->\n @if (showSubscript()) {\n <div class=\"tn-form-field-subscript\" [class.tn-form-field-subscript-dynamic]=\"subscriptSizing() === 'dynamic'\">\n @if (showError()) {\n <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n aria-live=\"polite\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\">\n {{ hint() }}\n </div>\n }\n </div>\n }\n</div>\n", styles: [".tn-form-field{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label-row{display:flex;align-items:center;gap:.375rem;margin-bottom:.5rem}.tn-form-field-label{display:block;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-form-field-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-form-field-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-form-field-tooltip:hover,.tn-form-field-tooltip:focus-visible{color:var(--tn-primary, #007bff)}.tn-form-field-tooltip:focus-visible{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;border-radius:50%}.tn-form-field-wrapper{position:relative;width:100%;overflow:visible}.tn-form-field-wrapper :ng-deep .tn-select-container,.tn-form-field-wrapper :ng-deep .tn-input-container{margin-bottom:0}.tn-form-field-wrapper :ng-deep .tn-select-label,.tn-form-field-wrapper :ng-deep .tn-input-label{display:none}.tn-form-field-wrapper :ng-deep .tn-select-error,.tn-form-field-wrapper :ng-deep .tn-input-error{display:none}.tn-form-field-wrapper :ng-deep .tn-select-dropdown{z-index:1000}.tn-form-field-subscript{min-height:1.25rem;margin-top:.25rem;font-size:.75rem;line-height:1.4}.tn-form-field-subscript-dynamic{min-height:0}.tn-form-field-error{color:var(--tn-error, #dc3545);margin:0}.tn-form-field-hint{color:var(--tn-fg2, #6c757d);margin:0}.tn-form-field-wrapper:has(:focus-visible) .tn-form-field-label{color:var(--tn-primary, #007bff)}.tn-form-field-wrapper:has(.error) .tn-form-field-label{color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-form-field-label{transition:none}}@media(prefers-contrast:high){.tn-form-field-label,.tn-form-field-error{font-weight:600}}\n"], dependencies: [{ kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
6335
6615
  }
6336
6616
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, decorators: [{
6337
6617
  type: Component,
6338
- args: [{ selector: 'tn-form-field', standalone: true, imports: [TnTestIdDirective, TnIconComponent, TnTooltipDirective], template: "<div class=\"tn-form-field\" tnTestIdType=\"form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label() || tooltip()) {\n <div class=\"tn-form-field-label-row\">\n @if (label()) {\n <label class=\"tn-form-field-label\" [class.required]=\"required()\">\n {{ label() }}\n @if (required()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n </label>\n }\n @if (tooltip()) {\n <button\n type=\"button\"\n class=\"tn-form-field-tooltip\"\n [attr.aria-label]=\"tooltip()\"\n [tnTooltip]=\"tooltip()\"\n [tnTooltipPosition]=\"tooltipPosition()\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" />\n </button>\n }\n </div>\n }\n\n <!-- Form Control Content -->\n <div class=\"tn-form-field-wrapper\">\n <ng-content />\n </div>\n\n <!-- Hint or Error Message -->\n @if (showSubscript()) {\n <div class=\"tn-form-field-subscript\" [class.tn-form-field-subscript-dynamic]=\"subscriptSizing() === 'dynamic'\">\n @if (showError()) {\n <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n aria-live=\"polite\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\">\n {{ hint() }}\n </div>\n }\n </div>\n }\n</div>\n", styles: [".tn-form-field{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label-row{display:flex;align-items:center;gap:.375rem;margin-bottom:.5rem}.tn-form-field-label{display:block;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-form-field-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-form-field-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-form-field-tooltip:hover,.tn-form-field-tooltip:focus-visible{color:var(--tn-primary, #007bff)}.tn-form-field-tooltip:focus-visible{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;border-radius:50%}.tn-form-field-wrapper{position:relative;width:100%;overflow:visible}.tn-form-field-wrapper :ng-deep .tn-select-container,.tn-form-field-wrapper :ng-deep .tn-input-container{margin-bottom:0}.tn-form-field-wrapper :ng-deep .tn-select-label,.tn-form-field-wrapper :ng-deep .tn-input-label{display:none}.tn-form-field-wrapper :ng-deep .tn-select-error,.tn-form-field-wrapper :ng-deep .tn-input-error{display:none}.tn-form-field-wrapper :ng-deep .tn-select-dropdown{z-index:1000}.tn-form-field-subscript{min-height:1.25rem;margin-top:.25rem;font-size:.75rem;line-height:1.4}.tn-form-field-subscript-dynamic{min-height:0}.tn-form-field-error{color:var(--tn-error, #dc3545);margin:0}.tn-form-field-hint{color:var(--tn-fg2, #6c757d);margin:0}.tn-form-field-wrapper:has(:focus-visible) .tn-form-field-label{color:var(--tn-primary, #007bff)}.tn-form-field-wrapper:has(.error) .tn-form-field-label{color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-form-field-label{transition:none}}@media(prefers-contrast:high){.tn-form-field-label,.tn-form-field-error{font-weight:600}}\n"] }]
6618
+ args: [{ selector: 'tn-form-field', standalone: true, imports: [TnTestIdDirective, TnIconComponent, TnTooltipDirective], template: "<div class=\"tn-form-field\" tnTestIdType=\"form-field\" [tnTestId]=\"testId()\">\n <!-- Label -->\n @if (label() || tooltip()) {\n <div class=\"tn-form-field-label-row\">\n @if (label()) {\n <label class=\"tn-form-field-label\" [class.required]=\"showRequired()\">\n {{ label() }}\n @if (showRequired()) {\n <span class=\"required-asterisk\" aria-label=\"required\">*</span>\n }\n </label>\n }\n @if (tooltip()) {\n <button\n type=\"button\"\n class=\"tn-form-field-tooltip\"\n [attr.aria-label]=\"tooltip()\"\n [tnTooltip]=\"tooltip()\"\n [tnTooltipPosition]=\"tooltipPosition()\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" />\n </button>\n }\n </div>\n }\n\n <!-- Form Control Content -->\n <div class=\"tn-form-field-wrapper\">\n <ng-content />\n </div>\n\n <!-- Hint or Error Message -->\n @if (showSubscript()) {\n <div class=\"tn-form-field-subscript\" [class.tn-form-field-subscript-dynamic]=\"subscriptSizing() === 'dynamic'\">\n @if (showError()) {\n <div\n class=\"tn-form-field-error\"\n role=\"alert\"\n aria-live=\"polite\">\n {{ errorMessage() }}\n </div>\n }\n @if (showHint()) {\n <div class=\"tn-form-field-hint\">\n {{ hint() }}\n </div>\n }\n </div>\n }\n</div>\n", styles: [".tn-form-field{display:block;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label-row{display:flex;align-items:center;gap:.375rem;margin-bottom:.5rem}.tn-form-field-label{display:block;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-form-field-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-form-field-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-form-field-tooltip:hover,.tn-form-field-tooltip:focus-visible{color:var(--tn-primary, #007bff)}.tn-form-field-tooltip:focus-visible{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;border-radius:50%}.tn-form-field-wrapper{position:relative;width:100%;overflow:visible}.tn-form-field-wrapper :ng-deep .tn-select-container,.tn-form-field-wrapper :ng-deep .tn-input-container{margin-bottom:0}.tn-form-field-wrapper :ng-deep .tn-select-label,.tn-form-field-wrapper :ng-deep .tn-input-label{display:none}.tn-form-field-wrapper :ng-deep .tn-select-error,.tn-form-field-wrapper :ng-deep .tn-input-error{display:none}.tn-form-field-wrapper :ng-deep .tn-select-dropdown{z-index:1000}.tn-form-field-subscript{min-height:1.25rem;margin-top:.25rem;font-size:.75rem;line-height:1.4}.tn-form-field-subscript-dynamic{min-height:0}.tn-form-field-error{color:var(--tn-error, #dc3545);margin:0}.tn-form-field-hint{color:var(--tn-fg2, #6c757d);margin:0}.tn-form-field-wrapper:has(:focus-visible) .tn-form-field-label{color:var(--tn-primary, #007bff)}.tn-form-field-wrapper:has(.error) .tn-form-field-label{color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-form-field-label{transition:none}}@media(prefers-contrast:high){.tn-form-field-label,.tn-form-field-error{font-weight:600}}\n"] }]
6339
6619
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], subscriptSizing: [{ type: i0.Input, args: [{ isSignal: true, alias: "subscriptSizing", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipPosition", required: false }] }], errorMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessages", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { isSignal: true }] }] } });
6340
6620
 
6341
6621
  /**
@@ -7378,81 +7658,6 @@ class TnSelectHarness extends ComponentHarness {
7378
7658
  }
7379
7659
  }
7380
7660
 
7381
- /**
7382
- * Marks an icon name for inclusion in the TrueNAS UI sprite generation.
7383
- *
7384
- * This function serves two purposes:
7385
- * 1. At runtime: Applies library-specific prefixes (e.g., mdi-, app-)
7386
- * 2. At build time: Marker for the scanner to detect icons for sprite inclusion
7387
- *
7388
- * Use this when icon names are computed dynamically or come from variables,
7389
- * to ensure they're included in the sprite at build time.
7390
- *
7391
- * @example
7392
- * // Static icon name - automatically detected from template
7393
- * <tn-icon name="folder"></tn-icon>
7394
- *
7395
- * @example
7396
- * // Dynamic MDI icon
7397
- * const iconName = condition ? tnIconMarker("pencil", "mdi") : tnIconMarker("delete", "mdi");
7398
- * <tn-icon [name]="iconName"></tn-icon>
7399
- *
7400
- * @example
7401
- * // Dynamic custom icon (consumer's own icon)
7402
- * const logo = tnIconMarker("your-logo-name", "custom");
7403
- * <tn-icon [name]="logo"></tn-icon>
7404
- *
7405
- * @example
7406
- * // Array of dynamic icons
7407
- * const actions = [
7408
- * { name: "Save", icon: tnIconMarker("content-save", "mdi") },
7409
- * { name: "Cancel", icon: tnIconMarker("close", "mdi") }
7410
- * ];
7411
- *
7412
- * @param iconName - The icon name to mark for sprite inclusion
7413
- * @param library - Optional library type: 'mdi', 'material', or 'custom'
7414
- * @returns The icon name with appropriate prefix applied
7415
- * @public
7416
- */
7417
- function tnIconMarker(iconName, library) {
7418
- // Apply library-specific prefixes
7419
- if (library === 'mdi' && !iconName.startsWith('mdi-')) {
7420
- return `mdi-${iconName}`;
7421
- }
7422
- if (library === 'custom' && !iconName.startsWith('app-')) {
7423
- return `app-${iconName}`;
7424
- }
7425
- if (library === 'material' && !iconName.startsWith('mat-')) {
7426
- return `mat-${iconName}`;
7427
- }
7428
- return iconName;
7429
- }
7430
- /**
7431
- * INTERNAL LIBRARY USE ONLY
7432
- *
7433
- * Marks an icon name for inclusion in the sprite generation with library namespace.
7434
- * This function MUST be used by library component code for custom icons.
7435
- *
7436
- * The TypeScript type enforces that the icon name starts with 'tn-' prefix,
7437
- * which reserves this namespace exclusively for library-provided custom icons.
7438
- *
7439
- * @example
7440
- * ```typescript
7441
- * // ✅ Correct - Library component code
7442
- * const icon = libIconMarker('tn-dataset');
7443
- *
7444
- * // ❌ Wrong - Will cause TypeScript error
7445
- * const icon = libIconMarker('dataset');
7446
- * ```
7447
- *
7448
- * @param iconName - The icon name with 'tn-' prefix (enforced by TypeScript)
7449
- * @returns The same icon name (identity function)
7450
- * @internal
7451
- */
7452
- function libIconMarker(iconName) {
7453
- return iconName;
7454
- }
7455
-
7456
7661
  /// <reference types="jest" />
7457
7662
  /**
7458
7663
  * Creates default mock implementation of TnSpriteLoaderService.