@wildmason/aegis 1.5.4 → 1.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, HostListener, ChangeDetectionStrategy, Component, model, signal, computed, inject, ElementRef, Renderer2, ChangeDetectorRef, viewChild, effect, ViewEncapsulation, Input, Directive, contentChildren, forwardRef, Injectable } from '@angular/core';
2
+ import { input, output, HostListener, ChangeDetectionStrategy, Component, model, signal, computed, inject, ElementRef, Renderer2, ChangeDetectorRef, viewChild, effect, ViewEncapsulation, Input, Directive, contentChildren, forwardRef, ViewChild, Injectable } from '@angular/core';
3
3
  import { CdkTrapFocus } from '@angular/cdk/a11y';
4
4
  import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
5
5
  import { NgTemplateOutlet } from '@angular/common';
6
6
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
7
+ import { Subject, debounceTime, switchMap, of, catchError, finalize } from 'rxjs';
8
+ import * as i1 from '@angular/cdk/scrolling';
9
+ import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
7
10
  import { invoke } from '@tauri-apps/api/core';
8
11
 
9
12
  /**
@@ -659,6 +662,656 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImpor
659
662
  args: [{ selector: 'wm-page-header', standalone: true, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<header class=\"wm-page-header\">\r\n <div class=\"wm-page-header__left\">\r\n <div class=\"wm-page-header__eyebrow\">\r\n <ng-content select=\"[icon]\" />\r\n <span>{{ flavourText() }}</span>\r\n </div>\r\n <h2 class=\"wm-page-header__title\">{{ title() }}</h2>\r\n @if (subtitle()) {\r\n <p class=\"wm-page-header__subtitle\">{{ subtitle() }}</p>\r\n }\r\n </div>\r\n\r\n <div class=\"wm-page-header__right\">\r\n <div class=\"wm-page-header__status\">\r\n @if (badgeLabel()) {\r\n <span class=\"wm-page-header__badge\">{{ badgeLabel() }}</span>\r\n }\r\n <ng-content select=\"[back]\" />\r\n </div>\r\n <div class=\"wm-page-header__actions\">\r\n <ng-content />\r\n </div>\r\n </div>\r\n</header>\r\n", styles: ["wm-page-header,.wm-page-header{display:flex;align-items:flex-end;justify-content:space-between;border-bottom:1px solid var(--wm-border-subtle);padding-bottom:1.5rem}.wm-page-header__left{display:flex;flex-direction:column;gap:4px}.wm-page-header__eyebrow{display:flex;align-items:center;gap:8px;font-size:.6875rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--wm-text-muted)}.wm-page-header__eyebrow svg,.wm-page-header__eyebrow img{width:12px;height:12px;flex-shrink:0}.wm-page-header__title{margin:0;font-size:1.5rem;font-weight:700;letter-spacing:-.025em;color:var(--wm-text-primary)}.wm-page-header__subtitle{margin:0;font-size:.875rem;color:var(--wm-text-muted)}.wm-page-header__right{display:flex;flex-direction:column;align-items:flex-end;gap:12px}.wm-page-header__status,.wm-page-header__actions{display:flex;align-items:center;gap:12px}.wm-page-header__actions:empty{display:none}.wm-page-header__badge{font-size:.6875rem;font-family:monospace;font-weight:700;text-transform:uppercase;letter-spacing:-.05em;color:var(--wm-text-muted);background:var(--wm-bg-float);padding:4px 8px;border-radius:var(--wm-radius-sm);border:1px solid var(--wm-border-subtle);box-shadow:0 1px 2px #0000000d;white-space:nowrap}@media(max-width:767px){wm-page-header,.wm-page-header{flex-direction:column;align-items:stretch;gap:6px;padding-bottom:0;border-bottom:none}.wm-page-header__right{align-items:flex-start}}\n"] }]
660
663
  }], propDecorators: { flavourText: [{ type: i0.Input, args: [{ isSignal: true, alias: "flavourText", required: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], badgeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeLabel", required: false }] }] } });
661
664
 
665
+ /** Module-level counter for unique panel instance IDs. */
666
+ let nextPanelId = 0;
667
+ /**
668
+ * Internal sub-component that renders the virtual-scrolled option list
669
+ * inside the WmCombobox's CDK overlay. NOT exported from Aegis.
670
+ */
671
+ class ComboboxPanel {
672
+ /** Filtered options to display. */
673
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
674
+ /** Index of the keyboard-highlighted option (-1 = none). */
675
+ highlightIndex = input(-1, ...(ngDevMode ? [{ debugName: "highlightIndex" }] : []));
676
+ /** Values currently selected (for checkmark display). */
677
+ selectedValues = input(new Set(), ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
678
+ /** Whether the parent is multi-select. */
679
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
680
+ /** Pixel height per row. */
681
+ itemHeight = input(36, ...(ngDevMode ? [{ debugName: "itemHeight" }] : []));
682
+ /** Max panel height in pixels. */
683
+ maxHeight = input(256, ...(ngDevMode ? [{ debugName: "maxHeight" }] : []));
684
+ /** Current filter text — used for match highlighting in labels. */
685
+ filterQuery = input('', ...(ngDevMode ? [{ debugName: "filterQuery" }] : []));
686
+ /** Show spinner when async filter is pending. */
687
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
688
+ /** Shown in empty state when freeform input is enabled. */
689
+ freeformHint = input('', ...(ngDevMode ? [{ debugName: "freeformHint" }] : []));
690
+ /** Emitted on mousedown (not click) to prevent focus steal. */
691
+ optionClicked = output();
692
+ viewport;
693
+ /** Unique instance ID for generating DOM IDs. */
694
+ instanceId = nextPanelId++;
695
+ /** The DOM ID of the listbox element — used by parent for aria-controls. */
696
+ listboxId = `wm-combobox-${this.instanceId}-listbox`;
697
+ /** Computed viewport height: min of (options * itemHeight) and maxHeight. */
698
+ viewportHeight = computed(() => {
699
+ const count = this.options().length;
700
+ return Math.min(count * this.itemHeight(), this.maxHeight());
701
+ }, ...(ngDevMode ? [{ debugName: "viewportHeight" }] : []));
702
+ /** Whether to show the empty state. */
703
+ showEmpty = computed(() => this.options().length === 0 && !this.loading(), ...(ngDevMode ? [{ debugName: "showEmpty" }] : []));
704
+ /** Text for the empty state. */
705
+ emptyText = computed(() => this.freeformHint() || 'No matches', ...(ngDevMode ? [{ debugName: "emptyText" }] : []));
706
+ /**
707
+ * Precomputed label segments for each option, split by the current filter query.
708
+ * Recomputes whenever options or filterQuery changes.
709
+ */
710
+ segmentedLabels = computed(() => {
711
+ const query = this.filterQuery().trim();
712
+ return this.options().map(opt => this.splitLabel(opt.label, query));
713
+ }, ...(ngDevMode ? [{ debugName: "segmentedLabels" }] : []));
714
+ /** Map from option value to a stable key for DOM IDs. */
715
+ optionKeyMap = new Map();
716
+ nextObjectIndex = 0;
717
+ constructor() {
718
+ // Scroll the highlighted option into view whenever highlightIndex changes.
719
+ effect(() => {
720
+ const idx = this.highlightIndex();
721
+ if (idx >= 0 && this.viewport) {
722
+ this.viewport.scrollToIndex(idx);
723
+ }
724
+ });
725
+ }
726
+ /** Returns a stable DOM ID for the option at the given index. */
727
+ optionIdAt(index) {
728
+ const opts = this.options();
729
+ if (index < 0 || index >= opts.length) {
730
+ return '';
731
+ }
732
+ return `wm-combobox-${this.instanceId}-opt-${this.keyFor(opts[index].value)}`;
733
+ }
734
+ /** Handle mousedown on an option — emits event and prevents default. */
735
+ onOptionMousedown(event, option, index) {
736
+ event.preventDefault();
737
+ if (!option.disabled) {
738
+ this.optionClicked.emit({ option, index });
739
+ }
740
+ }
741
+ /** Check whether a value is currently selected. */
742
+ isSelected(value) {
743
+ return this.selectedValues().has(value);
744
+ }
745
+ /** Get label segments for the option at the given virtual-scroll index. */
746
+ segmentsAt(index) {
747
+ return this.segmentedLabels()[index] ?? [{ text: '', match: false }];
748
+ }
749
+ /**
750
+ * Returns a stable string key for an option value.
751
+ * Strings/numbers use the value directly. Objects use a monotonic index.
752
+ */
753
+ keyFor(value) {
754
+ if (typeof value === 'string' || typeof value === 'number') {
755
+ return String(value);
756
+ }
757
+ let key = this.optionKeyMap.get(value);
758
+ if (key == null) {
759
+ key = `obj-${this.nextObjectIndex++}`;
760
+ this.optionKeyMap.set(value, key);
761
+ }
762
+ return key;
763
+ }
764
+ /**
765
+ * Split a label into segments where the query matches (case-insensitive).
766
+ * Returns an array of { text, match } segments.
767
+ */
768
+ splitLabel(label, query) {
769
+ if (!query) {
770
+ return [{ text: label, match: false }];
771
+ }
772
+ const segments = [];
773
+ const lowerLabel = label.toLowerCase();
774
+ const lowerQuery = query.toLowerCase();
775
+ let cursor = 0;
776
+ while (cursor < label.length) {
777
+ const matchIndex = lowerLabel.indexOf(lowerQuery, cursor);
778
+ if (matchIndex === -1) {
779
+ segments.push({ text: label.slice(cursor), match: false });
780
+ break;
781
+ }
782
+ if (matchIndex > cursor) {
783
+ segments.push({ text: label.slice(cursor, matchIndex), match: false });
784
+ }
785
+ segments.push({
786
+ text: label.slice(matchIndex, matchIndex + query.length),
787
+ match: true,
788
+ });
789
+ cursor = matchIndex + query.length;
790
+ }
791
+ return segments;
792
+ }
793
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ComboboxPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
794
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.1", type: ComboboxPanel, isStandalone: true, selector: "wm-combobox-panel", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, highlightIndex: { classPropertyName: "highlightIndex", publicName: "highlightIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedValues: { classPropertyName: "selectedValues", publicName: "selectedValues", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, itemHeight: { classPropertyName: "itemHeight", publicName: "itemHeight", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, filterQuery: { classPropertyName: "filterQuery", publicName: "filterQuery", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, freeformHint: { classPropertyName: "freeformHint", publicName: "freeformHint", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionClicked: "optionClicked" }, viewQueries: [{ propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true }], ngImport: i0, template: "@if (loading()) {\n <div class=\"wm-cb-loading\" aria-live=\"polite\">\n <svg\n class=\"wm-cb-spinner\"\n width=\"20\" height=\"20\" viewBox=\"0 0 20 20\"\n fill=\"none\" aria-hidden=\"true\"\n >\n <circle\n cx=\"10\" cy=\"10\" r=\"8\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-dasharray=\"40 60\"\n stroke-linecap=\"round\"\n />\n </svg>\n <span class=\"wm-cb-loading-text\">Loading\u2026</span>\n </div>\n} @else if (showEmpty()) {\n <div class=\"wm-cb-empty\" aria-live=\"polite\">{{ emptyText() }}</div>\n} @else {\n <cdk-virtual-scroll-viewport\n [itemSize]=\"itemHeight()\"\n [style.height.px]=\"viewportHeight()\"\n class=\"wm-cb-viewport\"\n >\n <div\n [id]=\"listboxId\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple() ? true : null\"\n >\n <div\n *cdkVirtualFor=\"let opt of options(); let i = index\"\n role=\"option\"\n class=\"wm-cb-option\"\n [id]=\"optionIdAt(i)\"\n [class.is-highlighted]=\"i === highlightIndex()\"\n [class.is-selected]=\"isSelected(opt.value)\"\n [class.is-disabled]=\"!!opt.disabled\"\n [attr.aria-selected]=\"isSelected(opt.value)\"\n [attr.aria-disabled]=\"opt.disabled ? true : null\"\n (mousedown)=\"onOptionMousedown($event, opt, i)\"\n >\n <span class=\"wm-cb-option-label\">\n @for (seg of segmentsAt(i); track $index) {\n @if (seg.match) {\n <b class=\"wm-cb-match\">{{ seg.text }}</b>\n } @else {\n {{ seg.text }}\n }\n }\n </span>\n @if (isSelected(opt.value)) {\n <svg\n class=\"wm-cb-check\"\n width=\"14\" height=\"14\" viewBox=\"0 0 24 24\"\n fill=\"none\" aria-hidden=\"true\"\n >\n <polyline\n points=\"20 6 9 17 4 12\"\n stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"\n />\n </svg>\n }\n </div>\n </div>\n </cdk-virtual-scroll-viewport>\n}\n", styles: [":host{display:block}.wm-cb-viewport{width:100%}.wm-cb-option{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4375rem .625rem;border:none;border-radius:var(--wm-radius-sm);background:transparent;color:var(--wm-text-primary);font-size:.875rem;font-family:inherit;cursor:pointer;text-align:left;transition:background var(--wm-transition-fast);-webkit-user-select:none;user-select:none}.wm-cb-option:hover:not(.is-disabled):not(.is-highlighted){background:var(--wm-bg-active)}.wm-cb-option.is-highlighted{background:color-mix(in srgb,var(--wm-color-accent) 15%,transparent)}.wm-cb-option.is-selected{background:color-mix(in srgb,var(--wm-color-accent) 10%,transparent);color:var(--wm-color-accent);font-weight:500}.wm-cb-option.is-disabled{opacity:.4;cursor:not-allowed}.wm-cb-option-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wm-cb-match{font-weight:700;color:var(--wm-color-accent)}.wm-cb-check{flex-shrink:0;color:var(--wm-color-accent)}.wm-cb-empty{padding:.75rem .625rem;color:var(--wm-text-muted);font-size:.875rem;text-align:center}.wm-cb-loading{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem .625rem;color:var(--wm-text-muted);font-size:.875rem}.wm-cb-spinner{animation:wm-cb-spin .8s linear infinite}@keyframes wm-cb-spin{to{transform:rotate(360deg)}}.wm-cb-loading-text{color:var(--wm-text-muted)}\n"], dependencies: [{ kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i1.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i1.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i1.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
795
+ }
796
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ComboboxPanel, decorators: [{
797
+ type: Component,
798
+ args: [{ selector: 'wm-combobox-panel', imports: [ScrollingModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (loading()) {\n <div class=\"wm-cb-loading\" aria-live=\"polite\">\n <svg\n class=\"wm-cb-spinner\"\n width=\"20\" height=\"20\" viewBox=\"0 0 20 20\"\n fill=\"none\" aria-hidden=\"true\"\n >\n <circle\n cx=\"10\" cy=\"10\" r=\"8\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-dasharray=\"40 60\"\n stroke-linecap=\"round\"\n />\n </svg>\n <span class=\"wm-cb-loading-text\">Loading\u2026</span>\n </div>\n} @else if (showEmpty()) {\n <div class=\"wm-cb-empty\" aria-live=\"polite\">{{ emptyText() }}</div>\n} @else {\n <cdk-virtual-scroll-viewport\n [itemSize]=\"itemHeight()\"\n [style.height.px]=\"viewportHeight()\"\n class=\"wm-cb-viewport\"\n >\n <div\n [id]=\"listboxId\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple() ? true : null\"\n >\n <div\n *cdkVirtualFor=\"let opt of options(); let i = index\"\n role=\"option\"\n class=\"wm-cb-option\"\n [id]=\"optionIdAt(i)\"\n [class.is-highlighted]=\"i === highlightIndex()\"\n [class.is-selected]=\"isSelected(opt.value)\"\n [class.is-disabled]=\"!!opt.disabled\"\n [attr.aria-selected]=\"isSelected(opt.value)\"\n [attr.aria-disabled]=\"opt.disabled ? true : null\"\n (mousedown)=\"onOptionMousedown($event, opt, i)\"\n >\n <span class=\"wm-cb-option-label\">\n @for (seg of segmentsAt(i); track $index) {\n @if (seg.match) {\n <b class=\"wm-cb-match\">{{ seg.text }}</b>\n } @else {\n {{ seg.text }}\n }\n }\n </span>\n @if (isSelected(opt.value)) {\n <svg\n class=\"wm-cb-check\"\n width=\"14\" height=\"14\" viewBox=\"0 0 24 24\"\n fill=\"none\" aria-hidden=\"true\"\n >\n <polyline\n points=\"20 6 9 17 4 12\"\n stroke=\"currentColor\" stroke-width=\"2.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"\n />\n </svg>\n }\n </div>\n </div>\n </cdk-virtual-scroll-viewport>\n}\n", styles: [":host{display:block}.wm-cb-viewport{width:100%}.wm-cb-option{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4375rem .625rem;border:none;border-radius:var(--wm-radius-sm);background:transparent;color:var(--wm-text-primary);font-size:.875rem;font-family:inherit;cursor:pointer;text-align:left;transition:background var(--wm-transition-fast);-webkit-user-select:none;user-select:none}.wm-cb-option:hover:not(.is-disabled):not(.is-highlighted){background:var(--wm-bg-active)}.wm-cb-option.is-highlighted{background:color-mix(in srgb,var(--wm-color-accent) 15%,transparent)}.wm-cb-option.is-selected{background:color-mix(in srgb,var(--wm-color-accent) 10%,transparent);color:var(--wm-color-accent);font-weight:500}.wm-cb-option.is-disabled{opacity:.4;cursor:not-allowed}.wm-cb-option-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wm-cb-match{font-weight:700;color:var(--wm-color-accent)}.wm-cb-check{flex-shrink:0;color:var(--wm-color-accent)}.wm-cb-empty{padding:.75rem .625rem;color:var(--wm-text-muted);font-size:.875rem;text-align:center}.wm-cb-loading{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem .625rem;color:var(--wm-text-muted);font-size:.875rem}.wm-cb-spinner{animation:wm-cb-spin .8s linear infinite}@keyframes wm-cb-spin{to{transform:rotate(360deg)}}.wm-cb-loading-text{color:var(--wm-text-muted)}\n"] }]
799
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], highlightIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightIndex", required: false }] }], selectedValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedValues", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], filterQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterQuery", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], freeformHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "freeformHint", required: false }] }], optionClicked: [{ type: i0.Output, args: ["optionClicked"] }], viewport: [{
800
+ type: ViewChild,
801
+ args: [CdkVirtualScrollViewport]
802
+ }] } });
803
+
804
+ /**
805
+ * WmCombobox — Accessible combobox with filtering, single/multi-select,
806
+ * freeform entry, chips, and virtual-scrolled panel.
807
+ *
808
+ * Built on Angular CDK Overlay and the WAI-ARIA APG combobox pattern.
809
+ * DOM focus ALWAYS stays on the input; assistive tech focus moves via
810
+ * `aria-activedescendant`.
811
+ *
812
+ * Implements ControlValueAccessor for both template-driven and reactive forms.
813
+ *
814
+ * Usage:
815
+ * <!-- Single-select -->
816
+ * <wm-combobox [options]="opts" [(value)]="selected" />
817
+ *
818
+ * <!-- Multi-select with chips -->
819
+ * <wm-combobox [options]="opts" [(values)]="selectedIds" [multiple]="true" />
820
+ *
821
+ * <!-- Async filter -->
822
+ * <wm-combobox [options]="opts" [filterFn]="searchApi" />
823
+ *
824
+ * <!-- Reactive form -->
825
+ * <wm-combobox formControlName="tag" [options]="tags" />
826
+ */
827
+ class WmCombobox {
828
+ // ── Public inputs ──────────────────────────────────────────────────────
829
+ /** Full option list (before filtering). */
830
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
831
+ /** Selected value (single-select). Two-way bindable. */
832
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
833
+ /** Selected values (multi-select). Two-way bindable. */
834
+ values = model([], ...(ngDevMode ? [{ debugName: "values" }] : []));
835
+ /** Enable multi-select mode with chips. */
836
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
837
+ /** Allow typed text to be accepted as a freeform value. */
838
+ allowFreeform = input(false, ...(ngDevMode ? [{ debugName: "allowFreeform" }] : []));
839
+ /** Placeholder text for the input. */
840
+ placeholder = input('Search\u2026', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
841
+ /** Disable the combobox. */
842
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
843
+ /** Custom async filter function. Overrides default substring matching. */
844
+ filterFn = input(null, ...(ngDevMode ? [{ debugName: "filterFn" }] : []));
845
+ /** Pixel height of each option row. */
846
+ itemHeight = input(36, ...(ngDevMode ? [{ debugName: "itemHeight" }] : []));
847
+ /** Max panel height in pixels. */
848
+ maxPanelHeight = input(256, ...(ngDevMode ? [{ debugName: "maxPanelHeight" }] : []));
849
+ // ── Public outputs ─────────────────────────────────────────────────────
850
+ /** Emitted when the input text changes. */
851
+ query = output();
852
+ /** Emitted when the panel opens. */
853
+ opened = output();
854
+ /** Emitted when the panel closes. */
855
+ closed = output();
856
+ // ── View children ──────────────────────────────────────────────────────
857
+ inputRef = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputRef" }] : []));
858
+ panel = viewChild(ComboboxPanel, ...(ngDevMode ? [{ debugName: "panel" }] : []));
859
+ // ── Internal state ─────────────────────────────────────────────────────
860
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
861
+ inputText = signal('', ...(ngDevMode ? [{ debugName: "inputText" }] : []));
862
+ highlightIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightIndex" }] : []));
863
+ asyncLoading = signal(false, ...(ngDevMode ? [{ debugName: "asyncLoading" }] : []));
864
+ asyncResults = signal(null, ...(ngDevMode ? [{ debugName: "asyncResults" }] : []));
865
+ /** Text the input had when the panel opened — restored on Escape. */
866
+ savedText = '';
867
+ hostEl = inject((ElementRef));
868
+ // ── CVA state ──────────────────────────────────────────────────────────
869
+ onChangeFn = () => { };
870
+ onTouchedFn = () => { };
871
+ // ── Async filter plumbing ──────────────────────────────────────────────
872
+ filterSubject = new Subject();
873
+ filterSub = null;
874
+ // ── Live region debounce ───────────────────────────────────────────────
875
+ liveText = signal('', ...(ngDevMode ? [{ debugName: "liveText" }] : []));
876
+ liveTimer = null;
877
+ // ── Computed ───────────────────────────────────────────────────────────
878
+ /** Set of currently selected values for the panel. */
879
+ selectedValues = computed(() => {
880
+ if (this.multiple()) {
881
+ return new Set(this.values());
882
+ }
883
+ const v = this.value();
884
+ return v != null ? new Set([v]) : new Set();
885
+ }, ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
886
+ /** Filtered options shown in the panel. */
887
+ filteredOptions = computed(() => {
888
+ // If async filterFn is set and we have results, use those.
889
+ const asyncRes = this.asyncResults();
890
+ if (this.filterFn() && asyncRes !== null) {
891
+ return asyncRes;
892
+ }
893
+ const query = this.inputText().trim().toLowerCase();
894
+ const opts = this.options();
895
+ if (!query)
896
+ return opts;
897
+ return opts.filter(o => o.label.toLowerCase().includes(query));
898
+ }, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : []));
899
+ /** Chips for multi-select mode. */
900
+ chips = computed(() => {
901
+ if (!this.multiple())
902
+ return [];
903
+ const vals = this.values();
904
+ const opts = this.options();
905
+ return vals.map(v => {
906
+ const opt = opts.find(o => o.value === v);
907
+ return { value: v, label: opt?.label ?? String(v) };
908
+ });
909
+ }, ...(ngDevMode ? [{ debugName: "chips" }] : []));
910
+ /** The aria-activedescendant value. */
911
+ activeDescendant = computed(() => {
912
+ const idx = this.highlightIndex();
913
+ const p = this.panel();
914
+ if (idx < 0 || !p)
915
+ return null;
916
+ return p.optionIdAt(idx) || null;
917
+ }, ...(ngDevMode ? [{ debugName: "activeDescendant" }] : []));
918
+ /** The listbox ID from the panel (for aria-controls). */
919
+ panelListboxId = computed(() => {
920
+ const p = this.panel();
921
+ return p ? p.listboxId : null;
922
+ }, ...(ngDevMode ? [{ debugName: "panelListboxId" }] : []));
923
+ /** Freeform hint for empty state. */
924
+ freeformHint = computed(() => {
925
+ if (!this.allowFreeform())
926
+ return '';
927
+ const text = this.inputText().trim();
928
+ return text ? `Press Enter to add "${text}"` : '';
929
+ }, ...(ngDevMode ? [{ debugName: "freeformHint" }] : []));
930
+ /** Overlay width tracks host element width. */
931
+ get overlayWidth() {
932
+ return this.hostEl.nativeElement.offsetWidth || 200;
933
+ }
934
+ /** Overlay positions: below-first with above-fallback. */
935
+ positions = [
936
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
937
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
938
+ ];
939
+ constructor() {
940
+ // Wire up async filter subscription.
941
+ this.filterSub = this.filterSubject
942
+ .pipe(debounceTime(200), switchMap(q => {
943
+ const fn = this.filterFn();
944
+ if (!fn)
945
+ return of(null);
946
+ this.asyncLoading.set(true);
947
+ return fn(q).pipe(catchError(() => of([])), finalize(() => this.asyncLoading.set(false)));
948
+ }))
949
+ .subscribe(results => {
950
+ if (results !== null) {
951
+ this.asyncResults.set(results);
952
+ }
953
+ });
954
+ // Update live region when filtered options change (debounced 300ms).
955
+ effect(() => {
956
+ const count = this.filteredOptions().length;
957
+ const total = this.options().length;
958
+ // Read isOpen to track it in this effect.
959
+ const open = this.isOpen();
960
+ // Clear any pending timer.
961
+ if (this.liveTimer !== null) {
962
+ clearTimeout(this.liveTimer);
963
+ }
964
+ if (!open) {
965
+ this.liveText.set('');
966
+ return;
967
+ }
968
+ this.liveTimer = setTimeout(() => {
969
+ if (count === 0) {
970
+ this.liveText.set('No results');
971
+ }
972
+ else if (this.filterFn() || this.inputText().trim()) {
973
+ this.liveText.set(total > 0 && count < total
974
+ ? `${count} of ${total} results`
975
+ : `${count} results available`);
976
+ }
977
+ else {
978
+ this.liveText.set(`${count} results available`);
979
+ }
980
+ }, 300);
981
+ });
982
+ }
983
+ ngOnDestroy() {
984
+ this.filterSub?.unsubscribe();
985
+ if (this.liveTimer !== null) {
986
+ clearTimeout(this.liveTimer);
987
+ }
988
+ }
989
+ // ── CVA ────────────────────────────────────────────────────────────────
990
+ writeValue(value) {
991
+ if (this.multiple()) {
992
+ const arr = Array.isArray(value) ? value : [];
993
+ this.values.set(arr);
994
+ }
995
+ else {
996
+ this.value.set(value);
997
+ // Show label for current value.
998
+ const opt = this.options().find(o => o.value === value);
999
+ this.inputText.set(opt?.label ?? '');
1000
+ }
1001
+ }
1002
+ registerOnChange(fn) {
1003
+ this.onChangeFn = fn;
1004
+ }
1005
+ registerOnTouched(fn) {
1006
+ this.onTouchedFn = fn;
1007
+ }
1008
+ // ── Panel open/close ───────────────────────────────────────────────────
1009
+ open(highlightFirst = false, highlightLast = false) {
1010
+ if (this.disabled() || this.isOpen())
1011
+ return;
1012
+ this.savedText = this.inputText();
1013
+ this.isOpen.set(true);
1014
+ this.opened.emit();
1015
+ if (highlightFirst) {
1016
+ this.highlightFirst();
1017
+ }
1018
+ else if (highlightLast) {
1019
+ this.highlightLast();
1020
+ }
1021
+ else {
1022
+ this.highlightIndex.set(-1);
1023
+ }
1024
+ }
1025
+ close(restoreText = false) {
1026
+ if (!this.isOpen())
1027
+ return;
1028
+ this.isOpen.set(false);
1029
+ this.highlightIndex.set(-1);
1030
+ this.closed.emit();
1031
+ if (restoreText) {
1032
+ this.inputText.set(this.savedText);
1033
+ }
1034
+ else if (!this.multiple()) {
1035
+ // Restore label of selected value.
1036
+ this.syncInputToSelection();
1037
+ }
1038
+ }
1039
+ // ── Input events ───────────────────────────────────────────────────────
1040
+ onInput(event) {
1041
+ const text = event.target.value;
1042
+ this.inputText.set(text);
1043
+ this.query.emit(text);
1044
+ this.highlightIndex.set(-1);
1045
+ this.asyncResults.set(null);
1046
+ if (this.filterFn()) {
1047
+ this.filterSubject.next(text);
1048
+ }
1049
+ if (!this.isOpen()) {
1050
+ this.open();
1051
+ }
1052
+ }
1053
+ onBlur() {
1054
+ this.onTouchedFn();
1055
+ // Short delay so mousedown on an option fires first.
1056
+ setTimeout(() => {
1057
+ if (!this.isOpen())
1058
+ return;
1059
+ if (this.allowFreeform() && !this.multiple()) {
1060
+ this.acceptFreeform();
1061
+ }
1062
+ this.close();
1063
+ }, 150);
1064
+ }
1065
+ onTriggerClick() {
1066
+ if (this.disabled())
1067
+ return;
1068
+ if (this.isOpen()) {
1069
+ this.close();
1070
+ }
1071
+ else {
1072
+ this.open();
1073
+ // In single-select, select the input text for easy re-typing.
1074
+ if (!this.multiple() && this.inputText()) {
1075
+ const el = this.inputRef()?.nativeElement;
1076
+ el?.select();
1077
+ }
1078
+ }
1079
+ }
1080
+ // ── Keyboard ───────────────────────────────────────────────────────────
1081
+ onKeydown(event) {
1082
+ if (this.disabled())
1083
+ return;
1084
+ const open = this.isOpen();
1085
+ const hi = this.highlightIndex();
1086
+ const opts = this.filteredOptions();
1087
+ switch (event.key) {
1088
+ case 'ArrowDown':
1089
+ event.preventDefault();
1090
+ if (!open) {
1091
+ if (event.altKey) {
1092
+ this.open(false, false); // Alt+ArrowDown: open without highlight
1093
+ }
1094
+ else {
1095
+ this.open(true, false); // ArrowDown: open + highlight first
1096
+ }
1097
+ }
1098
+ else if (hi < 0) {
1099
+ this.highlightFirst();
1100
+ }
1101
+ else {
1102
+ const next = this.nextEnabled(hi, 1, opts);
1103
+ this.highlightIndex.set(next);
1104
+ }
1105
+ break;
1106
+ case 'ArrowUp':
1107
+ event.preventDefault();
1108
+ if (!open) {
1109
+ this.open(false, true); // ArrowUp: open + highlight last
1110
+ }
1111
+ else if (event.altKey) {
1112
+ this.close();
1113
+ }
1114
+ else if (hi < 0) {
1115
+ this.highlightLast();
1116
+ }
1117
+ else {
1118
+ const prev = this.nextEnabled(hi, -1, opts);
1119
+ this.highlightIndex.set(prev);
1120
+ }
1121
+ break;
1122
+ case 'Enter':
1123
+ if (open) {
1124
+ event.preventDefault();
1125
+ if (hi >= 0 && hi < opts.length) {
1126
+ this.selectOption(opts[hi]);
1127
+ }
1128
+ else if (this.allowFreeform() && this.inputText().trim()) {
1129
+ this.acceptFreeform();
1130
+ this.close();
1131
+ }
1132
+ }
1133
+ break;
1134
+ case 'Escape':
1135
+ if (open) {
1136
+ event.preventDefault();
1137
+ this.close(true); // Restore text
1138
+ }
1139
+ else {
1140
+ // Clear input text.
1141
+ this.inputText.set('');
1142
+ this.query.emit('');
1143
+ }
1144
+ break;
1145
+ case 'Tab':
1146
+ if (open) {
1147
+ if (hi >= 0 && hi < opts.length) {
1148
+ this.selectOption(opts[hi]);
1149
+ }
1150
+ this.close();
1151
+ }
1152
+ break;
1153
+ case 'ArrowLeft':
1154
+ case 'ArrowRight':
1155
+ case 'Home':
1156
+ case 'End':
1157
+ // Clear highlight so cursor navigation works normally.
1158
+ if (open && hi >= 0) {
1159
+ this.highlightIndex.set(-1);
1160
+ }
1161
+ break;
1162
+ case 'Backspace':
1163
+ if (open && hi >= 0) {
1164
+ // Clear highlight, let deletion happen in input.
1165
+ this.highlightIndex.set(-1);
1166
+ }
1167
+ else if (this.multiple() &&
1168
+ !this.inputText() &&
1169
+ this.values().length > 0) {
1170
+ // Remove last chip.
1171
+ this.removeChipByIndex(this.values().length - 1);
1172
+ }
1173
+ break;
1174
+ default:
1175
+ // Printable character: clear highlight, let it type.
1176
+ if (open && hi >= 0 && event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
1177
+ this.highlightIndex.set(-1);
1178
+ }
1179
+ // If closed and printable, open panel.
1180
+ if (!open && event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
1181
+ this.open();
1182
+ }
1183
+ break;
1184
+ }
1185
+ }
1186
+ // ── Option selection ───────────────────────────────────────────────────
1187
+ onOptionClicked(event) {
1188
+ this.selectOption(event.option);
1189
+ // Refocus input since mousedown prevented default.
1190
+ this.inputRef()?.nativeElement.focus();
1191
+ }
1192
+ selectOption(opt) {
1193
+ if (opt.disabled)
1194
+ return;
1195
+ if (this.multiple()) {
1196
+ const current = this.values();
1197
+ const idx = current.indexOf(opt.value);
1198
+ if (idx >= 0) {
1199
+ // Deselect.
1200
+ this.values.set(current.filter((_, i) => i !== idx));
1201
+ }
1202
+ else {
1203
+ this.values.set([...current, opt.value]);
1204
+ }
1205
+ // Clear input, keep panel open.
1206
+ this.inputText.set('');
1207
+ this.query.emit('');
1208
+ this.highlightIndex.set(-1);
1209
+ this.onChangeFn(this.values());
1210
+ }
1211
+ else {
1212
+ this.value.set(opt.value);
1213
+ this.inputText.set(opt.label);
1214
+ this.onChangeFn(opt.value);
1215
+ this.close();
1216
+ }
1217
+ }
1218
+ // ── Chip removal ───────────────────────────────────────────────────────
1219
+ removeChip(chipValue) {
1220
+ const current = this.values();
1221
+ this.values.set(current.filter(v => v !== chipValue));
1222
+ this.onChangeFn(this.values());
1223
+ this.inputRef()?.nativeElement.focus();
1224
+ }
1225
+ removeChipByIndex(index) {
1226
+ const current = this.values();
1227
+ this.values.set(current.filter((_, i) => i !== index));
1228
+ this.onChangeFn(this.values());
1229
+ }
1230
+ // ── Freeform ───────────────────────────────────────────────────────────
1231
+ acceptFreeform() {
1232
+ const text = this.inputText().trim();
1233
+ if (!text)
1234
+ return;
1235
+ if (this.multiple()) {
1236
+ const current = this.values();
1237
+ if (!current.includes(text)) {
1238
+ this.values.set([...current, text]);
1239
+ this.onChangeFn(this.values());
1240
+ }
1241
+ this.inputText.set('');
1242
+ this.query.emit('');
1243
+ }
1244
+ else {
1245
+ this.value.set(text);
1246
+ this.onChangeFn(text);
1247
+ }
1248
+ }
1249
+ // ── Highlight helpers ──────────────────────────────────────────────────
1250
+ highlightFirst() {
1251
+ const opts = this.filteredOptions();
1252
+ for (let i = 0; i < opts.length; i++) {
1253
+ if (!opts[i].disabled) {
1254
+ this.highlightIndex.set(i);
1255
+ return;
1256
+ }
1257
+ }
1258
+ this.highlightIndex.set(-1);
1259
+ }
1260
+ highlightLast() {
1261
+ const opts = this.filteredOptions();
1262
+ for (let i = opts.length - 1; i >= 0; i--) {
1263
+ if (!opts[i].disabled) {
1264
+ this.highlightIndex.set(i);
1265
+ return;
1266
+ }
1267
+ }
1268
+ this.highlightIndex.set(-1);
1269
+ }
1270
+ /**
1271
+ * Find the next enabled option in the given direction.
1272
+ * Returns -1 if wrapping past the end/start (clears highlight).
1273
+ */
1274
+ nextEnabled(from, direction, opts) {
1275
+ let i = from + direction;
1276
+ if (i < 0 || i >= opts.length)
1277
+ return -1; // Wrap clears highlight.
1278
+ while (i >= 0 && i < opts.length) {
1279
+ if (!opts[i].disabled)
1280
+ return i;
1281
+ i += direction;
1282
+ }
1283
+ return -1;
1284
+ }
1285
+ /** Sync input text to the currently selected option label (single-select). */
1286
+ syncInputToSelection() {
1287
+ const v = this.value();
1288
+ if (v == null) {
1289
+ this.inputText.set('');
1290
+ return;
1291
+ }
1292
+ const opt = this.options().find(o => o.value === v);
1293
+ this.inputText.set(opt?.label ?? '');
1294
+ }
1295
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: WmCombobox, deps: [], target: i0.ɵɵFactoryTarget.Component });
1296
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.1", type: WmCombobox, isStandalone: true, selector: "wm-combobox", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, values: { classPropertyName: "values", publicName: "values", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, allowFreeform: { classPropertyName: "allowFreeform", publicName: "allowFreeform", 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 }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, itemHeight: { classPropertyName: "itemHeight", publicName: "itemHeight", isSignal: true, isRequired: false, transformFunction: null }, maxPanelHeight: { classPropertyName: "maxPanelHeight", publicName: "maxPanelHeight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", values: "valuesChange", query: "query", opened: "opened", closed: "closed" }, providers: [
1297
+ {
1298
+ provide: NG_VALUE_ACCESSOR,
1299
+ useExisting: forwardRef(() => WmCombobox),
1300
+ multi: true,
1301
+ },
1302
+ ], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "panel", first: true, predicate: ComboboxPanel, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Trigger wrapper \u2014 anchors the CDK overlay -->\n<div\n class=\"wm-cb-trigger\"\n [class.is-open]=\"isOpen()\"\n [class.is-disabled]=\"disabled()\"\n cdkOverlayOrigin\n #origin=\"cdkOverlayOrigin\"\n (click)=\"onTriggerClick()\"\n>\n <!-- Multi-select chips -->\n @if (multiple()) {\n @for (chip of chips(); track chip.value) {\n <span class=\"wm-cb-chip\">\n <span class=\"wm-cb-chip-label\">{{ chip.label }}</span>\n <button\n type=\"button\"\n class=\"wm-cb-chip-remove\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (mousedown)=\"$event.preventDefault(); $event.stopPropagation()\"\n (click)=\"$event.stopPropagation(); removeChip(chip.value)\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M18 6L6 18M6 6l12 12\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </button>\n </span>\n }\n }\n\n <!-- Text input -->\n <input\n #inputEl\n type=\"text\"\n class=\"wm-cb-input\"\n role=\"combobox\"\n autocomplete=\"off\"\n [attr.placeholder]=\"chips().length ? '' : placeholder()\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-autocomplete]=\"allowFreeform() ? 'both' : 'list'\"\n [attr.aria-activedescendant]=\"activeDescendant()\"\n [attr.aria-controls]=\"isOpen() ? panelListboxId() : null\"\n [disabled]=\"disabled() || null\"\n [value]=\"inputText()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n\n <!-- Chevron -->\n <svg\n class=\"wm-cb-chevron\"\n [class.is-open]=\"isOpen()\"\n width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" aria-hidden=\"true\"\n >\n <path d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n</div>\n\n<!-- Live region for screen readers -->\n<span class=\"wm-cb-sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {{ liveText() }}\n</span>\n\n<!-- Dropdown panel in CDK overlay -->\n<ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"origin\"\n [cdkConnectedOverlayOpen]=\"isOpen()\"\n [cdkConnectedOverlayPositions]=\"positions\"\n [cdkConnectedOverlayWidth]=\"overlayWidth\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"wm-cb-backdrop\"\n (backdropClick)=\"close()\"\n (detach)=\"close()\"\n>\n <div class=\"wm-cb-panel\">\n <wm-combobox-panel\n [options]=\"filteredOptions()\"\n [highlightIndex]=\"highlightIndex()\"\n [selectedValues]=\"selectedValues()\"\n [multiple]=\"multiple()\"\n [itemHeight]=\"itemHeight()\"\n [maxHeight]=\"maxPanelHeight()\"\n [filterQuery]=\"inputText()\"\n [loading]=\"asyncLoading()\"\n [freeformHint]=\"freeformHint()\"\n (optionClicked)=\"onOptionClicked($event)\"\n />\n </div>\n</ng-template>\n", styles: [":host{display:block}.wm-cb-trigger{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem;width:100%;min-height:36px;padding:.25rem .5rem;background:var(--wm-bg-base);border:1px solid var(--wm-border-input);border-radius:var(--wm-radius-sm);color:var(--wm-text-primary);font-size:.875rem;cursor:text;transition:border-color var(--wm-transition-fast),box-shadow var(--wm-transition-fast)}.wm-cb-trigger:hover:not(.is-disabled){border-color:var(--wm-color-accent)}.wm-cb-trigger:focus-within:not(.is-disabled){border-color:var(--wm-color-accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--wm-color-accent) 25%,transparent)}.wm-cb-trigger.is-open{border-color:var(--wm-color-accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--wm-color-accent) 20%,transparent)}.wm-cb-trigger.is-disabled{opacity:.45;cursor:not-allowed}.wm-cb-input{flex:1 1 60px;min-width:60px;border:none;outline:none;background:transparent;color:inherit;font:inherit;padding:.125rem 0}.wm-cb-input::placeholder{color:var(--wm-text-placeholder)}.wm-cb-input:disabled{cursor:not-allowed}.wm-cb-chevron{flex-shrink:0;color:var(--wm-text-muted);transition:transform var(--wm-transition-fast);pointer-events:none}.wm-cb-chevron.is-open{transform:rotate(180deg)}.wm-cb-chip{display:inline-flex;align-items:center;gap:.25rem;max-width:100%;padding:.125rem .375rem;background:color-mix(in srgb,var(--wm-color-accent) 15%,transparent);border:1px solid color-mix(in srgb,var(--wm-color-accent) 30%,transparent);border-radius:var(--wm-radius-sm);font-size:.8125rem;line-height:1.4;color:var(--wm-text-primary);-webkit-user-select:none;user-select:none}.wm-cb-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wm-cb-chip-remove{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--wm-text-muted);cursor:pointer;border-radius:var(--wm-radius-sm);transition:color var(--wm-transition-fast)}.wm-cb-chip-remove:hover{color:var(--wm-text-primary)}.wm-cb-backdrop{position:fixed;inset:0}.wm-cb-panel{width:100%;background:var(--wm-bg-float);border:1px solid var(--wm-border);border-radius:var(--wm-radius-lg);box-shadow:0 4px 6px -1px #00000014,0 10px 20px -4px #0000001f;overflow:hidden;padding:.25rem}.wm-cb-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "component", type: ComboboxPanel, selector: "wm-combobox-panel", inputs: ["options", "highlightIndex", "selectedValues", "multiple", "itemHeight", "maxHeight", "filterQuery", "loading", "freeformHint"], outputs: ["optionClicked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1303
+ }
1304
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: WmCombobox, decorators: [{
1305
+ type: Component,
1306
+ args: [{ selector: 'wm-combobox', imports: [CdkConnectedOverlay, CdkOverlayOrigin, ComboboxPanel], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1307
+ {
1308
+ provide: NG_VALUE_ACCESSOR,
1309
+ useExisting: forwardRef(() => WmCombobox),
1310
+ multi: true,
1311
+ },
1312
+ ], template: "<!-- Trigger wrapper \u2014 anchors the CDK overlay -->\n<div\n class=\"wm-cb-trigger\"\n [class.is-open]=\"isOpen()\"\n [class.is-disabled]=\"disabled()\"\n cdkOverlayOrigin\n #origin=\"cdkOverlayOrigin\"\n (click)=\"onTriggerClick()\"\n>\n <!-- Multi-select chips -->\n @if (multiple()) {\n @for (chip of chips(); track chip.value) {\n <span class=\"wm-cb-chip\">\n <span class=\"wm-cb-chip-label\">{{ chip.label }}</span>\n <button\n type=\"button\"\n class=\"wm-cb-chip-remove\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (mousedown)=\"$event.preventDefault(); $event.stopPropagation()\"\n (click)=\"$event.stopPropagation(); removeChip(chip.value)\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M18 6L6 18M6 6l12 12\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </button>\n </span>\n }\n }\n\n <!-- Text input -->\n <input\n #inputEl\n type=\"text\"\n class=\"wm-cb-input\"\n role=\"combobox\"\n autocomplete=\"off\"\n [attr.placeholder]=\"chips().length ? '' : placeholder()\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-autocomplete]=\"allowFreeform() ? 'both' : 'list'\"\n [attr.aria-activedescendant]=\"activeDescendant()\"\n [attr.aria-controls]=\"isOpen() ? panelListboxId() : null\"\n [disabled]=\"disabled() || null\"\n [value]=\"inputText()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n\n <!-- Chevron -->\n <svg\n class=\"wm-cb-chevron\"\n [class.is-open]=\"isOpen()\"\n width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" aria-hidden=\"true\"\n >\n <path d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n</div>\n\n<!-- Live region for screen readers -->\n<span class=\"wm-cb-sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {{ liveText() }}\n</span>\n\n<!-- Dropdown panel in CDK overlay -->\n<ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"origin\"\n [cdkConnectedOverlayOpen]=\"isOpen()\"\n [cdkConnectedOverlayPositions]=\"positions\"\n [cdkConnectedOverlayWidth]=\"overlayWidth\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"wm-cb-backdrop\"\n (backdropClick)=\"close()\"\n (detach)=\"close()\"\n>\n <div class=\"wm-cb-panel\">\n <wm-combobox-panel\n [options]=\"filteredOptions()\"\n [highlightIndex]=\"highlightIndex()\"\n [selectedValues]=\"selectedValues()\"\n [multiple]=\"multiple()\"\n [itemHeight]=\"itemHeight()\"\n [maxHeight]=\"maxPanelHeight()\"\n [filterQuery]=\"inputText()\"\n [loading]=\"asyncLoading()\"\n [freeformHint]=\"freeformHint()\"\n (optionClicked)=\"onOptionClicked($event)\"\n />\n </div>\n</ng-template>\n", styles: [":host{display:block}.wm-cb-trigger{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem;width:100%;min-height:36px;padding:.25rem .5rem;background:var(--wm-bg-base);border:1px solid var(--wm-border-input);border-radius:var(--wm-radius-sm);color:var(--wm-text-primary);font-size:.875rem;cursor:text;transition:border-color var(--wm-transition-fast),box-shadow var(--wm-transition-fast)}.wm-cb-trigger:hover:not(.is-disabled){border-color:var(--wm-color-accent)}.wm-cb-trigger:focus-within:not(.is-disabled){border-color:var(--wm-color-accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--wm-color-accent) 25%,transparent)}.wm-cb-trigger.is-open{border-color:var(--wm-color-accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--wm-color-accent) 20%,transparent)}.wm-cb-trigger.is-disabled{opacity:.45;cursor:not-allowed}.wm-cb-input{flex:1 1 60px;min-width:60px;border:none;outline:none;background:transparent;color:inherit;font:inherit;padding:.125rem 0}.wm-cb-input::placeholder{color:var(--wm-text-placeholder)}.wm-cb-input:disabled{cursor:not-allowed}.wm-cb-chevron{flex-shrink:0;color:var(--wm-text-muted);transition:transform var(--wm-transition-fast);pointer-events:none}.wm-cb-chevron.is-open{transform:rotate(180deg)}.wm-cb-chip{display:inline-flex;align-items:center;gap:.25rem;max-width:100%;padding:.125rem .375rem;background:color-mix(in srgb,var(--wm-color-accent) 15%,transparent);border:1px solid color-mix(in srgb,var(--wm-color-accent) 30%,transparent);border-radius:var(--wm-radius-sm);font-size:.8125rem;line-height:1.4;color:var(--wm-text-primary);-webkit-user-select:none;user-select:none}.wm-cb-chip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wm-cb-chip-remove{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--wm-text-muted);cursor:pointer;border-radius:var(--wm-radius-sm);transition:color var(--wm-transition-fast)}.wm-cb-chip-remove:hover{color:var(--wm-text-primary)}.wm-cb-backdrop{position:fixed;inset:0}.wm-cb-panel{width:100%;background:var(--wm-bg-float);border:1px solid var(--wm-border);border-radius:var(--wm-radius-lg);box-shadow:0 4px 6px -1px #00000014,0 10px 20px -4px #0000001f;overflow:hidden;padding:.25rem}.wm-cb-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
1313
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], values: [{ type: i0.Input, args: [{ isSignal: true, alias: "values", required: false }] }, { type: i0.Output, args: ["valuesChange"] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], allowFreeform: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowFreeform", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], maxPanelHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxPanelHeight", required: false }] }], query: [{ type: i0.Output, args: ["query"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], inputRef: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], panel: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ComboboxPanel), { isSignal: true }] }] } });
1314
+
662
1315
  let nextId = 0;
663
1316
  /**
664
1317
  * WmToastService — Application-wide toast notification manager.
@@ -879,5 +1532,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImpor
879
1532
  * Generated bundle index. Do not edit.
880
1533
  */
881
1534
 
882
- export { Checkbox, GhostField, NumberInput, PageHeader, Toggle, WmAutoFocus, WmButton, WmDialog, WmLicenseDialog, WmLicenseNudge, WmLicenseService, WmModal, WmPopover, WmSelect, WmTab, WmTabs, WmToastContainer, WmToastService };
1535
+ export { Checkbox, GhostField, NumberInput, PageHeader, Toggle, WmAutoFocus, WmButton, WmCombobox, WmDialog, WmLicenseDialog, WmLicenseNudge, WmLicenseService, WmModal, WmPopover, WmSelect, WmTab, WmTabs, WmToastContainer, WmToastService };
883
1536
  //# sourceMappingURL=wildmason-aegis.mjs.map