@truenas/ui-components 0.1.30 → 0.1.32

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,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, IterableDiffers, Pipe, PLATFORM_ID, DestroyRef } 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, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID, DestroyRef } 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';
@@ -4333,10 +4333,15 @@ class TnSelectComponent {
4333
4333
  placeholder = input('Select an option', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
4334
4334
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
4335
4335
  testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
4336
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
4337
+ compareWith = input(...(ngDevMode ? [undefined, { debugName: "compareWith" }] : []));
4336
4338
  selectionChange = output();
4339
+ /** Emits the full array of selected values after each toggle in multiple mode. */
4340
+ multiSelectionChange = output();
4337
4341
  // Internal state signals
4338
4342
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
4339
4343
  selectedValue = signal(null, ...(ngDevMode ? [{ debugName: "selectedValue" }] : []));
4344
+ selectedValues = signal([], ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
4340
4345
  formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
4341
4346
  // Computed disabled state (combines input and form state)
4342
4347
  isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
@@ -4367,7 +4372,17 @@ class TnSelectComponent {
4367
4372
  }
4368
4373
  // ControlValueAccessor implementation
4369
4374
  writeValue(value) {
4370
- this.selectedValue.set(value);
4375
+ if (this.multiple()) {
4376
+ if (Array.isArray(value)) {
4377
+ this.selectedValues.set(value);
4378
+ }
4379
+ else {
4380
+ this.selectedValues.set(value != null ? [value] : []);
4381
+ }
4382
+ }
4383
+ else {
4384
+ this.selectedValue.set(Array.isArray(value) ? value[0] ?? null : value);
4385
+ }
4371
4386
  }
4372
4387
  registerOnChange(fn) {
4373
4388
  this.onChange = fn;
@@ -4392,8 +4407,16 @@ class TnSelectComponent {
4392
4407
  this.isOpen.set(false);
4393
4408
  this.onTouched();
4394
4409
  }
4395
- onOptionClick(option) {
4396
- this.selectOption(option);
4410
+ onOptionClick(option, groupDisabled = false) {
4411
+ if (option.disabled || groupDisabled) {
4412
+ return;
4413
+ }
4414
+ if (this.multiple()) {
4415
+ this.toggleOption(option);
4416
+ }
4417
+ else {
4418
+ this.selectOption(option);
4419
+ }
4397
4420
  }
4398
4421
  selectOption(option) {
4399
4422
  if (option.disabled) {
@@ -4403,19 +4426,46 @@ class TnSelectComponent {
4403
4426
  this.onChange(option.value);
4404
4427
  this.selectionChange.emit(option.value);
4405
4428
  this.closeDropdown();
4406
- this.cdr.markForCheck(); // Trigger change detection
4429
+ this.cdr.markForCheck();
4407
4430
  }
4408
- // Computed properties
4409
- isSelected = computed(() => (option) => {
4431
+ toggleOption(option) {
4432
+ const current = this.selectedValues();
4433
+ const index = current.findIndex(v => this.compareValues(v, option.value));
4434
+ let updated;
4435
+ if (index >= 0) {
4436
+ updated = current.filter((_, i) => i !== index);
4437
+ }
4438
+ else {
4439
+ updated = [...current, option.value];
4440
+ }
4441
+ this.selectedValues.set(updated);
4442
+ this.onChange(updated);
4443
+ this.multiSelectionChange.emit(updated);
4444
+ this.cdr.markForCheck();
4445
+ }
4446
+ isOptionSelected(option) {
4447
+ if (this.multiple()) {
4448
+ return this.selectedValues().some(v => this.compareValues(v, option.value));
4449
+ }
4410
4450
  return this.compareValues(this.selectedValue(), option.value);
4411
- }, ...(ngDevMode ? [{ debugName: "isSelected" }] : []));
4451
+ }
4412
4452
  getDisplayText = computed(() => {
4453
+ if (this.multiple()) {
4454
+ const values = this.selectedValues();
4455
+ if (values.length === 0) {
4456
+ return this.placeholder();
4457
+ }
4458
+ const labels = values
4459
+ .map(v => this.findOptionByValue(v)?.label ?? String(v))
4460
+ .filter(Boolean);
4461
+ return labels.join(', ');
4462
+ }
4413
4463
  const value = this.selectedValue();
4414
4464
  if (value === null || value === undefined) {
4415
4465
  return this.placeholder();
4416
4466
  }
4417
4467
  const option = this.findOptionByValue(value);
4418
- return option ? option.label : value;
4468
+ return option ? option.label : String(value);
4419
4469
  }, ...(ngDevMode ? [{ debugName: "getDisplayText" }] : []));
4420
4470
  findOptionByValue(value) {
4421
4471
  // Search in regular options first
@@ -4436,6 +4486,10 @@ class TnSelectComponent {
4436
4486
  return this.options().length > 0 || this.optionGroups().length > 0;
4437
4487
  }, ...(ngDevMode ? [{ debugName: "hasAnyOptions" }] : []));
4438
4488
  compareValues(a, b) {
4489
+ const customCompare = this.compareWith();
4490
+ if (customCompare) {
4491
+ return customCompare(a, b);
4492
+ }
4439
4493
  if (a === b) {
4440
4494
  return true;
4441
4495
  }
@@ -4445,6 +4499,8 @@ class TnSelectComponent {
4445
4499
  return false;
4446
4500
  }
4447
4501
  // Keyboard navigation
4502
+ // TODO: Add ArrowUp/ArrowDown option navigation, Enter/Space toggle,
4503
+ // and aria-activedescendant tracking for full keyboard accessibility.
4448
4504
  onKeydown(event) {
4449
4505
  switch (event.key) {
4450
4506
  case 'Enter':
@@ -4469,24 +4525,24 @@ class TnSelectComponent {
4469
4525
  }
4470
4526
  }
4471
4527
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4472
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, providers: [
4528
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, providers: [
4473
4529
  {
4474
4530
  provide: NG_VALUE_ACCESSOR,
4475
4531
  useExisting: forwardRef(() => TnSelectComponent),
4476
4532
  multi: true
4477
4533
  }
4478
- ], ngImport: i0, template: "<div class=\"tn-select-container\" [attr.data-testid]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + testId() : null\"\n [attr.aria-label]=\"placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"selectedValue() === null || selectedValue() === undefined\">\n {{ getDisplayText() }}\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\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.id]=\"'tn-select-dropdown-' + testId()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled ? null : 0\"\n [class.selected]=\"isSelected()(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isSelected()(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\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 || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled || group.disabled ? null : 0\"\n [class.selected]=\"isSelected()(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isSelected()(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\n\n {{ option.label }}\n </div>\n }\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n No options available\n </div>\n }\n </div>\n </div>\n }\n</div>", 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:.875rem;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 #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.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 #dc354540}.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{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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:200px;overflow:hidden}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:200px}.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:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa)!important;color:var(--tn-fg1, #212529)}.tn-select-option.selected:hover{background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.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"] });
4534
+ ], ngImport: i0, template: "<div class=\"tn-select-container\" [attr.data-testid]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + testId() : null\"\n [attr.aria-label]=\"placeholder()\"\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 {{ getDisplayText() }}\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\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + testId()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled ? null : 0\"\n [class.selected]=\"isOptionSelected(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 (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"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 || options().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-' + testId() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + testId() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled || group.disabled ? null : 0\"\n [class.selected]=\"isOptionSelected(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 (keydown.enter)=\"onOptionClick(option, !!group.disabled)\"\n (keydown.space)=\"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 No options available\n </div>\n }\n </div>\n </div>\n }\n</div>\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:.875rem;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 #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.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 #dc354540}.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{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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:200px;overflow:hidden}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:200px}.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:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa)!important;color:var(--tn-fg1, #212529)}.tn-select-option.selected:hover{background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.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"] }] });
4479
4535
  }
4480
4536
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, decorators: [{
4481
4537
  type: Component,
4482
- args: [{ selector: 'tn-select', standalone: true, imports: [], providers: [
4538
+ args: [{ selector: 'tn-select', standalone: true, imports: [TnCheckboxComponent], providers: [
4483
4539
  {
4484
4540
  provide: NG_VALUE_ACCESSOR,
4485
4541
  useExisting: forwardRef(() => TnSelectComponent),
4486
4542
  multi: true
4487
4543
  }
4488
- ], template: "<div class=\"tn-select-container\" [attr.data-testid]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + testId() : null\"\n [attr.aria-label]=\"placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"selectedValue() === null || selectedValue() === undefined\">\n {{ getDisplayText() }}\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\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.id]=\"'tn-select-dropdown-' + testId()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled ? null : 0\"\n [class.selected]=\"isSelected()(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isSelected()(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\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 || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled || group.disabled ? null : 0\"\n [class.selected]=\"isSelected()(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isSelected()(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\n\n {{ option.label }}\n </div>\n }\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n No options available\n </div>\n }\n </div>\n </div>\n }\n</div>", 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:.875rem;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 #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.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 #dc354540}.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{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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:200px;overflow:hidden}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:200px}.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:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa)!important;color:var(--tn-fg1, #212529)}.tn-select-option.selected:hover{background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.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"] }]
4489
- }], ctorParameters: () => [], 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 }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
4544
+ ], template: "<div class=\"tn-select-container\" [attr.data-testid]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + testId() : null\"\n [attr.aria-label]=\"placeholder()\"\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 {{ getDisplayText() }}\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\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + testId()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled ? null : 0\"\n [class.selected]=\"isOptionSelected(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 (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"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 || options().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-' + testId() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + testId() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled || group.disabled ? null : 0\"\n [class.selected]=\"isOptionSelected(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 (keydown.enter)=\"onOptionClick(option, !!group.disabled)\"\n (keydown.space)=\"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 No options available\n </div>\n }\n </div>\n </div>\n }\n</div>\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:.875rem;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 #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.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 #dc354540}.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{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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:200px;overflow:hidden}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:200px}.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:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa)!important;color:var(--tn-fg1, #212529)}.tn-select-option.selected:hover{background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.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"] }]
4545
+ }], ctorParameters: () => [], 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 }] }], 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 }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }] } });
4490
4546
 
4491
4547
  /**
4492
4548
  * Harness for interacting with tn-select in tests.
@@ -7890,7 +7946,7 @@ class TnTimeInputComponent {
7890
7946
  useExisting: forwardRef(() => TnTimeInputComponent),
7891
7947
  multi: true
7892
7948
  }
7893
- ], ngImport: i0, template: "<tn-select\n [options]=\"timeSelectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"testId()\"\n [ngModel]=\"_value\"\n (selectionChange)=\"onSelectionChange($event)\" />\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "disabled", "testId"], outputs: ["selectionChange"] }] });
7949
+ ], ngImport: i0, template: "<tn-select\n [options]=\"timeSelectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"testId()\"\n [ngModel]=\"_value\"\n (selectionChange)=\"onSelectionChange($event)\" />\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "disabled", "testId", "multiple", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }] });
7894
7950
  }
7895
7951
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTimeInputComponent, decorators: [{
7896
7952
  type: Component,
@@ -10451,6 +10507,257 @@ class TnEmptyHarness extends ComponentHarness {
10451
10507
  }
10452
10508
  }
10453
10509
 
10510
+ var TnToastType;
10511
+ (function (TnToastType) {
10512
+ TnToastType["Info"] = "info";
10513
+ TnToastType["Success"] = "success";
10514
+ TnToastType["Warning"] = "warning";
10515
+ TnToastType["Error"] = "error";
10516
+ })(TnToastType || (TnToastType = {}));
10517
+ var TnToastPosition;
10518
+ (function (TnToastPosition) {
10519
+ TnToastPosition["Top"] = "top";
10520
+ TnToastPosition["Bottom"] = "bottom";
10521
+ })(TnToastPosition || (TnToastPosition = {}));
10522
+
10523
+ // Mark icons for sprite inclusion (dynamic names aren't detected by the scanner)
10524
+ const TOAST_ICONS = {
10525
+ [TnToastType.Info]: tnIconMarker('info', 'material'),
10526
+ [TnToastType.Success]: tnIconMarker('check_circle', 'material'),
10527
+ [TnToastType.Warning]: tnIconMarker('warning', 'material'),
10528
+ [TnToastType.Error]: tnIconMarker('error', 'material'),
10529
+ };
10530
+ class TnToastComponent {
10531
+ message = signal('', ...(ngDevMode ? [{ debugName: "message" }] : []));
10532
+ action = signal(null, ...(ngDevMode ? [{ debugName: "action" }] : []));
10533
+ type = signal(TnToastType.Info, ...(ngDevMode ? [{ debugName: "type" }] : []));
10534
+ position = signal(TnToastPosition.Bottom, ...(ngDevMode ? [{ debugName: "position" }] : []));
10535
+ visible = signal(false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
10536
+ icon = computed(() => TOAST_ICONS[this.type()], ...(ngDevMode ? [{ debugName: "icon" }] : []));
10537
+ onAction = () => { };
10538
+ onDismiss = () => { };
10539
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10540
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnToastComponent, isStandalone: true, selector: "tn-toast", host: { properties: { "class.tn-toast--top": "position() === \"top\"", "class.tn-toast--bottom": "position() === \"bottom\"" } }, ngImport: i0, template: "<div\n class=\"tn-toast\"\n role=\"alert\"\n aria-live=\"polite\"\n [class.tn-toast--visible]=\"visible()\"\n [class.tn-toast--info]=\"type() === 'info'\"\n [class.tn-toast--success]=\"type() === 'success'\"\n [class.tn-toast--warning]=\"type() === 'warning'\"\n [class.tn-toast--error]=\"type() === 'error'\">\n <tn-icon class=\"tn-toast__icon\" size=\"sm\" [name]=\"icon()\" />\n <span class=\"tn-toast__message\">{{ message() }}</span>\n @if (action()) {\n <button\n class=\"tn-toast__action\"\n type=\"button\"\n (click)=\"onAction()\">\n {{ action() }}\n </button>\n }\n</div>\n", styles: ["tn-toast{position:fixed;left:50%;transform:translate(-50%);z-index:10000;pointer-events:none}tn-toast.tn-toast--bottom{bottom:1.5rem}tn-toast.tn-toast--top{top:1.5rem}.tn-toast{display:flex;align-items:center;gap:.75rem;padding:.75rem 1.25rem;border-radius:.5rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:.875rem;line-height:1.4;pointer-events:auto;background-color:var(--tn-bg2, #333);color:var(--tn-fg1, #fff);border-left:3px solid transparent;box-shadow:0 8px 24px #0000004d;opacity:0;transform:translateY(1rem);transition:opacity .2s ease-out,transform .2s ease-out;max-width:560px}.tn-toast.tn-toast--visible{opacity:1;transform:translateY(0)}.tn-toast.tn-toast--info{border-left-color:var(--tn-info, #3b82f6)}.tn-toast.tn-toast--success{border-left-color:var(--tn-success, #10b981)}.tn-toast.tn-toast--warning{border-left-color:var(--tn-warning, #f59e0b)}.tn-toast.tn-toast--error{border-left-color:var(--tn-error, #ef4444)}.tn-toast__icon{flex-shrink:0}.tn-toast--info .tn-toast__icon{color:var(--tn-info, #3b82f6)}.tn-toast--success .tn-toast__icon{color:var(--tn-success, #10b981)}.tn-toast--warning .tn-toast__icon{color:var(--tn-warning, #f59e0b)}.tn-toast--error .tn-toast__icon{color:var(--tn-error, #ef4444)}.tn-toast__message{flex:1}.tn-toast__action{background:none;border:none;color:var(--tn-primary, #3b82f6);font-family:inherit;font-size:.875rem;font-weight:600;cursor:pointer;padding:.25rem .5rem;border-radius:.25rem;white-space:nowrap;transition:background-color .15s ease}.tn-toast__action:hover{background-color:#ffffff1a}@media(prefers-reduced-motion:reduce){.tn-toast{transition:none}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
10541
+ }
10542
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnToastComponent, decorators: [{
10543
+ type: Component,
10544
+ args: [{ selector: 'tn-toast', standalone: true, imports: [TnIconComponent], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: {
10545
+ '[class.tn-toast--top]': 'position() === "top"',
10546
+ '[class.tn-toast--bottom]': 'position() === "bottom"',
10547
+ }, template: "<div\n class=\"tn-toast\"\n role=\"alert\"\n aria-live=\"polite\"\n [class.tn-toast--visible]=\"visible()\"\n [class.tn-toast--info]=\"type() === 'info'\"\n [class.tn-toast--success]=\"type() === 'success'\"\n [class.tn-toast--warning]=\"type() === 'warning'\"\n [class.tn-toast--error]=\"type() === 'error'\">\n <tn-icon class=\"tn-toast__icon\" size=\"sm\" [name]=\"icon()\" />\n <span class=\"tn-toast__message\">{{ message() }}</span>\n @if (action()) {\n <button\n class=\"tn-toast__action\"\n type=\"button\"\n (click)=\"onAction()\">\n {{ action() }}\n </button>\n }\n</div>\n", styles: ["tn-toast{position:fixed;left:50%;transform:translate(-50%);z-index:10000;pointer-events:none}tn-toast.tn-toast--bottom{bottom:1.5rem}tn-toast.tn-toast--top{top:1.5rem}.tn-toast{display:flex;align-items:center;gap:.75rem;padding:.75rem 1.25rem;border-radius:.5rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:.875rem;line-height:1.4;pointer-events:auto;background-color:var(--tn-bg2, #333);color:var(--tn-fg1, #fff);border-left:3px solid transparent;box-shadow:0 8px 24px #0000004d;opacity:0;transform:translateY(1rem);transition:opacity .2s ease-out,transform .2s ease-out;max-width:560px}.tn-toast.tn-toast--visible{opacity:1;transform:translateY(0)}.tn-toast.tn-toast--info{border-left-color:var(--tn-info, #3b82f6)}.tn-toast.tn-toast--success{border-left-color:var(--tn-success, #10b981)}.tn-toast.tn-toast--warning{border-left-color:var(--tn-warning, #f59e0b)}.tn-toast.tn-toast--error{border-left-color:var(--tn-error, #ef4444)}.tn-toast__icon{flex-shrink:0}.tn-toast--info .tn-toast__icon{color:var(--tn-info, #3b82f6)}.tn-toast--success .tn-toast__icon{color:var(--tn-success, #10b981)}.tn-toast--warning .tn-toast__icon{color:var(--tn-warning, #f59e0b)}.tn-toast--error .tn-toast__icon{color:var(--tn-error, #ef4444)}.tn-toast__message{flex:1}.tn-toast__action{background:none;border:none;color:var(--tn-primary, #3b82f6);font-family:inherit;font-size:.875rem;font-weight:600;cursor:pointer;padding:.25rem .5rem;border-radius:.25rem;white-space:nowrap;transition:background-color .15s ease}.tn-toast__action:hover{background-color:#ffffff1a}@media(prefers-reduced-motion:reduce){.tn-toast{transition:none}}\n"] }]
10548
+ }] });
10549
+
10550
+ class TnToastRef {
10551
+ _onAction = new Subject();
10552
+ _afterDismissed = new Subject();
10553
+ _dismissed = false;
10554
+ /** @internal */
10555
+ _componentRef;
10556
+ /** Observable that emits when the action button is clicked. */
10557
+ onAction() {
10558
+ return this._onAction.asObservable();
10559
+ }
10560
+ /** Observable that emits when the toast is dismissed (by action, duration, or programmatically). */
10561
+ afterDismissed() {
10562
+ return this._afterDismissed.asObservable();
10563
+ }
10564
+ /** Programmatically dismiss the toast. */
10565
+ dismiss() {
10566
+ if (this._dismissed) {
10567
+ return;
10568
+ }
10569
+ this._dismissed = true;
10570
+ this._afterDismissed.next();
10571
+ this._afterDismissed.complete();
10572
+ this._onAction.complete();
10573
+ }
10574
+ /** @internal */
10575
+ _triggerAction() {
10576
+ this._onAction.next();
10577
+ this.dismiss();
10578
+ }
10579
+ }
10580
+ class TnToastService {
10581
+ appRef = inject(ApplicationRef);
10582
+ injector = inject(EnvironmentInjector);
10583
+ activeRef = null;
10584
+ /**
10585
+ * Opens a toast notification.
10586
+ *
10587
+ * @param message The message to display.
10588
+ * @param actionOrConfig Optional action button text, or config object.
10589
+ * @param config Optional config when action is provided as second arg.
10590
+ * @returns A TnToastRef that can be used to dismiss the toast or listen for events.
10591
+ *
10592
+ * @example
10593
+ * ```typescript
10594
+ * // Simple notification
10595
+ * this.toast.open('Changes saved');
10596
+ *
10597
+ * // With action button
10598
+ * const ref = this.toast.open('Item deleted', 'Undo');
10599
+ * ref.onAction().subscribe(() => this.undoDelete());
10600
+ *
10601
+ * // With config
10602
+ * this.toast.open('Error occurred', { type: 'error', duration: 6000 });
10603
+ *
10604
+ * // Action + config
10605
+ * this.toast.open('Failed to save', 'Retry', { type: 'error' });
10606
+ * ```
10607
+ */
10608
+ open(message, actionOrConfig, config) {
10609
+ // Dismiss any existing toast
10610
+ if (this.activeRef) {
10611
+ this.activeRef.dismiss();
10612
+ }
10613
+ // Parse overloaded args
10614
+ let action;
10615
+ let resolvedConfig = {};
10616
+ if (typeof actionOrConfig === 'string') {
10617
+ action = actionOrConfig;
10618
+ resolvedConfig = config ?? {};
10619
+ }
10620
+ else if (actionOrConfig) {
10621
+ resolvedConfig = actionOrConfig;
10622
+ }
10623
+ const duration = resolvedConfig.duration ?? 4000;
10624
+ const type = resolvedConfig.type ?? TnToastType.Info;
10625
+ const position = resolvedConfig.position ?? TnToastPosition.Bottom;
10626
+ // Create ref
10627
+ const ref = new TnToastRef();
10628
+ this.activeRef = ref;
10629
+ // Create component
10630
+ const componentRef = createComponent(TnToastComponent, {
10631
+ environmentInjector: this.injector,
10632
+ });
10633
+ ref._componentRef = componentRef;
10634
+ const instance = componentRef.instance;
10635
+ instance.message.set(message);
10636
+ instance.action.set(action ?? null);
10637
+ instance.type.set(type);
10638
+ instance.position.set(position);
10639
+ instance.onAction = () => ref._triggerAction();
10640
+ instance.onDismiss = () => ref.dismiss();
10641
+ // Attach to DOM
10642
+ this.appRef.attachView(componentRef.hostView);
10643
+ document.body.appendChild(componentRef.location.nativeElement);
10644
+ // Animate in
10645
+ requestAnimationFrame(() => {
10646
+ instance.visible.set(true);
10647
+ });
10648
+ // Auto-dismiss
10649
+ let timeout = null;
10650
+ if (duration > 0) {
10651
+ timeout = setTimeout(() => ref.dismiss(), duration);
10652
+ }
10653
+ // Cleanup on dismiss
10654
+ ref.afterDismissed().subscribe(() => {
10655
+ if (timeout) {
10656
+ clearTimeout(timeout);
10657
+ }
10658
+ instance.visible.set(false);
10659
+ // Wait for animation to complete before removing
10660
+ setTimeout(() => {
10661
+ this.appRef.detachView(componentRef.hostView);
10662
+ componentRef.destroy();
10663
+ if (this.activeRef === ref) {
10664
+ this.activeRef = null;
10665
+ }
10666
+ }, 200);
10667
+ });
10668
+ return ref;
10669
+ }
10670
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
10671
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnToastService, providedIn: 'root' });
10672
+ }
10673
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnToastService, decorators: [{
10674
+ type: Injectable,
10675
+ args: [{ providedIn: 'root' }]
10676
+ }] });
10677
+
10678
+ /**
10679
+ * A mock implementation of TnToastService for unit testing.
10680
+ *
10681
+ * Records all `open()` calls so tests can assert on toast messages,
10682
+ * types, and actions without rendering actual toast components.
10683
+ *
10684
+ * @example
10685
+ * ```typescript
10686
+ * import { TnToastTesting } from '@truenas/ui-components';
10687
+ *
10688
+ * let toastMock: TnToastMock;
10689
+ *
10690
+ * beforeEach(() => {
10691
+ * toastMock = new TnToastMock();
10692
+ * TestBed.configureTestingModule({
10693
+ * providers: [TnToastTesting.providers(toastMock)],
10694
+ * });
10695
+ * });
10696
+ *
10697
+ * it('should show success toast', () => {
10698
+ * // ... trigger action that opens a toast
10699
+ * expect(toastMock.calls.length).toBe(1);
10700
+ * expect(toastMock.lastCall?.message).toBe('Saved successfully');
10701
+ * expect(toastMock.lastCall?.config.type).toBe(TnToastType.Success);
10702
+ * });
10703
+ *
10704
+ * it('should handle action click', () => {
10705
+ * // ... trigger action that opens a toast with action
10706
+ * toastMock.lastCall?.ref._triggerAction();
10707
+ * // ... assert retry behavior
10708
+ * });
10709
+ * ```
10710
+ */
10711
+ class TnToastMock {
10712
+ /** All recorded toast open() calls. */
10713
+ calls = [];
10714
+ /** The most recent toast call, or undefined if none. */
10715
+ get lastCall() {
10716
+ return this.calls[this.calls.length - 1];
10717
+ }
10718
+ /** Clears all recorded calls. */
10719
+ reset() {
10720
+ this.calls = [];
10721
+ }
10722
+ open(message, actionOrConfig, config) {
10723
+ let action;
10724
+ let resolvedConfig = {};
10725
+ if (typeof actionOrConfig === 'string') {
10726
+ action = actionOrConfig;
10727
+ resolvedConfig = config ?? {};
10728
+ }
10729
+ else if (actionOrConfig) {
10730
+ resolvedConfig = actionOrConfig;
10731
+ }
10732
+ const ref = new TnToastRef();
10733
+ this.calls.push({ message, action, config: resolvedConfig, ref });
10734
+ return ref;
10735
+ }
10736
+ }
10737
+ /**
10738
+ * Test utilities for TnToastService.
10739
+ *
10740
+ * Provides a mock that records toast calls without rendering components,
10741
+ * making tests fast and deterministic.
10742
+ *
10743
+ * @example
10744
+ * ```typescript
10745
+ * const toastMock = new TnToastMock();
10746
+ *
10747
+ * TestBed.configureTestingModule({
10748
+ * providers: [TnToastTesting.providers(toastMock)],
10749
+ * });
10750
+ * ```
10751
+ */
10752
+ class TnToastTesting {
10753
+ /**
10754
+ * Returns providers that replace TnToastService with the given mock.
10755
+ */
10756
+ static providers(mock) {
10757
+ return [{ provide: TnToastService, useValue: mock }];
10758
+ }
10759
+ }
10760
+
10454
10761
  class TnKeyboardShortcutService {
10455
10762
  shortcuts = new Map();
10456
10763
  globalEnabled = true;
@@ -11054,5 +11361,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11054
11361
  * Generated bundle index. Do not edit.
11055
11362
  */
11056
11363
 
11057
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateRangeInputComponent, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTabsHarness, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
11364
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateRangeInputComponent, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTabsHarness, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
11058
11365
  //# sourceMappingURL=truenas-ui-components.mjs.map