@truenas/ui-components 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
2
2
  import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
3
3
  import * as i0 from '@angular/core';
4
- import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, inject, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, ChangeDetectorRef, DestroyRef, TemplateRef, isSignal, IterableDiffers, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
4
+ import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, inject, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
5
5
  import * as i1$3 from '@angular/forms';
6
6
  import { NG_VALUE_ACCESSOR, FormsModule, NgControl, Validators } from '@angular/forms';
7
7
  import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
@@ -261,13 +261,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
261
261
  }]
262
262
  }], ctorParameters: () => [], propDecorators: { testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTestId", required: false }] }], tnTestIdType: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTestIdType", required: false }] }] } });
263
263
 
264
- let nextId$1 = 0;
264
+ let nextId$2 = 0;
265
265
  class TnAutocompleteComponent {
266
266
  elementRef = inject(ElementRef);
267
267
  overlay = inject(Overlay);
268
268
  viewContainerRef = inject(ViewContainerRef);
269
269
  /** Unique instance ID for ARIA linkage */
270
- uid = `tn-autocomplete-${nextId$1++}`;
270
+ uid = `tn-autocomplete-${nextId$2++}`;
271
271
  /**
272
272
  * All available options. The `label` is displayed; the `value` is committed
273
273
  * to the form control. A written value is resolved back to its option's
@@ -3208,7 +3208,7 @@ function unitExponent(unit) {
3208
3208
  // Module-level counter for deterministic, unique instance ids (matches the
3209
3209
  // tn-autocomplete convention). Deterministic ids are SSR/hydration-safe and
3210
3210
  // stable across snapshots, unlike a Math.random() seed.
3211
- let nextId = 0;
3211
+ let nextId$1 = 0;
3212
3212
  class TnInputComponent {
3213
3213
  inputEl = viewChild.required('inputEl');
3214
3214
  inputType = input(InputType.PlainText, ...(ngDevMode ? [{ debugName: "inputType" }] : []));
@@ -3372,7 +3372,7 @@ class TnInputComponent {
3372
3372
  // Unique per instance: a hard-coded id produced duplicate ids when multiple
3373
3373
  // tn-inputs share a page (invalid HTML, and it breaks any future label `for=`,
3374
3374
  // aria-describedby, or getElementById targeting this input).
3375
- id = `tn-input-${nextId++}`;
3375
+ id = `tn-input-${nextId$1++}`;
3376
3376
  // Protected: the display string is owned by the component and driven through the
3377
3377
  // CVA flow (writeValue / onValueChange). External writes would bypass that flow.
3378
3378
  // A signal so the `[value]` binding reflects writeValue reactively — a plain field
@@ -4368,6 +4368,536 @@ class TnChipHarness extends ComponentHarness {
4368
4368
  }
4369
4369
  }
4370
4370
 
4371
+ let nextId = 0;
4372
+ /**
4373
+ * An editable, multi-value chip input — tokenized entry where typed text
4374
+ * becomes removable `tn-chip`s alongside an inline text field. Text is
4375
+ * committed to a chip on Enter (or a configurable separator key); Backspace on
4376
+ * an empty field removes the last chip.
4377
+ *
4378
+ * It is a `ControlValueAccessor` over `string[]`, so it drops into a reactive
4379
+ * or template-driven form (`[formControl]`, `[(ngModel)]`) and slots into a
4380
+ * `tn-form-field` as a real projected control — the field's required/error
4381
+ * inference reads this control directly.
4382
+ *
4383
+ * Suggestions are optional: pass `[suggestions]` for a static list, or drive
4384
+ * them asynchronously by listening to `(searchChange)` and updating
4385
+ * `[suggestions]` as results arrive. The dropdown is portaled through a CDK
4386
+ * overlay so it escapes any ancestor `overflow: hidden` (cards, side panels).
4387
+ *
4388
+ * @example
4389
+ * ```html
4390
+ * <tn-form-field label="Tags">
4391
+ * <tn-chip-input [formControl]="tags" placeholder="Add a tag…" />
4392
+ * </tn-form-field>
4393
+ * ```
4394
+ */
4395
+ class TnChipInputComponent {
4396
+ overlay = inject(Overlay);
4397
+ viewContainerRef = inject(ViewContainerRef);
4398
+ /** Unique instance id for ARIA linkage between the input and its dropdown. */
4399
+ uid = `tn-chip-input-${nextId++}`;
4400
+ /** Placeholder shown in the text field when it is empty. */
4401
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
4402
+ /** Disables the whole control — chips become non-removable and the field read-only. */
4403
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
4404
+ /**
4405
+ * Keys that commit the current text as a chip, in addition to `Enter`.
4406
+ * Defaults to `Enter` plus comma. A separator key press never inserts its
4407
+ * own character.
4408
+ */
4409
+ separatorKeys = input(['Enter', ','], ...(ngDevMode ? [{ debugName: "separatorKeys" }] : []));
4410
+ /** Commit a pending (non-empty) text value as a chip when the field loses focus. */
4411
+ addOnBlur = input(false, ...(ngDevMode ? [{ debugName: "addOnBlur" }] : []));
4412
+ /**
4413
+ * Allow the same value to be added more than once. Off by default.
4414
+ * Duplicate detection is exact-match (case-sensitive), so with this off
4415
+ * `Angular` and `angular` are still distinct values — only suggestion
4416
+ * *filtering* is case-insensitive.
4417
+ */
4418
+ allowDuplicates = input(false, ...(ngDevMode ? [{ debugName: "allowDuplicates" }] : []));
4419
+ /** Hard cap on the number of chips; `undefined` means no limit. */
4420
+ maxChips = input(undefined, ...(ngDevMode ? [{ debugName: "maxChips" }] : []));
4421
+ /**
4422
+ * Optional suggestion list. When non-empty, a dropdown offers entries that
4423
+ * match the typed text and are not already selected. For async sources,
4424
+ * update this in response to `(searchChange)`.
4425
+ */
4426
+ suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : []));
4427
+ /** Accessible name for the text field. Leave unset inside a `tn-form-field`. */
4428
+ ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
4429
+ /**
4430
+ * Semantic test-id base. The library prepends the `chip-input` element type
4431
+ * (e.g. `testId="tags"` → `chip-input-tags`); each chip and suggestion is
4432
+ * scoped beneath it.
4433
+ */
4434
+ testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
4435
+ /** Emits the committed value whenever a chip is added. */
4436
+ chipAdded = output();
4437
+ /** Emits the removed value whenever a chip is removed. */
4438
+ chipRemoved = output();
4439
+ /**
4440
+ * Emits the current text as the user types (not on programmatic writes or
4441
+ * chip commits). Drive server-side suggestion lookups from this; debounce in
4442
+ * the consumer if the lookup is expensive.
4443
+ */
4444
+ searchChange = output();
4445
+ container = viewChild.required('container');
4446
+ inputEl = viewChild.required('inputEl');
4447
+ dropdownTemplate = viewChild.required('dropdownTemplate');
4448
+ /** Committed chip values — the form model. */
4449
+ values = signal([], ...(ngDevMode ? [{ debugName: "values" }] : []));
4450
+ /** Current text in the field. */
4451
+ inputValue = signal('', ...(ngDevMode ? [{ debugName: "inputValue" }] : []));
4452
+ /** Whether the suggestion dropdown is open. */
4453
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
4454
+ /** Index of the keyboard-highlighted suggestion, or -1. */
4455
+ highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : []));
4456
+ /** Whether the text field currently holds focus — gates async re-opening. */
4457
+ focused = signal(false, ...(ngDevMode ? [{ debugName: "focused" }] : []));
4458
+ /** CVA disabled state pushed by the form. */
4459
+ formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
4460
+ /** Combined disabled state from the input and the form. */
4461
+ isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
4462
+ /** Whether another chip may still be added under `maxChips`. */
4463
+ canAddMore = computed(() => {
4464
+ const max = this.maxChips();
4465
+ return max === undefined || this.values().length < max;
4466
+ }, ...(ngDevMode ? [{ debugName: "canAddMore" }] : []));
4467
+ /** Suggestions that match the typed text and are not already selected. */
4468
+ filteredSuggestions = computed(() => {
4469
+ const selected = new Set(this.values());
4470
+ const term = this.inputValue().trim().toLowerCase();
4471
+ return this.suggestions().filter((suggestion) => {
4472
+ if (selected.has(suggestion)) {
4473
+ return false;
4474
+ }
4475
+ return term === '' || suggestion.toLowerCase().includes(term);
4476
+ });
4477
+ }, ...(ngDevMode ? [{ debugName: "filteredSuggestions" }] : []));
4478
+ onChange = () => { };
4479
+ onTouched = () => { };
4480
+ overlayRef;
4481
+ overlaySubs = [];
4482
+ constructor() {
4483
+ // Async suggestions: when the user types, onInput runs syncDropdown()
4484
+ // against the still-stale list and leaves the panel closed; results land a
4485
+ // tick later via [suggestions]. Re-open the panel once fresh matches arrive
4486
+ // while the field is focused and actively searching. This only ever opens
4487
+ // (never closes), so it doesn't fight Escape, blur, or the post-commit close
4488
+ // — those stay shut until the suggestion set next changes.
4489
+ effect(() => {
4490
+ const hasMatches = this.filteredSuggestions().length > 0;
4491
+ untracked(() => {
4492
+ const activelySearching = this.focused()
4493
+ && this.inputValue().trim() !== ''
4494
+ && this.canAddMore()
4495
+ && !this.isDisabled();
4496
+ if (hasMatches && activelySearching) {
4497
+ this.open();
4498
+ }
4499
+ });
4500
+ });
4501
+ }
4502
+ ngOnDestroy() {
4503
+ this.detachOverlay();
4504
+ }
4505
+ // ── ControlValueAccessor ──
4506
+ writeValue(value) {
4507
+ // Reflect the model verbatim — deliberately NOT clamped to maxChips. A form
4508
+ // may legitimately seed more values than the cap; silently dropping them
4509
+ // would lose data. The cap only blocks further user-driven additions.
4510
+ this.values.set(Array.isArray(value) ? [...value] : []);
4511
+ }
4512
+ registerOnChange(fn) {
4513
+ this.onChange = fn;
4514
+ }
4515
+ registerOnTouched(fn) {
4516
+ this.onTouched = fn;
4517
+ }
4518
+ setDisabledState(isDisabled) {
4519
+ this.formDisabled.set(isDisabled);
4520
+ if (isDisabled) {
4521
+ this.close();
4522
+ }
4523
+ }
4524
+ // ── Template handlers ──
4525
+ /** Clicking anywhere in the container focuses the text field. */
4526
+ focusInput() {
4527
+ if (!this.isDisabled()) {
4528
+ this.inputEl().nativeElement.focus();
4529
+ }
4530
+ }
4531
+ onInput(event) {
4532
+ const value = event.target.value;
4533
+ this.inputValue.set(value);
4534
+ this.searchChange.emit(value);
4535
+ this.highlightedIndex.set(-1);
4536
+ this.syncDropdown();
4537
+ }
4538
+ onFocus() {
4539
+ this.focused.set(true);
4540
+ this.syncDropdown();
4541
+ }
4542
+ onBlur() {
4543
+ this.focused.set(false);
4544
+ if (this.addOnBlur()) {
4545
+ this.addChip(this.inputValue());
4546
+ }
4547
+ this.close();
4548
+ this.onTouched();
4549
+ }
4550
+ onKeydown(event) {
4551
+ // Mid-IME-composition (Japanese/Chinese/Korean), the Enter that confirms a
4552
+ // candidate also fires keydown with isComposing=true — committing here would
4553
+ // swallow the confirmation and chip a half-composed value. Let it through.
4554
+ if (event.isComposing) {
4555
+ return;
4556
+ }
4557
+ const suggestions = this.filteredSuggestions();
4558
+ if (event.key === 'ArrowDown') {
4559
+ if (suggestions.length && this.canAddMore()) {
4560
+ event.preventDefault();
4561
+ this.open();
4562
+ this.highlightedIndex.set((this.highlightedIndex() + 1) % suggestions.length);
4563
+ this.scrollToHighlighted();
4564
+ }
4565
+ return;
4566
+ }
4567
+ if (event.key === 'ArrowUp') {
4568
+ if (suggestions.length && this.isOpen()) {
4569
+ event.preventDefault();
4570
+ const next = this.highlightedIndex() - 1;
4571
+ this.highlightedIndex.set(next < 0 ? suggestions.length - 1 : next);
4572
+ this.scrollToHighlighted();
4573
+ }
4574
+ return;
4575
+ }
4576
+ if (event.key === 'Escape') {
4577
+ if (this.isOpen()) {
4578
+ event.preventDefault();
4579
+ this.close();
4580
+ }
4581
+ return;
4582
+ }
4583
+ if (this.isCommitKey(event)) {
4584
+ event.preventDefault();
4585
+ const idx = this.highlightedIndex();
4586
+ if (this.isOpen() && idx >= 0 && idx < suggestions.length) {
4587
+ this.addChip(suggestions[idx]);
4588
+ }
4589
+ else {
4590
+ this.addChip(this.inputValue());
4591
+ }
4592
+ return;
4593
+ }
4594
+ // Backspace on an empty field removes the last chip — the standard
4595
+ // chip-input affordance for quick correction.
4596
+ if (event.key === 'Backspace' && this.inputValue() === '' && this.values().length > 0) {
4597
+ event.preventDefault();
4598
+ this.removeChip(this.values().length - 1);
4599
+ }
4600
+ }
4601
+ onSuggestionClick(suggestion) {
4602
+ this.addChip(suggestion);
4603
+ this.inputEl().nativeElement.focus();
4604
+ }
4605
+ /** Prevents the option `mousedown` from blurring the input before the click lands. */
4606
+ onSuggestionMousedown(event) {
4607
+ event.preventDefault();
4608
+ }
4609
+ removeChip(index) {
4610
+ if (this.isDisabled()) {
4611
+ return;
4612
+ }
4613
+ const removed = this.values()[index];
4614
+ if (removed === undefined) {
4615
+ return;
4616
+ }
4617
+ this.values.update((values) => values.filter((_, i) => i !== index));
4618
+ this.onChange(this.values());
4619
+ this.onTouched();
4620
+ this.chipRemoved.emit(removed);
4621
+ // Removing via a chip's close button leaves focus on the (now-destroyed)
4622
+ // button; return it to the field so keyboard users stay oriented. The
4623
+ // Backspace path is already focused here, so this is a harmless no-op there.
4624
+ this.inputEl().nativeElement.focus();
4625
+ this.syncDropdown();
4626
+ }
4627
+ /** Scopes a per-chip test id beneath the component's base. */
4628
+ chipTestId(value) {
4629
+ const base = this.testId();
4630
+ if (base === undefined) {
4631
+ return undefined;
4632
+ }
4633
+ return [...(Array.isArray(base) ? base : [base]), value];
4634
+ }
4635
+ // ── Internal ──
4636
+ isCommitKey(event) {
4637
+ return event.key === 'Enter' || this.separatorKeys().includes(event.key);
4638
+ }
4639
+ addChip(raw) {
4640
+ const value = (raw ?? '').trim();
4641
+ if (!value || this.isDisabled() || !this.canAddMore()) {
4642
+ return;
4643
+ }
4644
+ if (!this.allowDuplicates() && this.values().includes(value)) {
4645
+ this.clearInput();
4646
+ return;
4647
+ }
4648
+ this.values.update((values) => [...values, value]);
4649
+ this.onChange(this.values());
4650
+ this.onTouched();
4651
+ this.chipAdded.emit(value);
4652
+ this.clearInput();
4653
+ }
4654
+ clearInput() {
4655
+ this.inputValue.set('');
4656
+ this.inputEl().nativeElement.value = '';
4657
+ this.close();
4658
+ }
4659
+ /**
4660
+ * Opens the dropdown when there is something to show, closes it otherwise.
4661
+ * Stays closed once the chip cap is reached — suggesting rows that
4662
+ * `addChip()` would reject is misleading.
4663
+ */
4664
+ syncDropdown() {
4665
+ if (this.filteredSuggestions().length > 0 && this.canAddMore() && !this.isDisabled()) {
4666
+ this.open();
4667
+ }
4668
+ else {
4669
+ this.close();
4670
+ }
4671
+ }
4672
+ /** Keeps the keyboard-highlighted suggestion visible within the scrolling panel. */
4673
+ scrollToHighlighted() {
4674
+ const idx = this.highlightedIndex();
4675
+ const options = this.overlayRef?.overlayElement
4676
+ ?.querySelectorAll('.tn-chip-input__option');
4677
+ options?.[idx]?.scrollIntoView({ block: 'nearest' });
4678
+ }
4679
+ open() {
4680
+ if (this.isOpen() || this.isDisabled()) {
4681
+ return;
4682
+ }
4683
+ this.isOpen.set(true);
4684
+ this.attachOverlay();
4685
+ }
4686
+ close() {
4687
+ this.isOpen.set(false);
4688
+ this.highlightedIndex.set(-1);
4689
+ this.detachOverlay();
4690
+ }
4691
+ attachOverlay() {
4692
+ const anchor = this.container().nativeElement;
4693
+ const positionStrategy = this.overlay
4694
+ .position()
4695
+ .flexibleConnectedTo(anchor)
4696
+ .withPositions([
4697
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
4698
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
4699
+ ]);
4700
+ this.overlayRef = this.overlay.create({
4701
+ positionStrategy,
4702
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
4703
+ hasBackdrop: false,
4704
+ width: anchor.offsetWidth,
4705
+ });
4706
+ this.overlayRef.attach(new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef));
4707
+ this.overlaySubs.push(this.overlayRef.outsidePointerEvents().subscribe((event) => {
4708
+ const target = event.target;
4709
+ if (target && anchor.contains(target)) {
4710
+ return;
4711
+ }
4712
+ this.close();
4713
+ }));
4714
+ }
4715
+ detachOverlay() {
4716
+ this.overlaySubs.forEach((sub) => sub.unsubscribe());
4717
+ this.overlaySubs = [];
4718
+ this.overlayRef?.dispose();
4719
+ this.overlayRef = undefined;
4720
+ }
4721
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnChipInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4722
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnChipInputComponent, isStandalone: true, selector: "tn-chip-input", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, separatorKeys: { classPropertyName: "separatorKeys", publicName: "separatorKeys", isSignal: true, isRequired: false, transformFunction: null }, addOnBlur: { classPropertyName: "addOnBlur", publicName: "addOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, maxChips: { classPropertyName: "maxChips", publicName: "maxChips", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { chipAdded: "chipAdded", chipRemoved: "chipRemoved", searchChange: "searchChange" }, providers: [
4723
+ {
4724
+ provide: NG_VALUE_ACCESSOR,
4725
+ useExisting: forwardRef(() => TnChipInputComponent),
4726
+ multi: true,
4727
+ },
4728
+ ], viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"value\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track suggestion) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"chipTestId(suggestion)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{max-height:var(--tn-chip-input-panel-max-height, 320px);overflow-y:auto;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"], dependencies: [{ kind: "component", type: TnChipComponent, selector: "tn-chip", inputs: ["label", "icon", "closable", "disabled", "color", "testId"], outputs: ["onClose", "onClick"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
4729
+ }
4730
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnChipInputComponent, decorators: [{
4731
+ type: Component,
4732
+ args: [{ selector: 'tn-chip-input', standalone: true, imports: [TnChipComponent, TnTestIdDirective], providers: [
4733
+ {
4734
+ provide: NG_VALUE_ACCESSOR,
4735
+ useExisting: forwardRef(() => TnChipInputComponent),
4736
+ multi: true,
4737
+ },
4738
+ ], template: "<!-- Click-to-focus is a pointer affordance only; keyboard users Tab straight to\n the real <input> inside, so the container itself is not a focus target. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #container\n class=\"tn-chip-input\"\n [class.tn-chip-input--disabled]=\"isDisabled()\"\n (click)=\"focusInput()\"\n>\n @for (value of values(); track $index) {\n <tn-chip\n class=\"tn-chip-input__chip\"\n color=\"secondary\"\n [label]=\"value\"\n [closable]=\"!isDisabled()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"chipTestId(value)\"\n (onClose)=\"removeChip($index)\"\n />\n }\n\n <input\n #inputEl\n type=\"text\"\n class=\"tn-chip-input__field\"\n role=\"combobox\"\n autocomplete=\"off\"\n tnTestIdType=\"chip-input\"\n [tnTestId]=\"testId()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"isOpen() ? uid + '-listbox' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n />\n</div>\n\n<!-- Suggestion dropdown \u2014 portaled via CDK overlay so it escapes ancestor\n overflow/clipping. Attach/detach is driven by open()/close(). -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-chip-input__dropdown\"\n role=\"listbox\"\n [attr.id]=\"uid + '-listbox'\"\n >\n @for (suggestion of filteredSuggestions(); track suggestion) {\n <!-- Options are tabindex=\"-1\" and never focused: keyboard selection goes\n through the input's aria-activedescendant + the Enter branch in\n onKeydown, so a key handler here would be dead code. Click is a\n pointer-only affordance. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n <div\n class=\"tn-chip-input__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"chipTestId(suggestion)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"highlightedIndex() === $index\"\n (mousedown)=\"onSuggestionMousedown($event)\"\n (click)=\"onSuggestionClick(suggestion)\"\n >\n {{ suggestion }}\n </div>\n }\n </div>\n</ng-template>\n", styles: [".tn-chip-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;min-height:2.5rem;padding:.375rem .5rem;box-sizing:border-box;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:text;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-chip-input:focus-within{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-chip-input--disabled{background-color:var(--tn-alt-bg1, #f8f9fa);opacity:.6;cursor:not-allowed}.tn-chip-input__chip{flex-shrink:0}.tn-chip-input__field{flex:1;min-width:120px;padding:.25rem;font-size:1rem;line-height:1.5;color:var(--tn-fg1, #212529);background:transparent;border:none;outline:none}.tn-chip-input__field::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-chip-input__field:disabled{cursor:not-allowed}.tn-chip-input__dropdown{max-height:var(--tn-chip-input-panel-max-height, 320px);overflow-y:auto;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.tn-chip-input__option{padding:.5rem .75rem;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;font-size:1rem;color:var(--tn-fg1, #212529);cursor:pointer}.tn-chip-input__option:hover,.tn-chip-input__option.highlighted{background-color:var(--tn-bg3, #f3f4f6)}@media(prefers-reduced-motion:reduce){.tn-chip-input{transition:none}}@media(prefers-contrast:high){.tn-chip-input{border-width:2px}}\n"] }]
4739
+ }], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], separatorKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorKeys", required: false }] }], addOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnBlur", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], maxChips: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxChips", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], chipAdded: [{ type: i0.Output, args: ["chipAdded"] }], chipRemoved: [{ type: i0.Output, args: ["chipRemoved"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
4740
+
4741
+ /**
4742
+ * Harness for interacting with `tn-chip-input` in tests. Reads the committed
4743
+ * chips and drives the text field — adding values (optionally via the
4744
+ * suggestion dropdown) and removing them.
4745
+ *
4746
+ * @example
4747
+ * ```typescript
4748
+ * const tags = await loader.getHarness(TnChipInputHarness.with({ testId: 'chip-input-tags' }));
4749
+ * await tags.addChip('production');
4750
+ * expect(await tags.getChips()).toEqual(['production']);
4751
+ * await tags.removeChip('production');
4752
+ * ```
4753
+ */
4754
+ class TnChipInputHarness extends ComponentHarness {
4755
+ static hostSelector = 'tn-chip-input';
4756
+ _input = this.locatorFor('.tn-chip-input__field');
4757
+ getChipHarnesses = this.locatorForAll(TnChipHarness);
4758
+ /** Suggestion options live in a CDK overlay on the document root. */
4759
+ documentRoot = this.documentRootLocatorFactory();
4760
+ static with(options = {}) {
4761
+ return new HarnessPredicate(TnChipInputHarness, options)
4762
+ .addOption('testId', options.testId, async (harness, testId) => (await (await harness._input()).getAttribute('data-testid')) === testId
4763
+ || (await (await harness._input()).getAttribute('data-test')) === testId);
4764
+ }
4765
+ /** Labels of the currently committed chips, in order. */
4766
+ async getChips() {
4767
+ const chips = await this.getChipHarnesses();
4768
+ const labels = [];
4769
+ for (const chip of chips) {
4770
+ labels.push(await chip.getLabel());
4771
+ }
4772
+ return labels;
4773
+ }
4774
+ /** Types `value` into the field and commits it with Enter. */
4775
+ async addChip(value) {
4776
+ const input = await this._input();
4777
+ await input.focus();
4778
+ await input.setInputValue(value);
4779
+ await input.dispatchEvent('input');
4780
+ await input.sendKeys(TestKey.ENTER);
4781
+ }
4782
+ /** Types `value` into the field without committing it. */
4783
+ async typeText(value) {
4784
+ const input = await this._input();
4785
+ await input.focus();
4786
+ await input.setInputValue(value);
4787
+ await input.dispatchEvent('input');
4788
+ }
4789
+ /** Whether the text field currently holds DOM focus. */
4790
+ async isInputFocused() {
4791
+ return (await this._input()).isFocused();
4792
+ }
4793
+ /** Focuses the text field (opens the dropdown when there are suggestions). */
4794
+ async focus() {
4795
+ await (await this._input()).focus();
4796
+ }
4797
+ /** Blurs the text field. */
4798
+ async blur() {
4799
+ await (await this._input()).blur();
4800
+ }
4801
+ /** Sends a key (or `TestKey`) to the focused field — e.g. Backspace, arrows, a separator. */
4802
+ async pressKey(key) {
4803
+ await (await this._input()).sendKeys(key);
4804
+ }
4805
+ /** Types `value`, then commits the matching suggestion from the dropdown. */
4806
+ async selectSuggestion(value) {
4807
+ const input = await this._input();
4808
+ await input.focus();
4809
+ await input.setInputValue(value);
4810
+ await input.dispatchEvent('input');
4811
+ const options = await this.documentRoot.locatorForAll('.tn-chip-input__option')();
4812
+ for (const option of options) {
4813
+ if ((await option.text()).trim() === value) {
4814
+ await option.click();
4815
+ return;
4816
+ }
4817
+ }
4818
+ throw new Error(`No suggestion matching "${value}" was found.`);
4819
+ }
4820
+ /** Removes the chip with the given label via its close button. */
4821
+ async removeChip(value) {
4822
+ const chip = await this.locatorFor(TnChipHarness.with({ label: value }))();
4823
+ await chip.close();
4824
+ }
4825
+ /** Visible suggestion option texts, or `[]` when the dropdown is closed. */
4826
+ async getSuggestions() {
4827
+ const options = await this.documentRoot.locatorForAll('.tn-chip-input__option')();
4828
+ const texts = [];
4829
+ for (const option of options) {
4830
+ texts.push((await option.text()).trim());
4831
+ }
4832
+ return texts;
4833
+ }
4834
+ async isDisabled() {
4835
+ const input = await this._input();
4836
+ return (await input.getProperty('disabled')) === true;
4837
+ }
4838
+ }
4839
+
4840
+ /**
4841
+ * Marks an `<ng-template>` whose content is rendered as the card's footer actions,
4842
+ * bottom-right in the footer after any declarative `secondaryAction`/`primaryAction`
4843
+ * buttons. The template's root nodes become direct children of the footer's flex row,
4844
+ * so projected `<tn-button>`s sit and align exactly like the declarative ones — pass
4845
+ * bare buttons, no wrapper or styling required.
4846
+ *
4847
+ * Reach for this only when an action needs markup the declarative `TnCardAction` config
4848
+ * can't express — most commonly a permission-gated button wrapped in a structural
4849
+ * directive. Because the content renders via `ngTemplateOutlet` in the consumer's
4850
+ * context (not light-DOM projection), structural directives like `*ixRequiresRoles`
4851
+ * work directly on the button with no wrapper element:
4852
+ *
4853
+ * @example
4854
+ * ```html
4855
+ * <tn-card>
4856
+ * <ng-template tnCardFooterActions>
4857
+ * <tn-button *ixRequiresRoles="roles" label="Add" (click)="add()" />
4858
+ * </ng-template>
4859
+ * </tn-card>
4860
+ * ```
4861
+ */
4862
+ class TnCardFooterActionsDirective {
4863
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardFooterActionsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4864
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnCardFooterActionsDirective, isStandalone: true, selector: "ng-template[tnCardFooterActions]", ngImport: i0 });
4865
+ }
4866
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardFooterActionsDirective, decorators: [{
4867
+ type: Directive,
4868
+ args: [{
4869
+ selector: 'ng-template[tnCardFooterActions]',
4870
+ standalone: true,
4871
+ }]
4872
+ }] });
4873
+ /**
4874
+ * Marks an `<ng-template>` whose content is rendered as the card's header actions,
4875
+ * top-right in the header between the header control and the kebab menu. Use it for
4876
+ * header controls the declarative `headerControl`/`headerMenu` config can't express —
4877
+ * e.g. a permission-gated slide toggle. Same template-outlet semantics (and structural
4878
+ * directive support) as {@link TnCardFooterActionsDirective}.
4879
+ *
4880
+ * @example
4881
+ * ```html
4882
+ * <tn-card title="Shares">
4883
+ * <ng-template tnCardHeaderActions>
4884
+ * <tn-slide-toggle *ixRequiresRoles="roles" [checked]="running()" />
4885
+ * </ng-template>
4886
+ * </tn-card>
4887
+ * ```
4888
+ */
4889
+ class TnCardHeaderActionsDirective {
4890
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardHeaderActionsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4891
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnCardHeaderActionsDirective, isStandalone: true, selector: "ng-template[tnCardHeaderActions]", ngImport: i0 });
4892
+ }
4893
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardHeaderActionsDirective, decorators: [{
4894
+ type: Directive,
4895
+ args: [{
4896
+ selector: 'ng-template[tnCardHeaderActions]',
4897
+ standalone: true,
4898
+ }]
4899
+ }] });
4900
+
4371
4901
  class TnCardHeaderDirective {
4372
4902
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4373
4903
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnCardHeaderDirective, isStandalone: true, selector: "[tnCardHeader]", ngImport: i0 });
@@ -5037,6 +5567,12 @@ class TnCardComponent {
5037
5567
  this.registerMdiIcons();
5038
5568
  }
5039
5569
  projectedHeader = contentChild(TnCardHeaderDirective, ...(ngDevMode ? [{ debugName: "projectedHeader" }] : []));
5570
+ // Projected action templates (escape hatch for actions the declarative config can't
5571
+ // express, e.g. a permission-gated control wrapped in a structural directive). Rendered
5572
+ // via ngTemplateOutlet so the buttons inside become direct children of the header/footer
5573
+ // flex rows — same orientation as the declarative action buttons, no wrapper needed.
5574
+ headerActions = contentChild(TnCardHeaderActionsDirective, { ...(ngDevMode ? { debugName: "headerActions" } : {}), read: TemplateRef });
5575
+ footerActions = contentChild(TnCardFooterActionsDirective, { ...(ngDevMode ? { debugName: "footerActions" } : {}), read: TemplateRef });
5040
5576
  title = input(undefined, ...(ngDevMode ? [{ debugName: "title" }] : []));
5041
5577
  titleLink = input(undefined, ...(ngDevMode ? [{ debugName: "titleLink" }] : [])); // Makes title navigable
5042
5578
  elevation = input('medium', ...(ngDevMode ? [{ debugName: "elevation" }] : []));
@@ -5087,13 +5623,16 @@ class TnCardComponent {
5087
5623
  return ['tn-card', elevationClass, paddingClass, contentPaddingClass, borderedClass, backgroundClass].filter(Boolean);
5088
5624
  }, ...(ngDevMode ? [{ debugName: "classes" }] : []));
5089
5625
  hasHeader = computed(() => {
5090
- return !!(this.projectedHeader() || this.title() || this.headerStatus() || this.headerControl() || this.headerMenu());
5626
+ return !!(this.projectedHeader() || this.title() || this.headerStatus()
5627
+ || this.headerControl() || this.headerMenu() || this.headerActions());
5091
5628
  }, ...(ngDevMode ? [{ debugName: "hasHeader" }] : []));
5092
5629
  hasHeaderRight = computed(() => {
5093
- return !!(this.headerStatus() || this.headerControl() || this.headerMenu()?.length);
5630
+ return !!(this.headerStatus() || this.headerControl()
5631
+ || this.headerMenu()?.length || this.headerActions());
5094
5632
  }, ...(ngDevMode ? [{ debugName: "hasHeaderRight" }] : []));
5095
5633
  hasFooter = computed(() => {
5096
- return !!(this.primaryAction() || this.secondaryAction() || this.footerLink());
5634
+ return !!(this.primaryAction() || this.secondaryAction()
5635
+ || this.footerLink() || this.footerActions());
5097
5636
  }, ...(ngDevMode ? [{ debugName: "hasFooter" }] : []));
5098
5637
  onTitleClick() {
5099
5638
  const link = this.titleLink();
@@ -5114,7 +5653,7 @@ class TnCardComponent {
5114
5653
  return type ? `tn-card__status--${type}` : 'tn-card__status--neutral';
5115
5654
  }
5116
5655
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5117
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCardComponent, isStandalone: true, selector: "tn-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, titleLink: { classPropertyName: "titleLink", publicName: "titleLink", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, padContent: { classPropertyName: "padContent", publicName: "padContent", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, headerStatus: { classPropertyName: "headerStatus", publicName: "headerStatus", isSignal: true, isRequired: false, transformFunction: null }, headerControl: { classPropertyName: "headerControl", publicName: "headerControl", isSignal: true, isRequired: false, transformFunction: null }, headerMenu: { classPropertyName: "headerMenu", publicName: "headerMenu", isSignal: true, isRequired: false, transformFunction: null }, headerMenuTriggerTestId: { classPropertyName: "headerMenuTriggerTestId", publicName: "headerMenuTriggerTestId", isSignal: true, isRequired: false, transformFunction: null }, primaryAction: { classPropertyName: "primaryAction", publicName: "primaryAction", isSignal: true, isRequired: false, transformFunction: null }, secondaryAction: { classPropertyName: "secondaryAction", publicName: "secondaryAction", isSignal: true, isRequired: false, transformFunction: null }, footerLink: { classPropertyName: "footerLink", publicName: "footerLink", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "projectedHeader", first: true, predicate: TnCardHeaderDirective, descendants: true, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3\n class=\"tn-card__title\"\n [class.tn-card__title--link]=\"titleLink()\"\n [attr.tabindex]=\"titleLink() ? 0 : null\"\n [attr.role]=\"titleLink() ? 'button' : null\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick()\">\n {{ title() }}\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title--link{cursor:pointer;transition:color .2s ease}.tn-card__title--link:hover{color:var(--tn-primary, #2563eb)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSlideToggleComponent, selector: "tn-slide-toggle", inputs: ["labelPosition", "label", "disabled", "required", "color", "testId", "ariaLabel", "ariaLabelledby", "checked"], outputs: ["change", "toggleChange"] }, { kind: "component", type: TnMenuComponent, selector: "tn-menu", inputs: ["items", "contextMenu", "testId"], outputs: ["menuItemClick", "menuOpen", "menuClose"] }, { kind: "directive", type: TnMenuTriggerDirective, selector: "[tnMenuTriggerFor]", inputs: ["tnMenuTriggerFor", "tnMenuPosition"], exportAs: ["tnMenuTrigger"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
5656
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCardComponent, isStandalone: true, selector: "tn-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, titleLink: { classPropertyName: "titleLink", publicName: "titleLink", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, padContent: { classPropertyName: "padContent", publicName: "padContent", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, headerStatus: { classPropertyName: "headerStatus", publicName: "headerStatus", isSignal: true, isRequired: false, transformFunction: null }, headerControl: { classPropertyName: "headerControl", publicName: "headerControl", isSignal: true, isRequired: false, transformFunction: null }, headerMenu: { classPropertyName: "headerMenu", publicName: "headerMenu", isSignal: true, isRequired: false, transformFunction: null }, headerMenuTriggerTestId: { classPropertyName: "headerMenuTriggerTestId", publicName: "headerMenuTriggerTestId", isSignal: true, isRequired: false, transformFunction: null }, primaryAction: { classPropertyName: "primaryAction", publicName: "primaryAction", isSignal: true, isRequired: false, transformFunction: null }, secondaryAction: { classPropertyName: "secondaryAction", publicName: "secondaryAction", isSignal: true, isRequired: false, transformFunction: null }, footerLink: { classPropertyName: "footerLink", publicName: "footerLink", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "projectedHeader", first: true, predicate: TnCardHeaderDirective, descendants: true, isSignal: true }, { propertyName: "headerActions", first: true, predicate: TnCardHeaderActionsDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "footerActions", first: true, predicate: TnCardFooterActionsDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3\n class=\"tn-card__title\"\n [class.tn-card__title--link]=\"titleLink()\"\n [attr.tabindex]=\"titleLink() ? 0 : null\"\n [attr.role]=\"titleLink() ? 'button' : null\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick()\">\n {{ title() }}\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title--link{cursor:pointer;transition:color .2s ease}.tn-card__title--link:hover{color:var(--tn-primary, #2563eb)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSlideToggleComponent, selector: "tn-slide-toggle", inputs: ["labelPosition", "label", "disabled", "required", "color", "testId", "ariaLabel", "ariaLabelledby", "checked"], outputs: ["change", "toggleChange"] }, { kind: "component", type: TnMenuComponent, selector: "tn-menu", inputs: ["items", "contextMenu", "testId"], outputs: ["menuItemClick", "menuOpen", "menuClose"] }, { kind: "directive", type: TnMenuTriggerDirective, selector: "[tnMenuTriggerFor]", inputs: ["tnMenuTriggerFor", "tnMenuPosition"], exportAs: ["tnMenuTrigger"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
5118
5657
  }
5119
5658
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, decorators: [{
5120
5659
  type: Component,
@@ -5127,8 +5666,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
5127
5666
  TnMenuComponent,
5128
5667
  TnMenuTriggerDirective,
5129
5668
  TnTestIdDirective,
5130
- ], template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3\n class=\"tn-card__title\"\n [class.tn-card__title--link]=\"titleLink()\"\n [attr.tabindex]=\"titleLink() ? 0 : null\"\n [attr.role]=\"titleLink() ? 'button' : null\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick()\">\n {{ title() }}\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title--link{cursor:pointer;transition:color .2s ease}.tn-card__title--link:hover{color:var(--tn-primary, #2563eb)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"] }]
5131
- }], ctorParameters: () => [], propDecorators: { projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardHeaderDirective), { isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], titleLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleLink", required: false }] }], elevation: [{ type: i0.Input, args: [{ isSignal: true, alias: "elevation", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], padContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "padContent", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], background: [{ type: i0.Input, args: [{ isSignal: true, alias: "background", required: false }] }], headerStatus: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerStatus", required: false }] }], headerControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerControl", required: false }] }], headerMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMenu", required: false }] }], headerMenuTriggerTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMenuTriggerTestId", required: false }] }], primaryAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryAction", required: false }] }], secondaryAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryAction", required: false }] }], footerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerLink", required: false }] }] } });
5669
+ ], template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3\n class=\"tn-card__title\"\n [class.tn-card__title--link]=\"titleLink()\"\n [attr.tabindex]=\"titleLink() ? 0 : null\"\n [attr.role]=\"titleLink() ? 'button' : null\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick()\">\n {{ title() }}\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title--link{cursor:pointer;transition:color .2s ease}.tn-card__title--link:hover{color:var(--tn-primary, #2563eb)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"] }]
5670
+ }], ctorParameters: () => [], propDecorators: { projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardHeaderDirective), { isSignal: true }] }], headerActions: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardHeaderActionsDirective), { ...{ read: TemplateRef }, isSignal: true }] }], footerActions: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardFooterActionsDirective), { ...{ read: TemplateRef }, isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], titleLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleLink", required: false }] }], elevation: [{ type: i0.Input, args: [{ isSignal: true, alias: "elevation", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], padContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "padContent", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], background: [{ type: i0.Input, args: [{ isSignal: true, alias: "background", required: false }] }], headerStatus: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerStatus", required: false }] }], headerControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerControl", required: false }] }], headerMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMenu", required: false }] }], headerMenuTriggerTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMenuTriggerTestId", required: false }] }], primaryAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryAction", required: false }] }], secondaryAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryAction", required: false }] }], footerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerLink", required: false }] }] } });
5132
5671
 
5133
5672
  class TnExpansionPanelComponent {
5134
5673
  title = input(undefined, ...(ngDevMode ? [{ debugName: "title" }] : []));
@@ -16442,5 +16981,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
16442
16981
  * Generated bundle index. Do not edit.
16443
16982
  */
16444
16983
 
16445
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, 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, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, composeTestId, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, formatSize, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
16984
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, 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, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, composeTestId, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, formatSize, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
16446
16985
  //# sourceMappingURL=truenas-ui-components.mjs.map