@truenas/ui-components 0.3.18 → 0.3.20

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.
@@ -2199,7 +2199,11 @@ class TnBannerHarness extends ComponentHarness {
2199
2199
  */
2200
2200
  static with(options = {}) {
2201
2201
  return new HarnessPredicate(TnBannerHarness, options)
2202
- .addOption('textContains', options.textContains, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
2202
+ .addOption('textContains', options.textContains, (harness, text) => HarnessPredicate.stringMatches(harness.getText(),
2203
+ // strings trigger exact matching in `stringMatches`, but since we call the option
2204
+ // `textContains`, this would be misleading. here, we convert strings to a Regex
2205
+ // to trigger partial matching behavior on `stringMatches`.
2206
+ typeof text === 'string' ? new RegExp(helperEscapeRegex(text)) : text));
2203
2207
  }
2204
2208
  /**
2205
2209
  * Gets all text content from the banner (heading + message combined).
@@ -2218,6 +2222,15 @@ class TnBannerHarness extends ComponentHarness {
2218
2222
  return (await host.text()).trim();
2219
2223
  }
2220
2224
  }
2225
+ /**
2226
+ * helper function to stand-in for `RegExp.escape`, since that doesn't
2227
+ * exist in our deployment target of ES2023.
2228
+ * @param text a string to escape.
2229
+ * @returns an escaped string, safe for using in the `RegExp` constructor.
2230
+ */
2231
+ function helperEscapeRegex(text) {
2232
+ return text.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
2233
+ }
2221
2234
 
2222
2235
  const escapableChars = new Set(['*', '`', '\\']);
2223
2236
  function isWhitespace(char) {
@@ -8684,6 +8697,9 @@ class TnFormSectionHarness extends ComponentHarness {
8684
8697
  }
8685
8698
  }
8686
8699
 
8700
+ // Minimum gap the dropdown panel keeps from each viewport edge. Also derives
8701
+ // the pane's maxWidth (viewport minus both margins) — see attachOverlay().
8702
+ const VIEWPORT_MARGIN_PX = 8;
8687
8703
  class TnSelectComponent {
8688
8704
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
8689
8705
  optionGroups = input([], ...(ngDevMode ? [{ debugName: "optionGroups" }] : []));
@@ -8952,18 +8968,39 @@ class TnSelectComponent {
8952
8968
  */
8953
8969
  attachOverlay() {
8954
8970
  const trigger = this.triggerEl().nativeElement;
8971
+ // Flexible dimensions must stay OFF: with them on, CDK treats a panel
8972
+ // squeezed against the viewport edge as a valid "flexible fit" (no
8973
+ // minWidth is set, so any width fits), never falls through to the
8974
+ // end-aligned positions or push, and the panel content gets clipped at
8975
+ // the screen edge. Rigid dimensions + push keep the panel fully visible;
8976
+ // height is already capped by the panel's own max-height.
8955
8977
  const positionStrategy = this.overlay
8956
8978
  .position()
8957
8979
  .flexibleConnectedTo(trigger)
8980
+ .withFlexibleDimensions(false)
8981
+ .withPush(true)
8982
+ // Push can only rescue a panel narrower than the narrowed viewport
8983
+ // (clientWidth - 2 * margin); the pane maxWidth below covers the
8984
+ // wider-than-viewport case.
8985
+ .withViewportMargin(VIEWPORT_MARGIN_PX)
8958
8986
  .withPositions([
8959
8987
  { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
8960
8988
  { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
8989
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
8990
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
8961
8991
  ]);
8962
8992
  this.overlayRef = this.overlay.create({
8963
8993
  positionStrategy,
8964
8994
  scrollStrategy: this.overlay.scrollStrategies.reposition(),
8965
8995
  hasBackdrop: true,
8966
8996
  backdropClass: 'cdk-overlay-transparent-backdrop',
8997
+ // Cap the pane so option labels wrap instead of clipping when they are
8998
+ // wider than the viewport. Percentage, not 100vw: the pane resolves %
8999
+ // against the overlay container, which CDK sizes to clientWidth
9000
+ // (scrollbar excluded) — the same base its position math uses — so the
9001
+ // capped pane always fits the push viewport exactly. 100vw includes the
9002
+ // scrollbar and would overshoot by its width.
9003
+ maxWidth: `calc(100% - ${2 * VIEWPORT_MARGIN_PX}px)`,
8967
9004
  });
8968
9005
  const portal = new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef);
8969
9006
  this.overlayRef.attach(portal);
@@ -9266,7 +9303,7 @@ class TnSelectComponent {
9266
9303
  useExisting: forwardRef(() => TnSelectComponent),
9267
9304
  multi: true
9268
9305
  }
9269
- ], viewQueries: [{ propertyName: "triggerEl", first: true, predicate: ["triggerEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || displayOptions().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, #dc3545) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height)}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"], dependencies: [{ kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9306
+ ], viewQueries: [{ propertyName: "triggerEl", first: true, predicate: ["triggerEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || displayOptions().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, #dc3545) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height)}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;overflow-wrap:anywhere;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"], dependencies: [{ kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9270
9307
  }
9271
9308
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, decorators: [{
9272
9309
  type: Component,
@@ -9276,7 +9313,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
9276
9313
  useExisting: forwardRef(() => TnSelectComponent),
9277
9314
  multi: true
9278
9315
  }
9279
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || displayOptions().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, #dc3545) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height)}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
9316
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-select-container\">\n <!-- Select Trigger -->\n <div\n #triggerEl\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tnTestIdType=\"select\"\n [tnTestId]=\"resolvedTestId()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + idNamespace() : null\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-activedescendant]=\"isOpen() ? focusedOptionId() : null\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. The trigger keeps DOM focus throughout (combobox\n pattern); options use aria-activedescendant for virtual focus. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + idNamespace()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options (preceded by the synthetic empty option when allowEmpty is set) -->\n @for (option of displayOptions(); track $index) {\n <!-- Option activation lives on the trigger via aria-activedescendant; options have tabindex=\"-1\" and never receive DOM focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [class.tn-select-empty-option]=\"isEmptyOption(option)\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || displayOptions().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + idNamespace() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + idNamespace() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [attr.id]=\"optionId(option)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.focused]=\"isOptionFocused(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:1rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-error, #dc3545) 25%,transparent)}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{--tn-select-dropdown-max-height: 350px;background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-select-dropdown-max-height)}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;overflow-wrap:anywhere;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg2, #e8f4fd);box-shadow:inset 2px 0 0 var(--tn-primary, #007bff)}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-option.tn-select-empty-option{color:var(--tn-fg2, #6c757d)}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
9280
9317
  }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], noOptionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noOptionsLabel", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], optionTestIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTestIdKey", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }], triggerEl: [{ type: i0.ViewChild, args: ['triggerEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
9281
9318
 
9282
9319
  /**