codexly-ui 0.22.1 → 0.23.1

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.
@@ -14075,11 +14075,6 @@ class ClxPaginationComponent {
14075
14075
  _destroyRef = inject(DestroyRef);
14076
14076
  // ── FormControl interno para el select de pageSize ─────────────────────────
14077
14077
  _pageSizeCtrl = new FormControl(null);
14078
- /** True once the user has explicitly picked a pageSize from the select — after that, their
14079
- * choice is respected even if a later filter shrinks totalItems below it. Until then, a
14080
- * caller-provided default (e.g. 20) that exceeds a small totalItems is auto-corrected down
14081
- * to the smallest available option, so the select never opens pre-selected on the largest value. */
14082
- _userTouchedPageSize = false;
14083
14078
  constructor() {
14084
14079
  // Sincroniza el pageSize model → FormControl cada vez que cambia desde afuera
14085
14080
  effect(() => {
@@ -14088,31 +14083,14 @@ class ClxPaginationComponent {
14088
14083
  this._pageSizeCtrl.setValue(ps, { emitEvent: false });
14089
14084
  }
14090
14085
  });
14091
- // Mientras el usuario no haya elegido un pageSize a propósito, si el default provisto por el
14092
- // padre excede totalItems, se ajusta al más pequeño disponible en vez de quedar seleccionado
14093
- // como si fuera el "más grande elegido". Debe emitir pageChange/sizeChange igual que la
14094
- // selección manual — si no, el consumidor (ej. clx-table → su padre real) nunca se entera
14095
- // del nuevo tamaño y sigue pidiendo datos al backend con el size viejo.
14096
- effect(() => {
14097
- if (this._userTouchedPageSize)
14098
- return;
14099
- const total = this.totalItems();
14100
- if (!total || total <= 0)
14101
- return;
14102
- const smallest = this._pageSizeOptions()[0]?.value;
14103
- if (smallest != null && this.pageSize() > total && smallest !== this.pageSize()) {
14104
- this.pageSize.set(smallest);
14105
- this.currentPage.set(1);
14106
- this.sizeChange.emit({ pageSize: smallest });
14107
- this.pageChange.emit({ page: 1, pageSize: smallest });
14108
- }
14109
- });
14086
+ // El pageSize solo cambia cuando el usuario lo elige: nunca se ajusta solo según totalItems.
14087
+ // Antes se reducía a la opción más pequeña cuando había menos registros que el pageSize, lo
14088
+ // que mostraba 5 por página donde cabían todos y obligaba al consumidor a pedir los datos de nuevo.
14110
14089
  // Sincroniza el FormControl → pageSize model cuando el usuario elige
14111
14090
  this._pageSizeCtrl.valueChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(val => {
14112
14091
  const newSize = Number(val);
14113
14092
  if (!val || !Number.isFinite(newSize) || newSize <= 0 || newSize === this.pageSize())
14114
14093
  return;
14115
- this._userTouchedPageSize = true;
14116
14094
  this.pageSize.set(newSize);
14117
14095
  this.currentPage.set(1);
14118
14096
  this.sizeChange.emit({ pageSize: newSize });
@@ -19732,9 +19710,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
19732
19710
  let _clxSearchIdCounter = 0;
19733
19711
  /** Storefront search bar — same color/size/CVA conventions as ClxInputComponent, but styled as
19734
19712
  * one continuous pill (input fused with a solid search button) rather than a bordered field with
19735
- * a detached button, matching how e-commerce header search boxes are typically laid out. */
19713
+ * a detached button, matching how e-commerce header search boxes are typically laid out.
19714
+ *
19715
+ * Optional autocomplete: the host listens to (queryChange) — the text, debounced, once it has
19716
+ * `minChars` — and feeds [suggestions] back (product rows with image and price, shortcuts, past
19717
+ * searches, a "¿Quisiste decir…?" correction, the result total). The panel opens under the bar,
19718
+ * supports ↑ ↓ Enter Esc, and reports picks through (suggestionSelect); Enter without a picked row,
19719
+ * the button, the correction and "Ver todos" emit (search). With [openOnFocus] the panel also shows
19720
+ * for an empty box (e.g. recent searches). */
19736
19721
  class ClxSearchComponent {
19737
19722
  _themeSvc = inject(ClxThemeService);
19723
+ _el = inject((ElementRef));
19724
+ _sso = inject(ScrollStrategyOptions);
19738
19725
  color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
19739
19726
  _color = computed(() => this.color() ?? this._themeSvc.config().primaryColor, ...(ngDevMode ? [{ debugName: "_color" }] : /* istanbul ignore next */ []));
19740
19727
  /** Overrides only the search button's text color — see ClxButtonComponent's textColor input. */
@@ -19747,13 +19734,45 @@ class ClxSearchComponent {
19747
19734
  value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
19748
19735
  /** Disabled state for standalone use (without FormControl) */
19749
19736
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
19737
+ /** Autocomplete content for the current text; null = no panel. */
19738
+ suggestions = input(null, ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
19739
+ /** The host is fetching suggestions. */
19740
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
19741
+ /** (queryChange) waits this long after the last keystroke. */
19742
+ debounceMs = input(250, ...(ngDevMode ? [{ debugName: "debounceMs" }] : /* istanbul ignore next */ []));
19743
+ /** (queryChange) only from this many characters (0 = also for an empty box). */
19744
+ minChars = input(2, ...(ngDevMode ? [{ debugName: "minChars" }] : /* istanbul ignore next */ []));
19745
+ /** Show the panel when focusing an empty box (recent searches…). */
19746
+ openOnFocus = input(false, ...(ngDevMode ? [{ debugName: "openOnFocus" }] : /* istanbul ignore next */ []));
19750
19747
  search = output();
19748
+ /** Debounced text for the host to fetch suggestions for ('' when cleared). */
19749
+ queryChange = output();
19750
+ suggestionSelect = output();
19751
19751
  _inputId = `clx-search-${++_clxSearchIdCounter}`;
19752
19752
  _value = signal('', ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
19753
19753
  _cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_cvaDisabled" }] : /* istanbul ignore next */ []));
19754
19754
  _cvaConnected = false;
19755
19755
  _cardDisabled = inject(CLX_CARD_DISABLED_CONTEXT, { optional: true });
19756
+ _focused = signal(false, ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
19757
+ _dismissed = signal(false, ...(ngDevMode ? [{ debugName: "_dismissed" }] : /* istanbul ignore next */ []));
19758
+ _active = signal(null, ...(ngDevMode ? [{ debugName: "_active" }] : /* istanbul ignore next */ []));
19759
+ _panelWidth = signal(0, ...(ngDevMode ? [{ debugName: "_panelWidth" }] : /* istanbul ignore next */ []));
19760
+ _debounce;
19756
19761
  _disabled = computed(() => this._cvaDisabled() || this.disabled() || !!this._cardDisabled?.(), ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
19762
+ _scrollStrategy = this._sso.reposition();
19763
+ _positions = [
19764
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 6 },
19765
+ ];
19766
+ /** Flat list of the panel's rows, for ↑ ↓. */
19767
+ _rows = computed(() => (this.suggestions()?.groups ?? [])
19768
+ .flatMap(g => g.items.map(item => ({ group: g.key, item }))), ...(ngDevMode ? [{ debugName: "_rows" }] : /* istanbul ignore next */ []));
19769
+ _panelOpen = computed(() => {
19770
+ if (!this._focused() || this._dismissed() || this._disabled())
19771
+ return false;
19772
+ const s = this.suggestions();
19773
+ const hasContent = !!s && (!!s.correction || s.groups.some(g => g.items.length > 0));
19774
+ return hasContent && (this._value().trim().length > 0 || this.openOnFocus());
19775
+ }, ...(ngDevMode ? [{ debugName: "_panelOpen" }] : /* istanbul ignore next */ []));
19757
19776
  _valueEffect = effect(() => {
19758
19777
  const v = this.value();
19759
19778
  if (!this._cvaConnected)
@@ -19761,6 +19780,9 @@ class ClxSearchComponent {
19761
19780
  }, ...(ngDevMode ? [{ debugName: "_valueEffect" }] : /* istanbul ignore next */ []));
19762
19781
  _onChange = () => { };
19763
19782
  _onTouched = () => { };
19783
+ constructor() {
19784
+ inject(DestroyRef).onDestroy(() => clearTimeout(this._debounce));
19785
+ }
19764
19786
  _sizeConfig = computed(() => INPUT_SIZE_MAP[this._size()], ...(ngDevMode ? [{ debugName: "_sizeConfig" }] : /* istanbul ignore next */ []));
19765
19787
  _buttonSize = computed(() => {
19766
19788
  switch (this._size()) {
@@ -19769,6 +19791,7 @@ class ClxSearchComponent {
19769
19791
  default: return 'md';
19770
19792
  }
19771
19793
  }, ...(ngDevMode ? [{ debugName: "_buttonSize" }] : /* istanbul ignore next */ []));
19794
+ _accentText = computed(() => resolveColor(this._color()).textEmphasis, ...(ngDevMode ? [{ debugName: "_accentText" }] : /* istanbul ignore next */ []));
19772
19795
  _wrapperClass = computed(() => {
19773
19796
  const focusCls = this._disabled() ? '' : resolveColor(this._color()).focusWithin;
19774
19797
  return `relative flex items-stretch ${resolveRadius(this._themeSvc.config().borderRadius)} border border-clx-border ${focusCls} overflow-hidden transition-[border-color,box-shadow] duration-200`;
@@ -19797,34 +19820,106 @@ class ClxSearchComponent {
19797
19820
  const val = event.target.value;
19798
19821
  this._value.set(val);
19799
19822
  this._onChange(val);
19823
+ this._dismissed.set(false);
19824
+ this._active.set(null);
19825
+ this._emitQuery(val);
19826
+ }
19827
+ _handleFocus() {
19828
+ this._panelWidth.set(Math.round(this._el.nativeElement.getBoundingClientRect().width));
19829
+ this._focused.set(true);
19830
+ this._dismissed.set(false);
19831
+ if (this.openOnFocus() && !this._value().trim())
19832
+ this.queryChange.emit('');
19800
19833
  }
19801
19834
  _handleBlur() {
19802
19835
  this._onTouched();
19836
+ this._focused.set(false);
19837
+ }
19838
+ _handleKeydown(event) {
19839
+ const rows = this._rows();
19840
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
19841
+ if (!this._panelOpen() || !rows.length)
19842
+ return;
19843
+ event.preventDefault();
19844
+ const current = this._active();
19845
+ const index = current ? rows.findIndex(r => r.group === current.group && r.item.id === current.id) : -1;
19846
+ const next = event.key === 'ArrowDown'
19847
+ ? (index + 1) % rows.length
19848
+ : (index <= 0 ? rows.length - 1 : index - 1);
19849
+ this._active.set({ group: rows[next].group, id: rows[next].item.id });
19850
+ }
19851
+ else if (event.key === 'Enter') {
19852
+ event.preventDefault();
19853
+ const current = this._active();
19854
+ const row = current && this._panelOpen()
19855
+ ? rows.find(r => r.group === current.group && r.item.id === current.id)
19856
+ : undefined;
19857
+ if (row)
19858
+ this._select(row.group, row.item);
19859
+ else
19860
+ this._handleSubmit();
19861
+ }
19862
+ else if (event.key === 'Escape') {
19863
+ this._close();
19864
+ }
19865
+ }
19866
+ _isActive(group, item) {
19867
+ const a = this._active();
19868
+ return !!a && a.group === group && a.id === item.id;
19869
+ }
19870
+ _activate(group, item) {
19871
+ this._active.set({ group, id: item.id });
19872
+ }
19873
+ _select(group, item) {
19874
+ this._close();
19875
+ this.suggestionSelect.emit({ group, item });
19876
+ }
19877
+ _searchFor(text) {
19878
+ this._value.set(text);
19879
+ this._onChange(text);
19880
+ this._handleSubmit();
19881
+ }
19882
+ _close() {
19883
+ this._dismissed.set(true);
19884
+ this._active.set(null);
19803
19885
  }
19804
19886
  _handleSubmit() {
19805
19887
  if (this._disabled())
19806
19888
  return;
19889
+ this._close();
19890
+ clearTimeout(this._debounce);
19807
19891
  this.search.emit(this._value());
19808
19892
  }
19893
+ _emitQuery(val) {
19894
+ clearTimeout(this._debounce);
19895
+ const text = val.trim();
19896
+ if (text.length > 0 && text.length < this.minChars())
19897
+ return;
19898
+ this._debounce = setTimeout(() => this.queryChange.emit(text), text ? this.debounceMs() : 0);
19899
+ }
19809
19900
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxSearchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19810
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.15", type: ClxSearchComponent, isStandalone: true, selector: "clx-search", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, buttonTextColor: { classPropertyName: "buttonTextColor", publicName: "buttonTextColor", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, buttonLabel: { classPropertyName: "buttonLabel", publicName: "buttonLabel", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { search: "search" }, host: { classAttribute: "block w-full" }, providers: [
19901
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxSearchComponent, isStandalone: true, selector: "clx-search", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, buttonTextColor: { classPropertyName: "buttonTextColor", publicName: "buttonTextColor", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, buttonLabel: { classPropertyName: "buttonLabel", publicName: "buttonLabel", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, debounceMs: { classPropertyName: "debounceMs", publicName: "debounceMs", isSignal: true, isRequired: false, transformFunction: null }, minChars: { classPropertyName: "minChars", publicName: "minChars", isSignal: true, isRequired: false, transformFunction: null }, openOnFocus: { classPropertyName: "openOnFocus", publicName: "openOnFocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { search: "search", queryChange: "queryChange", suggestionSelect: "suggestionSelect" }, host: { classAttribute: "block w-full" }, providers: [
19811
19902
  {
19812
19903
  provide: NG_VALUE_ACCESSOR,
19813
19904
  useExisting: forwardRef(() => ClxSearchComponent),
19814
19905
  multi: true,
19815
19906
  },
19816
19907
  ], ngImport: i0, template: `
19817
- <div [class]="_wrapperClass()">
19908
+ <div [class]="_wrapperClass()" cdkOverlayOrigin #origin="cdkOverlayOrigin">
19818
19909
  <input
19819
19910
  [id]="_inputId"
19820
19911
  type="search"
19912
+ role="combobox"
19913
+ [attr.aria-expanded]="_panelOpen()"
19914
+ [attr.aria-controls]="_inputId + '-panel'"
19821
19915
  [placeholder]="placeholder()"
19822
19916
  [disabled]="_disabled()"
19823
19917
  [value]="_value()"
19824
19918
  [class]="_inputClass()"
19825
19919
  autocomplete="off"
19826
19920
  (input)="_handleInput($event)"
19827
- (keydown.enter)="_handleSubmit()"
19921
+ (focus)="_handleFocus()"
19922
+ (keydown)="_handleKeydown($event)"
19828
19923
  (blur)="_handleBlur()"
19829
19924
  />
19830
19925
  <button
@@ -19841,14 +19936,77 @@ class ClxSearchComponent {
19841
19936
  {{ buttonLabel() }}
19842
19937
  </button>
19843
19938
  </div>
19844
- `, isInline: true, dependencies: [{ kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "emphasis", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
19939
+
19940
+ <ng-template
19941
+ cdkConnectedOverlay
19942
+ [cdkConnectedOverlayOrigin]="origin"
19943
+ [cdkConnectedOverlayOpen]="_panelOpen()"
19944
+ [cdkConnectedOverlayPositions]="_positions"
19945
+ [cdkConnectedOverlayScrollStrategy]="_scrollStrategy"
19946
+ [cdkConnectedOverlayWidth]="_panelWidth()"
19947
+ [cdkConnectedOverlayPush]="true"
19948
+ (overlayOutsideClick)="_close()"
19949
+ (detach)="_close()">
19950
+ <div [id]="_inputId + '-panel'" role="listbox"
19951
+ class="w-full max-h-[70vh] overflow-y-auto bg-clx-surface rounded-xl shadow-xl border border-clx-border py-2"
19952
+ (mousedown)="$event.preventDefault()">
19953
+ @if (loading()) {
19954
+ <div class="flex items-center gap-2 px-4 py-2 text-xs text-clx-text-subtle">
19955
+ <clx-spinner size="xs" [color]="_color()" /> Buscando…
19956
+ </div>
19957
+ }
19958
+ @if (suggestions()?.correction; as correction) {
19959
+ <button type="button" class="w-full text-left px-4 py-2 text-sm text-clx-text-muted hover:bg-clx-surface-2"
19960
+ (click)="_searchFor(correction)">
19961
+ ¿Quisiste decir <span class="font-semibold" [class]="_accentText()">{{ correction }}</span>?
19962
+ </button>
19963
+ }
19964
+ @for (group of suggestions()?.groups ?? []; track group.key) {
19965
+ @if (group.items.length) {
19966
+ <p class="px-4 pt-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-clx-text-subtle">{{ group.title }}</p>
19967
+ @for (item of group.items; track item.id) {
19968
+ <button type="button" role="option"
19969
+ [attr.aria-selected]="_isActive(group.key, item)"
19970
+ class="w-full flex items-center gap-3 px-4 py-2 text-left transition-colors"
19971
+ [class]="_isActive(group.key, item) ? 'bg-clx-surface-2' : 'hover:bg-clx-surface-2'"
19972
+ (mouseenter)="_activate(group.key, item)"
19973
+ (click)="_select(group.key, item)">
19974
+ @if (item.image) {
19975
+ <img [src]="item.image" [alt]="item.label" class="w-10 h-10 rounded-lg object-cover shrink-0 bg-clx-surface-2" />
19976
+ } @else if (item.icon) {
19977
+ <span class="w-8 h-8 rounded-lg bg-clx-surface-2 flex items-center justify-center shrink-0">
19978
+ <span clx-icon [name]="item.icon" size="sm" class="text-clx-text-muted"></span>
19979
+ </span>
19980
+ }
19981
+ <span class="flex-1 min-w-0">
19982
+ <span class="block text-sm text-clx-text-label truncate">{{ item.label }}</span>
19983
+ @if (item.sublabel) {
19984
+ <span class="block text-xs text-clx-text-subtle truncate">{{ item.sublabel }}</span>
19985
+ }
19986
+ </span>
19987
+ @if (item.meta) {
19988
+ <span class="text-sm font-semibold text-clx-text-label shrink-0">{{ item.meta }}</span>
19989
+ }
19990
+ </button>
19991
+ }
19992
+ }
19993
+ }
19994
+ @if (_value().trim() && suggestions()?.total) {
19995
+ <button type="button" class="w-full text-left px-4 pt-3 pb-1 text-sm font-semibold" [class]="_accentText()"
19996
+ (click)="_handleSubmit()">
19997
+ Ver los {{ suggestions()!.total }} resultados de “{{ _value().trim() }}”
19998
+ </button>
19999
+ }
20000
+ </div>
20001
+ </ng-template>
20002
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.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"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "emphasis", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSpinnerComponent, selector: "clx-spinner", inputs: ["size", "color", "variant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
19845
20003
  }
19846
20004
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxSearchComponent, decorators: [{
19847
20005
  type: Component,
19848
20006
  args: [{
19849
20007
  selector: 'clx-search',
19850
20008
  standalone: true,
19851
- imports: [ClxButtonComponent],
20009
+ imports: [OverlayModule, ClxButtonComponent, ClxIconComponent, ClxSpinnerComponent],
19852
20010
  providers: [
19853
20011
  {
19854
20012
  provide: NG_VALUE_ACCESSOR,
@@ -19857,17 +20015,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
19857
20015
  },
19858
20016
  ],
19859
20017
  template: `
19860
- <div [class]="_wrapperClass()">
20018
+ <div [class]="_wrapperClass()" cdkOverlayOrigin #origin="cdkOverlayOrigin">
19861
20019
  <input
19862
20020
  [id]="_inputId"
19863
20021
  type="search"
20022
+ role="combobox"
20023
+ [attr.aria-expanded]="_panelOpen()"
20024
+ [attr.aria-controls]="_inputId + '-panel'"
19864
20025
  [placeholder]="placeholder()"
19865
20026
  [disabled]="_disabled()"
19866
20027
  [value]="_value()"
19867
20028
  [class]="_inputClass()"
19868
20029
  autocomplete="off"
19869
20030
  (input)="_handleInput($event)"
19870
- (keydown.enter)="_handleSubmit()"
20031
+ (focus)="_handleFocus()"
20032
+ (keydown)="_handleKeydown($event)"
19871
20033
  (blur)="_handleBlur()"
19872
20034
  />
19873
20035
  <button
@@ -19884,6 +20046,69 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
19884
20046
  {{ buttonLabel() }}
19885
20047
  </button>
19886
20048
  </div>
20049
+
20050
+ <ng-template
20051
+ cdkConnectedOverlay
20052
+ [cdkConnectedOverlayOrigin]="origin"
20053
+ [cdkConnectedOverlayOpen]="_panelOpen()"
20054
+ [cdkConnectedOverlayPositions]="_positions"
20055
+ [cdkConnectedOverlayScrollStrategy]="_scrollStrategy"
20056
+ [cdkConnectedOverlayWidth]="_panelWidth()"
20057
+ [cdkConnectedOverlayPush]="true"
20058
+ (overlayOutsideClick)="_close()"
20059
+ (detach)="_close()">
20060
+ <div [id]="_inputId + '-panel'" role="listbox"
20061
+ class="w-full max-h-[70vh] overflow-y-auto bg-clx-surface rounded-xl shadow-xl border border-clx-border py-2"
20062
+ (mousedown)="$event.preventDefault()">
20063
+ @if (loading()) {
20064
+ <div class="flex items-center gap-2 px-4 py-2 text-xs text-clx-text-subtle">
20065
+ <clx-spinner size="xs" [color]="_color()" /> Buscando…
20066
+ </div>
20067
+ }
20068
+ @if (suggestions()?.correction; as correction) {
20069
+ <button type="button" class="w-full text-left px-4 py-2 text-sm text-clx-text-muted hover:bg-clx-surface-2"
20070
+ (click)="_searchFor(correction)">
20071
+ ¿Quisiste decir <span class="font-semibold" [class]="_accentText()">{{ correction }}</span>?
20072
+ </button>
20073
+ }
20074
+ @for (group of suggestions()?.groups ?? []; track group.key) {
20075
+ @if (group.items.length) {
20076
+ <p class="px-4 pt-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-clx-text-subtle">{{ group.title }}</p>
20077
+ @for (item of group.items; track item.id) {
20078
+ <button type="button" role="option"
20079
+ [attr.aria-selected]="_isActive(group.key, item)"
20080
+ class="w-full flex items-center gap-3 px-4 py-2 text-left transition-colors"
20081
+ [class]="_isActive(group.key, item) ? 'bg-clx-surface-2' : 'hover:bg-clx-surface-2'"
20082
+ (mouseenter)="_activate(group.key, item)"
20083
+ (click)="_select(group.key, item)">
20084
+ @if (item.image) {
20085
+ <img [src]="item.image" [alt]="item.label" class="w-10 h-10 rounded-lg object-cover shrink-0 bg-clx-surface-2" />
20086
+ } @else if (item.icon) {
20087
+ <span class="w-8 h-8 rounded-lg bg-clx-surface-2 flex items-center justify-center shrink-0">
20088
+ <span clx-icon [name]="item.icon" size="sm" class="text-clx-text-muted"></span>
20089
+ </span>
20090
+ }
20091
+ <span class="flex-1 min-w-0">
20092
+ <span class="block text-sm text-clx-text-label truncate">{{ item.label }}</span>
20093
+ @if (item.sublabel) {
20094
+ <span class="block text-xs text-clx-text-subtle truncate">{{ item.sublabel }}</span>
20095
+ }
20096
+ </span>
20097
+ @if (item.meta) {
20098
+ <span class="text-sm font-semibold text-clx-text-label shrink-0">{{ item.meta }}</span>
20099
+ }
20100
+ </button>
20101
+ }
20102
+ }
20103
+ }
20104
+ @if (_value().trim() && suggestions()?.total) {
20105
+ <button type="button" class="w-full text-left px-4 pt-3 pb-1 text-sm font-semibold" [class]="_accentText()"
20106
+ (click)="_handleSubmit()">
20107
+ Ver los {{ suggestions()!.total }} resultados de “{{ _value().trim() }}”
20108
+ </button>
20109
+ }
20110
+ </div>
20111
+ </ng-template>
19887
20112
  `,
19888
20113
  encapsulation: ViewEncapsulation.None,
19889
20114
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -19891,7 +20116,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
19891
20116
  class: 'block w-full',
19892
20117
  },
19893
20118
  }]
19894
- }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], buttonTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonTextColor", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], buttonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLabel", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], search: [{ type: i0.Output, args: ["search"] }] } });
20119
+ }], ctorParameters: () => [], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], buttonTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonTextColor", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], buttonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLabel", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], minChars: [{ type: i0.Input, args: [{ isSignal: true, alias: "minChars", required: false }] }], openOnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnFocus", required: false }] }], search: [{ type: i0.Output, args: ["search"] }], queryChange: [{ type: i0.Output, args: ["queryChange"] }], suggestionSelect: [{ type: i0.Output, args: ["suggestionSelect"] }] } });
19895
20120
 
19896
20121
  class ClxAnimateGroupDirective {
19897
20122
  stagger = input(100, { ...(ngDevMode ? { debugName: "stagger" } : /* istanbul ignore next */ {}), transform: numberAttribute });