@truenas/ui-components 0.1.45 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/fesm2022/truenas-ui-components.mjs +78 -4
  2. package/fesm2022/truenas-ui-components.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/scripts/icon-sprite/__fixtures__/consumer-templates/basic.component.html +30 -0
  5. package/scripts/icon-sprite/__fixtures__/custom-icons/brand.svg +1 -0
  6. package/scripts/icon-sprite/__fixtures__/custom-icons/logo.svg +1 -0
  7. package/scripts/icon-sprite/__fixtures__/forwarding-components/multi-icon.component.html +7 -0
  8. package/scripts/icon-sprite/__fixtures__/forwarding-components/multi-icon.component.ts +15 -0
  9. package/scripts/icon-sprite/__fixtures__/forwarding-components/no-library.component.html +3 -0
  10. package/scripts/icon-sprite/__fixtures__/forwarding-components/no-library.component.ts +11 -0
  11. package/scripts/icon-sprite/__fixtures__/forwarding-components/not-forwarding.component.html +1 -0
  12. package/scripts/icon-sprite/__fixtures__/forwarding-components/not-forwarding.component.ts +10 -0
  13. package/scripts/icon-sprite/__fixtures__/forwarding-components/single-icon.component.html +3 -0
  14. package/scripts/icon-sprite/__fixtures__/forwarding-components/single-icon.component.ts +13 -0
  15. package/scripts/icon-sprite/__fixtures__/marker-sources/icons.ts +24 -0
  16. package/scripts/icon-sprite/cli-main.ts +27 -8
  17. package/scripts/icon-sprite/generate-sprite.ts +15 -4
  18. package/scripts/icon-sprite/jest.config.ts +14 -0
  19. package/scripts/icon-sprite/lib/find-icons-in-forwarding-components.spec.ts +77 -0
  20. package/scripts/icon-sprite/lib/find-icons-in-forwarding-components.ts +209 -0
  21. package/scripts/icon-sprite/lib/find-icons-in-templates.spec.ts +119 -0
  22. package/scripts/icon-sprite/lib/find-icons-in-templates.ts +66 -17
  23. package/scripts/icon-sprite/lib/find-icons-with-marker.spec.ts +58 -0
  24. package/scripts/icon-sprite/lib/find-icons-with-marker.ts +37 -22
  25. package/scripts/icon-sprite/lib/validate-icons.spec.ts +170 -0
  26. package/scripts/icon-sprite/lib/validate-icons.ts +185 -0
  27. package/scripts/icon-sprite/tsconfig.json +14 -0
  28. package/types/truenas-ui-components.d.ts +70 -5
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, input, output, viewChild, signal, computed, effect, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, ViewEncapsulation, Directive, contentChildren, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, DestroyRef, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
2
+ import { inject, ElementRef, input, output, viewChild, signal, computed, effect, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, Directive, contentChildren, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, DestroyRef, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
3
3
  import * as i1$4 from '@angular/forms';
4
4
  import { NG_VALUE_ACCESSOR, FormsModule, NgControl } from '@angular/forms';
5
5
  import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
@@ -1062,6 +1062,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1062
1062
  }], ctorParameters: () => [{ type: i1$1.DomSanitizer }, { type: TnSpriteLoaderService }] });
1063
1063
 
1064
1064
  class TnIconComponent {
1065
+ static warnedIcons = new Set();
1065
1066
  name = input('', ...(ngDevMode ? [{ debugName: "name" }] : []));
1066
1067
  size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
1067
1068
  color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : []));
@@ -1165,6 +1166,8 @@ class TnIconComponent {
1165
1166
  if (registryResult) {
1166
1167
  return registryResult;
1167
1168
  }
1169
+ // Warn in dev mode when a sprite-prefixed icon is missing from the sprite
1170
+ this.warnMissingSpriteIcon(effectiveIconName, name, library);
1168
1171
  // 2. Try built-in third-party patterns (deprecated - use registry instead)
1169
1172
  const thirdPartyResult = this.tryThirdPartyIcon(effectiveIconName);
1170
1173
  if (thirdPartyResult) {
@@ -1266,6 +1269,39 @@ class TnIconComponent {
1266
1269
  // Default to first 2 characters
1267
1270
  return name.substring(0, 2).toUpperCase();
1268
1271
  }
1272
+ /**
1273
+ * Warns once per icon name in dev mode when a sprite-prefixed icon is not
1274
+ * found in the sprite. Only fires when the sprite is loaded and the icon
1275
+ * name has a prefix indicating it should be a sprite icon.
1276
+ */
1277
+ warnMissingSpriteIcon(effectiveIconName, originalName, library) {
1278
+ if (!isDevMode()) {
1279
+ return;
1280
+ }
1281
+ // Only warn for icons that look like they should be in the sprite
1282
+ const isSpriteIcon = /^(mdi-|mat-|tn-|app-)/.test(effectiveIconName);
1283
+ if (!isSpriteIcon) {
1284
+ return;
1285
+ }
1286
+ // Don't warn if sprite hasn't loaded yet (would be a false positive)
1287
+ if (!this.iconRegistry.getSpriteLoader().isSpriteLoaded()) {
1288
+ return;
1289
+ }
1290
+ // Deduplicate: warn once per icon name per session
1291
+ if (TnIconComponent.warnedIcons.has(effectiveIconName)) {
1292
+ return;
1293
+ }
1294
+ TnIconComponent.warnedIcons.add(effectiveIconName);
1295
+ const lib = library || (effectiveIconName.startsWith('mdi-') ? 'mdi' : effectiveIconName.startsWith('mat-') ? 'material' : undefined);
1296
+ // Build the marker example without a literal tnIconMarker() call,
1297
+ // so the grep-based sprite scanner doesn't pick this up as an actual icon.
1298
+ const markerFn = 'tnIcon' + 'Marker';
1299
+ const markerExample = lib
1300
+ ? `${markerFn}('${originalName}', '${lib}')`
1301
+ : `${markerFn}('${originalName}')`;
1302
+ console.warn(`[TrueNAS UI] Icon '${effectiveIconName}' not found in sprite. ` +
1303
+ `To include it, add ${markerExample} to your source and run 'npx truenas-icons generate'.`);
1304
+ }
1269
1305
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1270
1306
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnIconComponent, isStandalone: true, selector: "tn-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, library: { classPropertyName: "library", publicName: "library", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null }, customSize: { classPropertyName: "customSize", publicName: "customSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.name": "name()", "attr.library": "library()", "attr.size": "size()", "attr.color": "color()", "attr.full-size": "fullSize() || null", "attr.custom-size": "customSize() || null", "style.width": "hostDimension()", "style.height": "hostDimension()", "style.font-size": "hostFontSize()", "style.color": "color() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: ["tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;color:var(--tn-icon-color, currentColor);width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));font-size:var(--tn-icon-size, var(--tn-icon-md))}tn-icon[size=xs]{width:var(--tn-icon-xs);height:var(--tn-icon-xs);font-size:var(--tn-icon-xs)}tn-icon[size=sm]{width:var(--tn-icon-sm);height:var(--tn-icon-sm);font-size:var(--tn-icon-sm)}tn-icon[size=md]{width:var(--tn-icon-md);height:var(--tn-icon-md);font-size:var(--tn-icon-md)}tn-icon[size=lg]{width:var(--tn-icon-lg);height:var(--tn-icon-lg);font-size:var(--tn-icon-lg)}tn-icon[size=xl]{width:var(--tn-icon-xl);height:var(--tn-icon-xl);font-size:var(--tn-icon-xl)}.tn-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1271
1307
  }
@@ -4645,6 +4681,7 @@ class TnFormFieldComponent {
4645
4681
  hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
4646
4682
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
4647
4683
  testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
4684
+ subscriptSizing = input('dynamic', ...(ngDevMode ? [{ debugName: "subscriptSizing" }] : []));
4648
4685
  control = contentChild(NgControl, ...(ngDevMode ? [{ debugName: "control" }] : []));
4649
4686
  hasError = signal(false, ...(ngDevMode ? [{ debugName: "hasError" }] : []));
4650
4687
  errorMessage = signal('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : []));
@@ -4708,13 +4745,16 @@ class TnFormFieldComponent {
4708
4745
  showHint = computed(() => {
4709
4746
  return !!this.hint() && !this.showError();
4710
4747
  }, ...(ngDevMode ? [{ debugName: "showHint" }] : []));
4748
+ showSubscript = computed(() => {
4749
+ return this.subscriptSizing() === 'fixed' || this.showError() || this.showHint();
4750
+ }, ...(ngDevMode ? [{ debugName: "showSubscript" }] : []));
4711
4751
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4712
- 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 } }, queries: [{ propertyName: "control", first: true, predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-form-field\" [attr.data-testid]=\"testId()\">\n <!-- Label -->\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\n <!-- Form Control Content -->\n <div class=\"tn-form-field-wrapper\">\n <ng-content />\n </div>\n\n <!-- Hint or Error Message -->\n <div class=\"tn-form-field-subscript\">\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</div>", styles: [".tn-form-field{display:block;width:100%;margin-bottom:1rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label{display:block;margin-bottom:.5rem;font-size:.875rem;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-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-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"] });
4752
+ 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 } }, queries: [{ propertyName: "control", first: true, predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-form-field\" [attr.data-testid]=\"testId()\">\n <!-- Label -->\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\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{display:block;margin-bottom:.5rem;font-size:.875rem;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-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"] });
4713
4753
  }
4714
4754
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFormFieldComponent, decorators: [{
4715
4755
  type: Component,
4716
- args: [{ selector: 'tn-form-field', standalone: true, imports: [], template: "<div class=\"tn-form-field\" [attr.data-testid]=\"testId()\">\n <!-- Label -->\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\n <!-- Form Control Content -->\n <div class=\"tn-form-field-wrapper\">\n <ng-content />\n </div>\n\n <!-- Hint or Error Message -->\n <div class=\"tn-form-field-subscript\">\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</div>", styles: [".tn-form-field{display:block;width:100%;margin-bottom:1rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-form-field-label{display:block;margin-bottom:.5rem;font-size:.875rem;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-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-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"] }]
4717
- }], 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 }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { isSignal: true }] }] } });
4756
+ args: [{ selector: 'tn-form-field', standalone: true, imports: [], template: "<div class=\"tn-form-field\" [attr.data-testid]=\"testId()\">\n <!-- Label -->\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\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{display:block;margin-bottom:.5rem;font-size:.875rem;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-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"] }]
4757
+ }], 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 }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { isSignal: true }] }] } });
4718
4758
 
4719
4759
  /**
4720
4760
  * Harness for interacting with `tn-form-field` in tests.
@@ -4871,6 +4911,40 @@ class TnFormFieldHarness extends ComponentHarness {
4871
4911
  const root = await this.locatorFor('.tn-form-field')();
4872
4912
  return root.getAttribute('data-testid');
4873
4913
  }
4914
+ /**
4915
+ * Checks whether the subscript wrapper is currently rendered.
4916
+ *
4917
+ * @returns Promise resolving to true if the subscript area is present in the DOM.
4918
+ *
4919
+ * @example
4920
+ * ```typescript
4921
+ * const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Name' }));
4922
+ * expect(await field.hasSubscript()).toBe(false);
4923
+ * ```
4924
+ */
4925
+ async hasSubscript() {
4926
+ const subscript = await this.locatorForOptional('.tn-form-field-subscript')();
4927
+ return subscript !== null;
4928
+ }
4929
+ /**
4930
+ * Gets the current subscript sizing mode.
4931
+ *
4932
+ * @returns Promise resolving to `'fixed'` or `'dynamic'`.
4933
+ *
4934
+ * @example
4935
+ * ```typescript
4936
+ * const field = await loader.getHarness(TnFormFieldHarness.with({ label: 'Name' }));
4937
+ * expect(await field.getSubscriptSizing()).toBe('dynamic');
4938
+ * ```
4939
+ */
4940
+ async getSubscriptSizing() {
4941
+ const subscript = await this.locatorForOptional('.tn-form-field-subscript')();
4942
+ if (!subscript) {
4943
+ return 'dynamic';
4944
+ }
4945
+ const isDynamic = await subscript.hasClass('tn-form-field-subscript-dynamic');
4946
+ return isDynamic ? 'dynamic' : 'fixed';
4947
+ }
4874
4948
  }
4875
4949
 
4876
4950
  class TnSelectComponent {