@kaufmann-lab/ui 0.1.19 → 0.1.21

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,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, Component, model, signal, inject, DestroyRef, viewChild, computed, afterNextRender, booleanAttribute } from '@angular/core';
3
- import { Divider, Select, Text, Icon, Modal, DataTable, EmptyState, Pagination, Link } from '@kaufmann-lab/ui/core';
2
+ import { input, output, Component, model, booleanAttribute, signal, inject, DestroyRef, viewChild, computed, afterRenderEffect, untracked, afterNextRender } from '@angular/core';
3
+ import { Divider, Select, Text, Button, ContentState, FilterChips, Icon, Input, Modal, DataTable, EmptyState, Pagination, Skeleton, Link } from '@kaufmann-lab/ui/core';
4
4
  import { NgTemplateOutlet } from '@angular/common';
5
+ import * as i1 from '@angular/forms';
6
+ import { FormsModule } from '@angular/forms';
5
7
  import { matchMediaSignal, normalizeForSearch } from '@kaufmann-lab/ui/shared';
6
8
 
7
9
  /**
@@ -21,6 +23,18 @@ import { matchMediaSignal, normalizeForSearch } from '@kaufmann-lab/ui/shared';
21
23
  * { label: 'Sucursal', value: 'Sucursal Norte' },
22
24
  * ]"
23
25
  * />
26
+ *
27
+ * Con opciones en forma de objeto, `value`/`valueChange` usan el `value` de
28
+ * la opción, no su nombre:
29
+ *
30
+ * <kf-company-selector
31
+ * [companies]="[
32
+ * { value: '1002099', label: 'DIANA CAROLINA PINZÓN MALDONADO', tag: 'Personal', detail: 'DNI 46594489' },
33
+ * { value: '1004410', label: 'TRANSPORTES VIRGEN DE LA AGRICULTURA', tag: 'Empresa', detail: 'RUC 20434860808' },
34
+ * ]"
35
+ * [value]="activeAccount()"
36
+ * (valueChange)="activeAccount.set($event)"
37
+ * />
24
38
  */
25
39
  class CompanySelector {
26
40
  label = input('Selecciona empresa', /* @ts-ignore */
@@ -33,7 +47,7 @@ class CompanySelector {
33
47
  ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
34
48
  valueChange = output();
35
49
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CompanySelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
36
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: CompanySelector, isStandalone: true, selector: "kf-company-selector", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, companies: { classPropertyName: "companies", publicName: "companies", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<kf-select\n class=\"kf-company-selector__select\"\n [label]=\"label()\"\n [options]=\"companies()\"\n [value]=\"value()\"\n (valueChange)=\"valueChange.emit($event)\"\n/>\n@if (meta().length) {\n <div class=\"kf-company-selector__meta\">\n @for (item of meta(); track item.label; let last = $last) {\n <div class=\"kf-company-selector__meta-item\">\n <kf-text as=\"span\" variant=\"overline\">{{ item.label }}</kf-text>\n <span class=\"kf-company-selector__meta-value\">{{ item.value }}</span>\n </div>\n @if (!last) {\n <kf-divider orientation=\"vertical\" />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;align-items:center;gap:var(--kf-space-lg);flex-wrap:wrap;padding:var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);background:var(--kf-color-white);font-family:var(--kf-font-body)}.kf-company-selector__select{flex:1 1 260px;min-width:0}.kf-company-selector__meta{display:flex;gap:var(--kf-space-lg);flex-wrap:wrap}.kf-company-selector__meta-item{display:flex;flex-direction:column;gap:2px}.kf-company-selector__meta-value{font-family:var(--kf-font-mono);font-size:.875rem;color:var(--kf-color-ink)}\n"], dependencies: [{ kind: "component", type: Divider, selector: "kf-divider", inputs: ["orientation"] }, { kind: "component", type: Select, selector: "kf-select", inputs: ["label", "options", "value", "placeholder", "disabled", "invalid", "errorMessage", "variant", "width", "searchable", "searchThreshold"], outputs: ["valueChange"] }, { kind: "component", type: Text, selector: "kf-text", inputs: ["as", "variant", "tone"] }] });
50
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: CompanySelector, isStandalone: true, selector: "kf-company-selector", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, companies: { classPropertyName: "companies", publicName: "companies", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<kf-select\n class=\"kf-company-selector__select\"\n [label]=\"label()\"\n [options]=\"companies()\"\n [value]=\"value()\"\n (valueChange)=\"valueChange.emit($event)\"\n/>\n@if (meta().length) {\n <div class=\"kf-company-selector__meta\">\n @for (item of meta(); track item.label; let last = $last) {\n <div class=\"kf-company-selector__meta-item\">\n <kf-text as=\"span\" variant=\"overline\">{{ item.label }}</kf-text>\n <span class=\"kf-company-selector__meta-value\">{{ item.value }}</span>\n </div>\n @if (!last) {\n <kf-divider orientation=\"vertical\" />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;align-items:center;gap:var(--kf-space-lg);flex-wrap:wrap;padding:var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);background:var(--kf-color-white);font-family:var(--kf-font-body)}.kf-company-selector__select{flex:1 1 260px;min-width:0}.kf-company-selector__meta{display:flex;gap:var(--kf-space-lg);flex-wrap:wrap}.kf-company-selector__meta-item{display:flex;flex-direction:column;gap:2px}.kf-company-selector__meta-value{font-family:var(--kf-font-mono);font-size:.875rem;color:var(--kf-color-ink)}\n"], dependencies: [{ kind: "component", type: Divider, selector: "kf-divider", inputs: ["orientation"] }, { kind: "component", type: Select, selector: "kf-select", inputs: ["label", "options", "value", "placeholder", "disabled", "invalid", "errorMessage", "variant", "width", "searchable", "searchThreshold", "ariaLabel", "icon", "panelTitle", "panelWidth"], outputs: ["valueChange"] }, { kind: "component", type: Text, selector: "kf-text", inputs: ["as", "variant", "tone"] }] });
37
51
  }
38
52
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CompanySelector, decorators: [{
39
53
  type: Component,
@@ -83,10 +97,8 @@ const PAGE_SIZE = 10;
83
97
  * este modo necesita. Compone también `kf-data-table` para la tabla
84
98
  * completa (columnas configurables, fade de overflow horizontal ya
85
99
  * resuelto ahí) y `kf-pagination` para paginar — ninguno de los tres se
86
- * reimplementa. Agrega un dropdown propio de "Ordenar por" (calibrado
87
- * visualmente contra el trigger de `kf-select`, pero compacto: `kf-select`
88
- * fuerza un ancho fijo de 240px pensado para formularios, no para una
89
- * barra de herramientas de tabla — ver JSDoc de `sortOpen` más abajo).
100
+ * reimplementa, igual que el buscador (`kf-input`), el orden (`kf-select`)
101
+ * y los chips (`kf-filter-chips`).
90
102
  *
91
103
  * En mobile, `kf-data-table[cardable]` con `defaultView` forzado a
92
104
  * `'cards'` decide la vista (mismo criterio que `matchMediaSignal` ya
@@ -94,12 +106,70 @@ const PAGE_SIZE = 10;
94
106
  * no tiene sentido ofrecer un toggle tabla/cards que en mobile daría una
95
107
  * tabla angosta con scroll horizontal.
96
108
  *
97
- * DEUDA TÉCNICA CONOCIDA (confirmada con el usuario, ver CHANGELOG.md): el
98
- * diseño validado con mockups pedía que en mobile el modal ocupe el 100%
99
- * de la pantalla (botón "atrás", sin overlay centrado) — `kf-modal` no
100
- * tiene ese modo, así que hoy en mobile se ve como un diálogo centrado
101
- * angosto (`width: min(480px, ...)`). Es funcional (cards legibles,
102
- * búsqueda, chips, paginación), solo visualmente distinto del mockup.
109
+ * El modal mide `min(modalWidth, 100vw - 32px)` (default 1040px) con alto
110
+ * máximo de 85vh: encabezado (título + conteo "12 de 86 cotizaciones" en
111
+ * región `aria-live`) y filtros FIJOS arriba, solo la lista se desplaza. En
112
+ * ≤640px de viewport ocupa toda la pantalla (`fullscreenOnMobile` de
113
+ * `kf-modal`). Filtros: fila 1 = buscador (`kf-input`, crece, mínimo 240px)
114
+ * + orden (`kf-select` estándar); fila 2 = `kf-filter-chips` con contador y
115
+ * un chip "Todas" (`allChipLabel`) — en varias líneas en escritorio, una
116
+ * sola línea con scroll horizontal en mobile. Elegir un ítem (o el actual)
117
+ * cierra el modal.
118
+ *
119
+ * ## Trigger (variante `inline`)
120
+ * - `triggerSize="default"` (histórico): tarjeta a ancho completo con
121
+ * miniatura y resumen.
122
+ * - `triggerSize="compact"`: control de barra de herramientas de alto FIJO
123
+ * (`--kf-quote-picker-trigger-height`, 48px), sin miniatura: línea 1 =
124
+ * título en mono + estado (columnas `status`), línea 2 = primer campo con
125
+ * elipsis, y caret. Ancho flexible (`min-width: 0`). Su nombre accesible
126
+ * es el texto visible con un prefijo: "Cambiar cotización. Actual: <id>,
127
+ * <estado>, <modelo>".
128
+ * - `inlinePanel` (default `true`): con `false`, el trigger abre DIRECTO el
129
+ * modal del picker (`aria-haspopup="dialog"`), sin panel inline — el mismo
130
+ * modal que la variante `modal`, así todas las pantallas comparten un
131
+ * único selector.
132
+ *
133
+ * Con `loading` el trigger compacto muestra un esqueleto del MISMO alto
134
+ * (región `role="status"` con `loadingLabel`), así la barra no salta.
135
+ *
136
+ * <kf-quote-picker [items]="quotes()" [columns]="columns" [idKey]="idKey"
137
+ * triggerSize="compact" [inlinePanel]="false" modalLayout="list"
138
+ * [selectedId]="id()" [chips]="chips()" [sortOptions]="sortOptions"
139
+ * [itemLabels]="{ singular: 'cotización', plural: 'cotizaciones' }"
140
+ * (selectedIdChange)="go($event)" />
141
+ *
142
+ * ## `modalLayout` (modal de la variante `modal` y el de `inlinePanel` en false)
143
+ * - `'table'` (default, comportamiento histórico): `kf-data-table` paginada
144
+ * con toggle Tabla/Tarjetas. La fila/tarjeta de `selectedId` lleva
145
+ * `aria-current="true"` y fondo `primary-soft` (`currentRowKey`).
146
+ * - `'list'`: lista de una línea por ítem con COLUMNAS ALINEADAS (CSS grid
147
+ * + `subgrid`: cada pista mide lo mismo en todas las filas), sin paginar
148
+ * ni toggle; se desplaza el cuerpo del modal. Orden de columnas: título
149
+ * (`cardRole: 'title'`, mono) | primer campo (flexible, una línea con
150
+ * elipsis y `title`) | resto de campos (`width` o su ancho de contenido;
151
+ * `align: 'end'` para montos, cifras tabulares) | `status` (hasta
152
+ * 13.75rem; el consumidor pasa `kf-badge truncate` con `title`) | check
153
+ * de la fila actual. Cabecera de columnas discreta y fija al desplazar
154
+ * (`listHeader`). Filas de 52px como mínimo; la de `selectedId` lleva
155
+ * `aria-current="true"`, fondo `primary-soft` y un check, y al abrir se
156
+ * desplaza a la vista. Con la lista de hasta 560px de ancho (container
157
+ * query, no viewport): 2 líneas (título + estado / campos), con el primer
158
+ * campo en hasta 2 líneas.
159
+ *
160
+ * Límite conocido: sin virtualización. Cada fila es un `<button>` en el
161
+ * DOM (búsqueda, foco y `aria-current` funcionan sin trucos); se probó
162
+ * fluido con 120 filas y sigue usable con unos cientos. Para miles,
163
+ * filtrar en el servidor antes de pasar `items`.
164
+ *
165
+ * En la variante modal, además: orden con `kf-select` estándar (ícono
166
+ * `sort`, "Ordenar por"), chips con `aria-pressed`, buscador con
167
+ * `searchAriaLabel` y foco inicial (`initialFocus` de `kf-modal`), conteo
168
+ * en una región `aria-live="polite"` (`itemLabels`), y estados
169
+ * `loading` (spinner con `loadingLabel`) y `error` (mensaje + botón
170
+ * "Reintentar" que emite `retry`) vía `kf-content-state`. El vacío "Sin
171
+ * resultados para esta búsqueda" solo aparece sin carga ni error. Escape
172
+ * con el panel del `kf-select` abierto cierra solo el select.
103
173
  *
104
174
  * <kf-quote-picker
105
175
  * [items]="quotes()"
@@ -158,7 +228,47 @@ class QuotePicker {
158
228
  * ícono fijo. Default `'file'` solo por no dejar el input sin valor. */
159
229
  placeholderIcon = input('file', /* @ts-ignore */
160
230
  ...(ngDevMode ? [{ debugName: "placeholderIcon" }] : /* istanbul ignore next */ []));
231
+ /** Disposición de la variante `modal`. Ver JSDoc de la clase. */
232
+ modalLayout = input('table', /* @ts-ignore */
233
+ ...(ngDevMode ? [{ debugName: "modalLayout" }] : /* istanbul ignore next */ []));
234
+ /** Nombre accesible del buscador. Default: `searchPlaceholder`. */
235
+ searchAriaLabel = input(/* @ts-ignore */
236
+ ...(ngDevMode ? [undefined, { debugName: "searchAriaLabel" }] : /* istanbul ignore next */ []));
237
+ /** Estado de carga: spinner con `loadingLabel` en el modal y, con
238
+ * `triggerSize="compact"`, un esqueleto del trigger del mismo alto. */
239
+ loading = input(false, /* @ts-ignore */
240
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
241
+ /** Texto del estado de carga. */
242
+ loadingLabel = input('Cargando…', /* @ts-ignore */
243
+ ...(ngDevMode ? [{ debugName: "loadingLabel" }] : /* istanbul ignore next */ []));
244
+ /** Mensaje de error de la variante `modal`; con valor muestra el estado de error y "Reintentar". */
245
+ error = input(null, /* @ts-ignore */
246
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
247
+ /** Tamaño del trigger de la variante `inline`. Ver JSDoc de la clase. */
248
+ triggerSize = input('default', /* @ts-ignore */
249
+ ...(ngDevMode ? [{ debugName: "triggerSize" }] : /* istanbul ignore next */ []));
250
+ /** Con `false`, el trigger abre directo el modal (sin panel inline). */
251
+ inlinePanel = input(true, { ...(ngDevMode ? { debugName: "inlinePanel" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
252
+ /** Ancho máximo del modal (se topa con el viewport menos 32px). */
253
+ modalWidth = input('1040px', /* @ts-ignore */
254
+ ...(ngDevMode ? [{ debugName: "modalWidth" }] : /* istanbul ignore next */ []));
255
+ /** Sustantivo del conteo del encabezado. Default "resultado/resultados". */
256
+ itemLabels = input({
257
+ singular: 'resultado',
258
+ plural: 'resultados',
259
+ }, /* @ts-ignore */
260
+ ...(ngDevMode ? [{ debugName: "itemLabels" }] : /* istanbul ignore next */ []));
261
+ /** Chip inicial "sin filtro" con el total. Vacío (`''`) lo oculta. */
262
+ allChipLabel = input('Todas', /* @ts-ignore */
263
+ ...(ngDevMode ? [{ debugName: "allChipLabel" }] : /* istanbul ignore next */ []));
264
+ /** Nombre accesible del grupo de chips del modal. */
265
+ chipsAriaLabel = input('Filtrar resultados', /* @ts-ignore */
266
+ ...(ngDevMode ? [{ debugName: "chipsAriaLabel" }] : /* istanbul ignore next */ []));
267
+ /** Cabecera de columnas en `modalLayout="list"` (solo visual). */
268
+ listHeader = input(true, { ...(ngDevMode ? { debugName: "listHeader" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
161
269
  selectedIdChange = output();
270
+ /** "Reintentar" del estado de error. */
271
+ retry = output();
162
272
  /** Disparado al superar `expandThreshold` en modo inline; el consumidor decide pasar a `modal`. */
163
273
  expandToModal = output();
164
274
  searchQuery = signal('', /* @ts-ignore */
@@ -178,9 +288,15 @@ class QuotePicker {
178
288
  * vs. cards apiladas dentro del modal.
179
289
  */
180
290
  isMobile = matchMediaSignal('(max-width: 900px)');
291
+ /** Mismo corte que el modal a pantalla completa: chips en una sola línea. */
292
+ isNarrowViewport = matchMediaSignal('(max-width: 640px)');
181
293
  destroyRef = inject(DestroyRef);
182
294
  previewScrollEl = viewChild('previewScroll', /* @ts-ignore */
183
295
  ...(ngDevMode ? [{ debugName: "previewScrollEl" }] : /* istanbul ignore next */ []));
296
+ modalListEl = viewChild('modalList', /* @ts-ignore */
297
+ ...(ngDevMode ? [{ debugName: "modalListEl" }] : /* istanbul ignore next */ []));
298
+ /** Para desplazar la fila actual a la vista UNA vez por apertura, no en cada render. */
299
+ scrolledToCurrent = false;
184
300
  /** Mismo mecanismo de fade que `kf-data-table` — ver comentario en QuotePicker.css. */
185
301
  canFadeBottom = signal(false, /* @ts-ignore */
186
302
  ...(ngDevMode ? [{ debugName: "canFadeBottom" }] : /* istanbul ignore next */ []));
@@ -250,12 +366,107 @@ class QuotePicker {
250
366
  return this.sortedItems().slice(start, start + PAGE_SIZE);
251
367
  }, /* @ts-ignore */
252
368
  ...(ngDevMode ? [{ debugName: "pagedItems" }] : /* istanbul ignore next */ []));
369
+ resolvedSearchAriaLabel = computed(() => this.searchAriaLabel() ?? this.searchPlaceholder(), /* @ts-ignore */
370
+ ...(ngDevMode ? [{ debugName: "resolvedSearchAriaLabel" }] : /* istanbul ignore next */ []));
371
+ /** Estado del cuerpo del modal para `kf-content-state`. */
372
+ modalState = computed(() => this.loading() ? 'loading' : this.error() ? 'error' : 'ready', /* @ts-ignore */
373
+ ...(ngDevMode ? [{ debugName: "modalState" }] : /* istanbul ignore next */ []));
374
+ /**
375
+ * Conteo del encabezado, en región `aria-live`: "86 cotizaciones" sin
376
+ * filtros, "12 de 86 cotizaciones" con búsqueda o chip. Vacío mientras
377
+ * carga o con error.
378
+ */
379
+ resultsLabel = computed(() => {
380
+ if (this.modalState() !== 'ready') {
381
+ return '';
382
+ }
383
+ const total = this.items().length;
384
+ const shown = this.sortedItems().length;
385
+ const { singular, plural } = this.itemLabels();
386
+ const noun = total === 1 ? singular : plural;
387
+ return shown === total ? `${total} ${noun}` : `${shown} de ${total} ${noun}`;
388
+ }, /* @ts-ignore */
389
+ ...(ngDevMode ? [{ debugName: "resultsLabel" }] : /* istanbul ignore next */ []));
390
+ /** Chips del modal en el formato de `kf-filter-chips`, con "Todas" al inicio. */
391
+ chipOptions = computed(() => {
392
+ const chips = this.chips().map((chip) => ({
393
+ value: chip.value,
394
+ label: chip.label,
395
+ count: chip.count,
396
+ }));
397
+ const allLabel = this.allChipLabel();
398
+ return allLabel
399
+ ? [{ value: '', label: allLabel, count: this.items().length }, ...chips]
400
+ : chips;
401
+ }, /* @ts-ignore */
402
+ ...(ngDevMode ? [{ debugName: "chipOptions" }] : /* istanbul ignore next */ []));
403
+ /**
404
+ * Pistas de la grilla de `modalLayout="list"`, en el orden visual: título,
405
+ * primer campo (flexible), resto, estados y el check. Todas las filas
406
+ * comparten estas pistas vía `subgrid`, por eso las columnas alinean.
407
+ */
408
+ listGridColumns = computed(() => {
409
+ const tracks = [];
410
+ const titleCol = this.titleColumn();
411
+ const primary = this.primaryFieldColumn();
412
+ if (titleCol) {
413
+ tracks.push(titleCol.width ?? 'max-content');
414
+ }
415
+ if (primary) {
416
+ tracks.push(primary.width ?? 'minmax(0, 1fr)');
417
+ }
418
+ for (const col of this.restFieldColumns()) {
419
+ tracks.push(col.width ?? (col.truncate ? `minmax(0, ${col.maxWidth ?? 'max-content'})` : 'max-content'));
420
+ }
421
+ for (const col of this.statusColumns()) {
422
+ tracks.push(col.width ?? `fit-content(${col.maxWidth ?? '13.75rem'})`);
423
+ }
424
+ tracks.push('1.25rem');
425
+ return tracks.join(' ');
426
+ }, /* @ts-ignore */
427
+ ...(ngDevMode ? [{ debugName: "listGridColumns" }] : /* istanbul ignore next */ []));
428
+ sortSelectOptions = computed(() => this.sortOptions().map((option) => ({ value: option.value, label: option.label })), /* @ts-ignore */
429
+ ...(ngDevMode ? [{ debugName: "sortSelectOptions" }] : /* istanbul ignore next */ []));
430
+ statusColumns = computed(() => this.columns().filter((c) => c.cardRole === 'status'), /* @ts-ignore */
431
+ ...(ngDevMode ? [{ debugName: "statusColumns" }] : /* istanbul ignore next */ []));
432
+ /** Primer campo (ej. modelo): ocupa el espacio libre de la fila en `modalLayout="list"`. */
433
+ primaryFieldColumn = computed(() => this.fieldColumns()[0], /* @ts-ignore */
434
+ ...(ngDevMode ? [{ debugName: "primaryFieldColumn" }] : /* istanbul ignore next */ []));
435
+ restFieldColumns = computed(() => this.fieldColumns().slice(1), /* @ts-ignore */
436
+ ...(ngDevMode ? [{ debugName: "restFieldColumns" }] : /* istanbul ignore next */ []));
437
+ /** Alineada al final: `align: 'end'` o el histórico `right: true`. */
438
+ isEndAligned(column) {
439
+ return column.align === 'end' || !!column.right;
440
+ }
441
+ /** `title` con el valor completo de una celda que puede cortarse con elipsis. */
442
+ cellTitle(item, column) {
443
+ return this.cellText(item, column) || null;
444
+ }
445
+ isSelected(item) {
446
+ return this.idKey()(item) === this.selectedId();
447
+ }
253
448
  sortLabel = computed(() => {
254
449
  const current = this.sortOptions().find((option) => option.value === this.sort());
255
450
  return current?.label ?? 'Ordenar por';
256
451
  }, /* @ts-ignore */
257
452
  ...(ngDevMode ? [{ debugName: "sortLabel" }] : /* istanbul ignore next */ []));
258
453
  constructor() {
454
+ afterRenderEffect(() => {
455
+ const list = this.modalListEl()?.nativeElement;
456
+ if (!this.open() || !list) {
457
+ this.scrolledToCurrent = false;
458
+ return;
459
+ }
460
+ untracked(() => {
461
+ if (this.scrolledToCurrent) {
462
+ return;
463
+ }
464
+ this.scrolledToCurrent = true;
465
+ list
466
+ .querySelector('.kf-quote-picker__row--current')
467
+ ?.scrollIntoView?.({ block: 'nearest' });
468
+ });
469
+ });
259
470
  afterNextRender(() => {
260
471
  const el = this.previewScrollEl()?.nativeElement;
261
472
  if (!el) {
@@ -284,14 +495,32 @@ class QuotePicker {
284
495
  toggleExpandedPanel() {
285
496
  this.expanded.set(!this.expanded());
286
497
  }
498
+ /** Click en el trigger: alterna el panel inline o, sin él, abre el modal. */
499
+ onTriggerClick() {
500
+ if (this.inlinePanel()) {
501
+ this.toggleExpandedPanel();
502
+ }
503
+ else {
504
+ this.open.set(true);
505
+ }
506
+ }
287
507
  onSearchInput(event) {
288
- this.searchQuery.set(event.target.value);
508
+ this.onSearchChange(event.target.value);
509
+ }
510
+ /** Buscador del modal (`kf-input` con `ngModel`), incluido el botón "×". */
511
+ onSearchChange(value) {
512
+ this.searchQuery.set(value ?? '');
289
513
  this.page.set(1);
290
514
  }
291
515
  selectChip(value) {
292
516
  this.activeChip.set(this.activeChip() === value ? null : value);
293
517
  this.page.set(1);
294
518
  }
519
+ /** Chips del modal: `''` es "Todas" (sin filtro). */
520
+ onChipChange(value) {
521
+ this.activeChip.set(value || null);
522
+ this.page.set(1);
523
+ }
295
524
  select(item) {
296
525
  const id = this.idKey()(item);
297
526
  // El panel se cierra siempre al elegir (haya cambiado el id o no) —
@@ -300,6 +529,9 @@ class QuotePicker {
300
529
  // consumidor sería una carga que ningún otro componente de la
301
530
  // librería le pide.
302
531
  this.expanded.set(false);
532
+ // El modal también se cierra al elegir: el consumidor ya no necesita
533
+ // cerrarlo a mano en `selectedIdChange` (hacerlo igual es inocuo).
534
+ this.open.set(false);
303
535
  if (id === this.selectedId()) {
304
536
  return;
305
537
  }
@@ -317,9 +549,14 @@ class QuotePicker {
317
549
  onTableRowClick(event) {
318
550
  this.select(event.row);
319
551
  }
552
+ /** @deprecated El orden de la variante modal ahora es un `kf-select`; este método ya no lo usa la plantilla. */
320
553
  toggleSort() {
321
554
  this.sortOpen.update((open) => !open);
322
555
  }
556
+ onSortChange(value) {
557
+ this.sort.set(value);
558
+ }
559
+ /** Alterna el orden (elegir el vigente lo quita). Se conserva por compatibilidad. */
323
560
  setSort(value) {
324
561
  this.sort.set(this.sort() === value ? null : value);
325
562
  this.sortOpen.set(false);
@@ -343,12 +580,27 @@ class QuotePicker {
343
580
  fieldColumns = computed(() => this.columns().filter((c) => !c.cardRole || c.cardRole === 'field'), /* @ts-ignore */
344
581
  ...(ngDevMode ? [{ debugName: "fieldColumns" }] : /* istanbul ignore next */ []));
345
582
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: QuotePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
346
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: QuotePicker, isStandalone: true, selector: "kf-quote-picker", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, selectedId: { classPropertyName: "selectedId", publicName: "selectedId", isSignal: true, isRequired: false, transformFunction: null }, idKey: { classPropertyName: "idKey", publicName: "idKey", isSignal: true, isRequired: true, transformFunction: null }, cellTemplates: { classPropertyName: "cellTemplates", publicName: "cellTemplates", isSignal: true, isRequired: false, transformFunction: null }, chips: { classPropertyName: "chips", publicName: "chips", isSignal: true, isRequired: false, transformFunction: null }, sortOptions: { classPropertyName: "sortOptions", publicName: "sortOptions", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, expandThreshold: { classPropertyName: "expandThreshold", publicName: "expandThreshold", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, placeholderIcon: { classPropertyName: "placeholderIcon", publicName: "placeholderIcon", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sort: "sortChange", open: "openChange", selectedIdChange: "selectedIdChange", expandToModal: "expandToModal" }, viewQueries: [{ propertyName: "previewScrollEl", first: true, predicate: ["previewScroll"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (variant() === 'inline') {\n <div class=\"kf-quote-picker__inline\">\n <!-- Trigger colapsado (default): ancho completo, nunca una caja angosta. -->\n <button\n type=\"button\"\n class=\"kf-quote-picker__trigger\"\n [attr.aria-expanded]=\"expanded()\"\n (click)=\"toggleExpandedPanel()\"\n >\n <span class=\"kf-quote-picker__thumb\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: selectedItem(), index: 0 }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"md\" />\n }\n </span>\n @if (selectedItem(); as item) {\n <span class=\"kf-quote-picker__trigger-summary\">\n @if (titleColumn(); as title) {\n <span class=\"kf-quote-picker__trigger-field kf-quote-picker__trigger-field--title\">{{\n cellValue(item, title)\n }}</span>\n }\n <!--\n En mobile solo se muestra el t\u00EDtulo (ID) \u2014 mostrar los 5 campos\n en una sola l\u00EDnea de ~90px de ancho real corta el texto sin\n elipsis a mitad del segundo campo (bug real medido:\n scrollWidth=519 vs. clientWidth=87 en 390px). El resto de los\n campos siguen disponibles en el panel expandido y en el modal,\n donde s\u00ED hay espacio horizontal.\n -->\n @if (!isMobile()) {\n @for (col of fieldColumns(); track col.key) {\n <span class=\"kf-quote-picker__trigger-sep\" aria-hidden=\"true\">\u00B7</span>\n <span class=\"kf-quote-picker__trigger-field\">{{ cellValue(item, col) }}</span>\n }\n }\n </span>\n } @else {\n <span class=\"kf-quote-picker__trigger-summary kf-quote-picker__trigger-summary--empty\"\n >Sin selecci\u00F3n</span\n >\n }\n <span class=\"kf-quote-picker__trigger-action\">\n {{ expanded() ? 'Cerrar' : title() }}\n <kf-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"kf-quote-picker__chevron\"\n [class.kf-quote-picker__chevron--open]=\"expanded()\"\n />\n </span>\n </button>\n\n @if (expanded()) {\n <div class=\"kf-quote-picker__panel\">\n <div class=\"kf-quote-picker__toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n\n <div class=\"kf-quote-picker__results-wrap\">\n <div #previewScroll class=\"kf-quote-picker__results\" role=\"listbox\">\n @for (item of previewItems(); track trackItem(item, $index)) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__result\"\n [class.kf-quote-picker__result--active]=\"idKey()(item) === selectedId()\"\n role=\"option\"\n [attr.aria-selected]=\"idKey()(item) === selectedId()\"\n (click)=\"select(item)\"\n (keydown)=\"onItemKeydown($event, item)\"\n >\n <span class=\"kf-quote-picker__thumb kf-quote-picker__thumb--sm\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: $index }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"sm\" />\n }\n </span>\n @if (titleColumn(); as title) {\n <span\n class=\"kf-quote-picker__result-field kf-quote-picker__result-field--title\"\n >{{ cellValue(item, title) }}</span\n >\n }\n <!--\n Wrapper propio (no los spans sueltos) para los campos\n secundarios: en mobile necesita saltar de l\u00EDnea COMPLETO\n (mismo criterio que \"unidades\" no debe competir con\n \"modelo\" en la primera l\u00EDnea) \u2014 `flex-basis: 100%` en un\n solo elemento fuerza ese salto sin tener que asumir cu\u00E1l\n de los N campos gen\u00E9ricos es el m\u00E1s corto o el m\u00E1s largo.\n -->\n <span class=\"kf-quote-picker__result-fields\">\n @for (col of fieldColumns(); track col.key; let first = $first) {\n <span\n class=\"kf-quote-picker__result-field\"\n [class.kf-quote-picker__result-field--primary]=\"first\"\n >{{ cellValue(item, col) }}</span\n >\n }\n </span>\n </button>\n } @empty {\n <p class=\"kf-quote-picker__empty\">Sin resultados para esta b\u00FAsqueda.</p>\n }\n </div>\n @if (canFadeBottom()) {\n <div class=\"kf-quote-picker__fade\" aria-hidden=\"true\"></div>\n }\n </div>\n\n @if (isHighVolume()) {\n <button type=\"button\" class=\"kf-quote-picker__see-all\" (click)=\"onSeeAll()\">\n Ver las {{ sortedItems().length }} en pantalla completa\n </button>\n }\n </div>\n }\n </div>\n} @else {\n <kf-modal [(open)]=\"open\" [title]=\"title()\" [dismissible]=\"true\">\n <div class=\"kf-quote-picker__modal-toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n <div class=\"kf-quote-picker__modal-filters\">\n <div class=\"kf-quote-picker__modal-chips\">\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n @if (sortOptions().length > 0) {\n <div class=\"kf-quote-picker__sort\">\n <button\n type=\"button\"\n class=\"kf-quote-picker__sort-trigger\"\n [attr.aria-expanded]=\"sortOpen()\"\n (click)=\"toggleSort()\"\n >\n <kf-icon name=\"sort\" size=\"sm\" />\n {{ sortLabel() }}\n <kf-icon name=\"chevron-down\" size=\"sm\" />\n </button>\n @if (sortOpen()) {\n <div class=\"kf-quote-picker__sort-panel\">\n @for (option of sortOptions(); track option.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__sort-option\"\n [class.kf-quote-picker__sort-option--active]=\"sort() === option.value\"\n (click)=\"setSort(option.value)\"\n >\n {{ option.label }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <kf-data-table\n [columns]=\"columns()\"\n [rows]=\"pagedItems()\"\n [cardable]=\"true\"\n [defaultView]=\"isMobile() ? 'cards' : 'table'\"\n [cellTemplates]=\"cellTemplates()\"\n [rowKey]=\"rowKeyFn\"\n (rowClick)=\"onTableRowClick($event)\"\n >\n <kf-empty-state empty icon=\"file\">Sin resultados para esta b\u00FAsqueda.</kf-empty-state>\n @if (sortedItems().length > 0) {\n <kf-pagination footer [page]=\"page()\" [pages]=\"pages()\" (pageChange)=\"page.set($event)\">\n {{ sortedItems().length }} resultados\n </kf-pagination>\n }\n </kf-data-table>\n </kf-modal>\n}\n", styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-quote-picker__trigger{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-md);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-primary);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-sm);cursor:pointer;font:inherit;text-align:left}.kf-quote-picker__trigger:hover{box-shadow:var(--kf-shadow-md)}.kf-quote-picker__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__thumb{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:none;border-radius:var(--kf-radius-sm);background:var(--kf-color-surface);color:var(--kf-color-gray-400)}.kf-quote-picker__thumb--sm{width:36px;height:36px}.kf-quote-picker__result--active .kf-quote-picker__thumb{background:var(--kf-color-white);color:var(--kf-color-primary)}.kf-quote-picker__trigger-summary{flex:1;min-width:0;display:flex;align-items:baseline;gap:var(--kf-space-xs);overflow:hidden;white-space:nowrap;color:var(--kf-color-ink)}.kf-quote-picker__trigger-summary--empty{color:var(--kf-color-text-muted)}.kf-quote-picker__trigger-field--title{font-family:var(--kf-font-title);font-weight:700;font-size:1.05rem;color:var(--kf-color-ink)}.kf-quote-picker__trigger-field{font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__trigger-sep{color:var(--kf-color-border-strong)}.kf-quote-picker__trigger-action{flex:none;display:flex;align-items:center;gap:6px;color:var(--kf-color-primary);font-size:.85rem;font-weight:600}.kf-quote-picker__chevron{transition:transform .14s}.kf-quote-picker__chevron--open{transform:rotate(180deg)}.kf-quote-picker__panel{display:flex;flex-direction:column;gap:var(--kf-space-md);margin-top:var(--kf-space-sm);padding:var(--kf-space-md) var(--kf-space-lg);background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md)}.kf-quote-picker__toolbar{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}.kf-quote-picker__search{flex:1;min-width:200px;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);color:var(--kf-color-text-muted)}.kf-quote-picker__search:focus-within{border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__search-input{flex:1;border:none;outline:none;background:transparent;font:inherit;color:var(--kf-color-ink)}.kf-quote-picker__search-input::placeholder{color:var(--kf-color-text-muted)}.kf-quote-picker__chip{flex:none;padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__chip--active{background:var(--kf-color-primary);border-color:var(--kf-color-primary);color:var(--kf-color-white)}.kf-quote-picker__results-wrap{position:relative}.kf-quote-picker__results{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--kf-space-sm);max-height:340px;overflow-y:auto}@media(max-width:900px){.kf-quote-picker__results{grid-template-columns:1fr}}.kf-quote-picker__result{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);background:var(--kf-color-white);cursor:pointer;text-align:left;font:inherit;min-width:0;flex-wrap:wrap;row-gap:2px}.kf-quote-picker__result-fields{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0;flex:1}.kf-quote-picker__result:hover{border-color:var(--kf-color-border)}.kf-quote-picker__result--active{border-color:var(--kf-color-primary);background:var(--kf-color-primary-soft)}.kf-quote-picker__result-field{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__result-field--title{flex:none;min-width:92px;font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__result-field--primary{flex:1;min-width:0}@media(max-width:900px){.kf-quote-picker__result-fields{flex-basis:100%;flex-wrap:wrap;row-gap:2px;padding-left:calc(36px + var(--kf-space-sm))}.kf-quote-picker__result-field--primary{flex:none;min-width:0;max-width:100%}}.kf-quote-picker__fade{position:absolute;left:0;right:0;bottom:0;height:36px;background:linear-gradient(to top,var(--kf-color-surface),transparent);pointer-events:none}.kf-quote-picker__see-all{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:var(--kf-space-sm);width:100%;margin-top:var(--kf-space-sm);padding:var(--kf-space-sm);border:1.5px dashed var(--kf-color-primary);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-primary);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer}.kf-quote-picker__see-all:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__empty{grid-column:1 / -1;padding:var(--kf-space-md);text-align:center;color:var(--kf-color-text-muted);font-size:.85rem;margin:0}.kf-quote-picker__modal-toolbar{display:flex;flex-direction:column;gap:var(--kf-space-sm);margin-bottom:var(--kf-space-md)}.kf-quote-picker__modal-filters{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);flex-wrap:wrap}.kf-quote-picker__modal-chips{display:flex;gap:var(--kf-space-sm);flex-wrap:wrap}.kf-quote-picker__sort{position:relative;flex:none}.kf-quote-picker__sort-trigger{display:flex;align-items:center;gap:var(--kf-space-sm);padding:6px var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__sort-trigger:hover{border-color:var(--kf-color-border-strong)}.kf-quote-picker__sort-panel{position:absolute;top:calc(100% + 4px);right:0;z-index:1;min-width:180px;padding:var(--kf-space-xs) 0;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-md)}.kf-quote-picker__sort-option{display:block;width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;background:transparent;color:var(--kf-color-text);font:inherit;font-size:.85rem;text-align:left;cursor:pointer}.kf-quote-picker__sort-option:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__sort-option--active{color:var(--kf-color-primary);font-weight:600}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size", "rotate", "filled"] }, { kind: "component", type: Modal, selector: "kf-modal", inputs: ["title", "open", "dismissible", "width"], outputs: ["openChange"] }, { kind: "component", type: DataTable, selector: "kf-data-table", inputs: ["columns", "rows", "loading", "rowKey", "flush", "cardable", "defaultView", "render", "cellTemplates", "density", "rowClickable", "pageSize", "pageSizeOptions", "itemLabel", "sort"], outputs: ["rowClick", "pageChange", "pageSizeChange", "sortChange"] }, { kind: "component", type: EmptyState, selector: "kf-empty-state", inputs: ["icon", "tone"] }, { kind: "component", type: Pagination, selector: "kf-pagination", inputs: ["page", "pages", "pageSizeOptions", "pageSize"], outputs: ["pageChange", "pageSizeChange"] }] });
583
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: QuotePicker, isStandalone: true, selector: "kf-quote-picker", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, selectedId: { classPropertyName: "selectedId", publicName: "selectedId", isSignal: true, isRequired: false, transformFunction: null }, idKey: { classPropertyName: "idKey", publicName: "idKey", isSignal: true, isRequired: true, transformFunction: null }, cellTemplates: { classPropertyName: "cellTemplates", publicName: "cellTemplates", isSignal: true, isRequired: false, transformFunction: null }, chips: { classPropertyName: "chips", publicName: "chips", isSignal: true, isRequired: false, transformFunction: null }, sortOptions: { classPropertyName: "sortOptions", publicName: "sortOptions", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, expandThreshold: { classPropertyName: "expandThreshold", publicName: "expandThreshold", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, placeholderIcon: { classPropertyName: "placeholderIcon", publicName: "placeholderIcon", isSignal: true, isRequired: false, transformFunction: null }, modalLayout: { classPropertyName: "modalLayout", publicName: "modalLayout", isSignal: true, isRequired: false, transformFunction: null }, searchAriaLabel: { classPropertyName: "searchAriaLabel", publicName: "searchAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingLabel: { classPropertyName: "loadingLabel", publicName: "loadingLabel", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, triggerSize: { classPropertyName: "triggerSize", publicName: "triggerSize", isSignal: true, isRequired: false, transformFunction: null }, inlinePanel: { classPropertyName: "inlinePanel", publicName: "inlinePanel", isSignal: true, isRequired: false, transformFunction: null }, modalWidth: { classPropertyName: "modalWidth", publicName: "modalWidth", isSignal: true, isRequired: false, transformFunction: null }, itemLabels: { classPropertyName: "itemLabels", publicName: "itemLabels", isSignal: true, isRequired: false, transformFunction: null }, allChipLabel: { classPropertyName: "allChipLabel", publicName: "allChipLabel", isSignal: true, isRequired: false, transformFunction: null }, chipsAriaLabel: { classPropertyName: "chipsAriaLabel", publicName: "chipsAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, listHeader: { classPropertyName: "listHeader", publicName: "listHeader", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sort: "sortChange", open: "openChange", selectedIdChange: "selectedIdChange", retry: "retry", expandToModal: "expandToModal" }, viewQueries: [{ propertyName: "previewScrollEl", first: true, predicate: ["previewScroll"], descendants: true, isSignal: true }, { propertyName: "modalListEl", first: true, predicate: ["modalList"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (variant() === 'inline') {\n <div class=\"kf-quote-picker__inline\">\n <!-- Trigger colapsado. `default`: tarjeta a ancho completo con miniatura\n (hist\u00F3rico). `compact`: control de barra de herramientas de alto fijo,\n sin miniatura. Con `inlinePanel` en false abre directo el modal. -->\n @if (triggerSize() === 'compact' && loading()) {\n <!-- Mismo alto que el trigger real: la barra no salta al llegar los datos. -->\n <div\n class=\"kf-quote-picker__trigger kf-quote-picker__trigger--compact kf-quote-picker__trigger--loading\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <span class=\"kf-quote-picker__sr-only\">{{ loadingLabel() }}</span>\n <span class=\"kf-quote-picker__compact\" aria-hidden=\"true\">\n <kf-skeleton width=\"35%\" height=\"12px\" />\n <kf-skeleton width=\"65%\" height=\"12px\" />\n </span>\n </div>\n } @else {\n <button\n type=\"button\"\n class=\"kf-quote-picker__trigger\"\n [class.kf-quote-picker__trigger--compact]=\"triggerSize() === 'compact'\"\n [attr.aria-expanded]=\"inlinePanel() ? expanded() : open()\"\n [attr.aria-haspopup]=\"inlinePanel() ? null : 'dialog'\"\n (click)=\"onTriggerClick()\"\n >\n @if (triggerSize() === 'compact') {\n <!-- Nombre accesible = texto visible con un prefijo y comas solo\n para lectores de pantalla: \"Cambiar cotizaci\u00F3n. Actual: <id>,\n <estado>, <modelo>\". -->\n <span class=\"kf-quote-picker__sr-only\">{{ title() }}. Actual: </span>\n @if (selectedItem(); as item) {\n <span class=\"kf-quote-picker__compact\">\n <span class=\"kf-quote-picker__compact-line\">\n @if (titleColumn(); as titleCol) {\n <span class=\"kf-quote-picker__compact-id\">{{ cellValue(item, titleCol) }}</span>\n }\n @for (col of statusColumns(); track col.key) {\n <span class=\"kf-quote-picker__sr-only\">, </span>\n <span class=\"kf-quote-picker__compact-status\">\n <ng-container\n *ngTemplateOutlet=\"cell; context: { $implicit: item, column: col, index: 0 }\"\n />\n </span>\n }\n </span>\n @if (primaryFieldColumn(); as primary) {\n <span class=\"kf-quote-picker__compact-line\">\n <span class=\"kf-quote-picker__sr-only\">, </span>\n <span class=\"kf-quote-picker__compact-primary\">{{\n cellValue(item, primary)\n }}</span>\n </span>\n }\n </span>\n } @else {\n <span class=\"kf-quote-picker__compact kf-quote-picker__trigger-summary--empty\"\n >Sin selecci\u00F3n</span\n >\n }\n <kf-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"kf-quote-picker__chevron\"\n [class.kf-quote-picker__chevron--open]=\"inlinePanel() && expanded()\"\n />\n } @else {\n <span class=\"kf-quote-picker__thumb\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: selectedItem(), index: 0 }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"md\" />\n }\n </span>\n @if (selectedItem(); as item) {\n <span class=\"kf-quote-picker__trigger-summary\">\n @if (titleColumn(); as title) {\n <span\n class=\"kf-quote-picker__trigger-field kf-quote-picker__trigger-field--title\"\n >{{ cellValue(item, title) }}</span\n >\n }\n <!--\n En mobile solo se muestra el t\u00EDtulo (ID) \u2014 mostrar los 5 campos\n en una sola l\u00EDnea de ~90px de ancho real corta el texto sin\n elipsis a mitad del segundo campo (bug real medido:\n scrollWidth=519 vs. clientWidth=87 en 390px). El resto de los\n campos siguen disponibles en el panel expandido y en el modal,\n donde s\u00ED hay espacio horizontal.\n -->\n @if (!isMobile()) {\n @for (col of fieldColumns(); track col.key) {\n <span class=\"kf-quote-picker__trigger-sep\" aria-hidden=\"true\">\u00B7</span>\n <span class=\"kf-quote-picker__trigger-field\">{{ cellValue(item, col) }}</span>\n }\n }\n </span>\n } @else {\n <span class=\"kf-quote-picker__trigger-summary kf-quote-picker__trigger-summary--empty\"\n >Sin selecci\u00F3n</span\n >\n }\n <span class=\"kf-quote-picker__trigger-action\">\n {{ inlinePanel() && expanded() ? 'Cerrar' : title() }}\n <kf-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"kf-quote-picker__chevron\"\n [class.kf-quote-picker__chevron--open]=\"inlinePanel() && expanded()\"\n />\n </span>\n }\n </button>\n }\n\n @if (inlinePanel() && expanded()) {\n <div class=\"kf-quote-picker__panel\">\n <div class=\"kf-quote-picker__toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"resolvedSearchAriaLabel()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n [attr.aria-pressed]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n\n <div class=\"kf-quote-picker__results-wrap\">\n <div #previewScroll class=\"kf-quote-picker__results\" role=\"listbox\">\n @for (item of previewItems(); track trackItem(item, $index)) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__result\"\n [class.kf-quote-picker__result--active]=\"idKey()(item) === selectedId()\"\n role=\"option\"\n [attr.aria-selected]=\"idKey()(item) === selectedId()\"\n (click)=\"select(item)\"\n (keydown)=\"onItemKeydown($event, item)\"\n >\n <span class=\"kf-quote-picker__thumb kf-quote-picker__thumb--sm\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: $index }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"sm\" />\n }\n </span>\n @if (titleColumn(); as title) {\n <span\n class=\"kf-quote-picker__result-field kf-quote-picker__result-field--title\"\n >{{ cellValue(item, title) }}</span\n >\n }\n <!--\n Wrapper propio (no los spans sueltos) para los campos\n secundarios: en mobile necesita saltar de l\u00EDnea COMPLETO\n (mismo criterio que \"unidades\" no debe competir con\n \"modelo\" en la primera l\u00EDnea) \u2014 `flex-basis: 100%` en un\n solo elemento fuerza ese salto sin tener que asumir cu\u00E1l\n de los N campos gen\u00E9ricos es el m\u00E1s corto o el m\u00E1s largo.\n -->\n <span class=\"kf-quote-picker__result-fields\">\n @for (col of fieldColumns(); track col.key; let first = $first) {\n <span\n class=\"kf-quote-picker__result-field\"\n [class.kf-quote-picker__result-field--primary]=\"first\"\n >{{ cellValue(item, col) }}</span\n >\n }\n </span>\n </button>\n } @empty {\n <p class=\"kf-quote-picker__empty\">Sin resultados para esta b\u00FAsqueda.</p>\n }\n </div>\n @if (canFadeBottom()) {\n <div class=\"kf-quote-picker__fade\" aria-hidden=\"true\"></div>\n }\n </div>\n\n @if (isHighVolume()) {\n <button type=\"button\" class=\"kf-quote-picker__see-all\" (click)=\"onSeeAll()\">\n Ver las {{ sortedItems().length }} en pantalla completa\n </button>\n }\n </div>\n }\n </div>\n}\n\n<!-- Modal: la variante `modal` y el que abre el trigger con `inlinePanel` en\n false. Zona de filtros FIJA (slot `kf-modal-toolbar`) y lista\n desplazable (cuerpo del modal). -->\n@if (variant() === 'modal' || !inlinePanel()) {\n <kf-modal\n class=\"kf-quote-picker__modal\"\n [(open)]=\"open\"\n [title]=\"title()\"\n [dismissible]=\"true\"\n [width]=\"modalWidth()\"\n maxHeight=\"85vh\"\n fullscreenOnMobile\n initialFocus=\".kf-quote-picker__modal-search\"\n >\n <p kf-modal-meta class=\"kf-quote-picker__count\" aria-live=\"polite\">{{ resultsLabel() }}</p>\n\n <div kf-modal-toolbar class=\"kf-quote-picker__filters\">\n <div class=\"kf-quote-picker__filters-row\">\n <kf-input\n class=\"kf-quote-picker__modal-search\"\n type=\"search\"\n icon=\"search\"\n clearable\n [placeholder]=\"searchPlaceholder()\"\n [ariaLabel]=\"resolvedSearchAriaLabel()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n />\n @if (sortOptions().length > 0) {\n <kf-select\n class=\"kf-quote-picker__sort\"\n icon=\"sort\"\n ariaLabel=\"Ordenar por\"\n panelTitle=\"Ordenar por\"\n placeholder=\"Ordenar por\"\n [options]=\"sortSelectOptions()\"\n [value]=\"sort() ?? undefined\"\n (valueChange)=\"onSortChange($event)\"\n />\n }\n </div>\n @if (chips().length > 0) {\n <kf-filter-chips\n class=\"kf-quote-picker__chips\"\n deselectable\n [ariaLabel]=\"chipsAriaLabel()\"\n [options]=\"chipOptions()\"\n [value]=\"activeChip() ?? ''\"\n [layout]=\"isNarrowViewport() ? 'scroll' : 'wrap'\"\n (valueChange)=\"onChipChange($event)\"\n />\n }\n </div>\n\n <kf-content-state\n [state]=\"modalState()\"\n load=\"spinner\"\n [loadingLabel]=\"loadingLabel()\"\n [title]=\"error() ?? ''\"\n >\n <kf-button actions type=\"button\" variant=\"outline\" icon=\"refresh\" (click)=\"retry.emit()\">\n Reintentar\n </kf-button>\n @if (modalLayout() === 'list') {\n @if (sortedItems().length > 0) {\n <div class=\"kf-quote-picker__list-wrap\">\n <ul\n #modalList\n class=\"kf-quote-picker__list\"\n [style.grid-template-columns]=\"listGridColumns()\"\n >\n @if (listHeader()) {\n <!-- Cabecera visual de columnas: cada fila (bot\u00F3n) ya se anuncia\n completa, as\u00ED que para lectores de pantalla ser\u00EDa ruido. -->\n <li class=\"kf-quote-picker__list-head\" aria-hidden=\"true\">\n @if (titleColumn(); as titleCol) {\n <span class=\"kf-quote-picker__head-cell kf-quote-picker__cell--title\">\n <kf-text as=\"span\" variant=\"overline\">{{ titleCol.label }}</kf-text>\n </span>\n }\n @if (primaryFieldColumn(); as primary) {\n <span class=\"kf-quote-picker__head-cell kf-quote-picker__cell--primary\">\n <kf-text as=\"span\" variant=\"overline\">{{ primary.label }}</kf-text>\n </span>\n }\n @for (col of restFieldColumns(); track col.key) {\n <span\n class=\"kf-quote-picker__head-cell kf-quote-picker__cell--field\"\n [class.kf-quote-picker__cell--end]=\"isEndAligned(col)\"\n [class.kf-quote-picker__cell--center]=\"col.align === 'center'\"\n >\n <kf-text as=\"span\" variant=\"overline\">{{ col.label }}</kf-text>\n </span>\n }\n @for (col of statusColumns(); track col.key) {\n <span class=\"kf-quote-picker__head-cell kf-quote-picker__cell--status\">\n <kf-text as=\"span\" variant=\"overline\">{{ col.label }}</kf-text>\n </span>\n }\n <span class=\"kf-quote-picker__head-cell kf-quote-picker__cell--check\"></span>\n </li>\n }\n @for (item of sortedItems(); track trackItem(item, $index); let i = $index) {\n <li class=\"kf-quote-picker__list-item\">\n <button\n type=\"button\"\n class=\"kf-quote-picker__row\"\n [class.kf-quote-picker__row--current]=\"isSelected(item)\"\n [attr.aria-current]=\"isSelected(item) ? 'true' : null\"\n (click)=\"select(item)\"\n >\n <span class=\"kf-quote-picker__row-head\">\n @if (titleColumn(); as titleCol) {\n <span class=\"kf-quote-picker__row-title kf-quote-picker__cell--title\">\n <ng-container\n *ngTemplateOutlet=\"\n cell;\n context: { $implicit: item, column: titleCol, index: i }\n \"\n />\n </span>\n }\n @for (col of statusColumns(); track col.key) {\n <span class=\"kf-quote-picker__row-status kf-quote-picker__cell--status\">\n <ng-container\n *ngTemplateOutlet=\"\n cell;\n context: { $implicit: item, column: col, index: i }\n \"\n />\n </span>\n }\n </span>\n <span class=\"kf-quote-picker__row-body\">\n @if (primaryFieldColumn(); as primary) {\n <span\n class=\"kf-quote-picker__row-primary kf-quote-picker__cell--primary\"\n [attr.title]=\"cellTitle(item, primary)\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n cell;\n context: { $implicit: item, column: primary, index: i }\n \"\n />\n </span>\n }\n @for (col of restFieldColumns(); track col.key) {\n <span\n class=\"kf-quote-picker__row-field kf-quote-picker__cell--field\"\n [class.kf-quote-picker__row-field--right]=\"isEndAligned(col)\"\n [class.kf-quote-picker__cell--end]=\"isEndAligned(col)\"\n [class.kf-quote-picker__cell--center]=\"col.align === 'center'\"\n [class.kf-quote-picker__cell--truncate]=\"col.truncate\"\n [attr.title]=\"col.truncate ? cellTitle(item, col) : null\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n cell;\n context: { $implicit: item, column: col, index: i }\n \"\n />\n </span>\n }\n </span>\n <span class=\"kf-quote-picker__row-check kf-quote-picker__cell--check\">\n @if (isSelected(item)) {\n <kf-icon name=\"check\" size=\"sm\" />\n }\n </span>\n </button>\n </li>\n }\n </ul>\n </div>\n } @else {\n <kf-empty-state icon=\"file\">Sin resultados para esta b\u00FAsqueda.</kf-empty-state>\n }\n } @else {\n <kf-data-table\n [columns]=\"columns()\"\n [rows]=\"pagedItems()\"\n [cardable]=\"true\"\n [defaultView]=\"isMobile() ? 'cards' : 'table'\"\n [cellTemplates]=\"cellTemplates()\"\n [rowKey]=\"rowKeyFn\"\n [currentRowKey]=\"selectedId()\"\n (rowClick)=\"onTableRowClick($event)\"\n >\n <kf-empty-state empty icon=\"file\">Sin resultados para esta b\u00FAsqueda.</kf-empty-state>\n @if (sortedItems().length > 0) {\n <kf-pagination footer [page]=\"page()\" [pages]=\"pages()\" (pageChange)=\"page.set($event)\">\n {{ sortedItems().length }} resultados\n </kf-pagination>\n }\n </kf-data-table>\n }\n </kf-content-state>\n </kf-modal>\n}\n\n<!-- Celda de la lista compacta: el template del consumidor si existe (badge,\n monto formateado), si no el valor como texto. -->\n<ng-template #cell let-item let-column=\"column\" let-index=\"index\">\n @if (cellTemplates()?.[column.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: index }\"\n />\n } @else {\n {{ cellValue(item, column) }}\n }\n</ng-template>\n", styles: [":host{display:block;min-width:0;font-family:var(--kf-font-body)}.kf-quote-picker__sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.kf-quote-picker__trigger{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-md);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-primary);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-sm);cursor:pointer;font:inherit;text-align:left}.kf-quote-picker__trigger:hover{box-shadow:var(--kf-shadow-md)}.kf-quote-picker__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__thumb{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:none;border-radius:var(--kf-radius-sm);background:var(--kf-color-surface);color:var(--kf-color-gray-400)}.kf-quote-picker__thumb--sm{width:36px;height:36px}.kf-quote-picker__result--active .kf-quote-picker__thumb{background:var(--kf-color-white);color:var(--kf-color-primary)}.kf-quote-picker__trigger-summary{flex:1;min-width:0;display:flex;align-items:baseline;gap:var(--kf-space-xs);overflow:hidden;white-space:nowrap;color:var(--kf-color-ink)}.kf-quote-picker__trigger-summary--empty{color:var(--kf-color-text-muted)}.kf-quote-picker__trigger-field--title{font-family:var(--kf-font-title);font-weight:700;font-size:1.05rem;color:var(--kf-color-ink)}.kf-quote-picker__trigger-field{font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__trigger-sep{color:var(--kf-color-border-strong)}.kf-quote-picker__trigger-action{flex:none;display:flex;align-items:center;gap:6px;color:var(--kf-color-primary);font-size:.85rem;font-weight:600}.kf-quote-picker__chevron{transition:transform .14s}.kf-quote-picker__chevron--open{transform:rotate(180deg)}.kf-quote-picker__panel{display:flex;flex-direction:column;gap:var(--kf-space-md);margin-top:var(--kf-space-sm);padding:var(--kf-space-md) var(--kf-space-lg);background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md)}.kf-quote-picker__toolbar{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}.kf-quote-picker__search{flex:1;min-width:200px;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);color:var(--kf-color-text-muted)}.kf-quote-picker__search:focus-within{border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__search-input{flex:1;border:none;outline:none;background:transparent;font:inherit;color:var(--kf-color-ink)}.kf-quote-picker__search-input::placeholder{color:var(--kf-color-text-muted)}.kf-quote-picker__chip{flex:none;padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__chip--active{background:var(--kf-color-primary);border-color:var(--kf-color-primary);color:var(--kf-color-white)}.kf-quote-picker__results-wrap{position:relative}.kf-quote-picker__results{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--kf-space-sm);max-height:340px;overflow-y:auto}@media(max-width:900px){.kf-quote-picker__results{grid-template-columns:1fr}}.kf-quote-picker__result{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);background:var(--kf-color-white);cursor:pointer;text-align:left;font:inherit;min-width:0;flex-wrap:wrap;row-gap:2px}.kf-quote-picker__result-fields{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0;flex:1}.kf-quote-picker__result:hover{border-color:var(--kf-color-border)}.kf-quote-picker__result--active{border-color:var(--kf-color-primary);background:var(--kf-color-primary-soft)}.kf-quote-picker__result-field{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__result-field--title{flex:none;min-width:92px;font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__result-field--primary{flex:1;min-width:0}@media(max-width:900px){.kf-quote-picker__result-fields{flex-basis:100%;flex-wrap:wrap;row-gap:2px;padding-left:calc(36px + var(--kf-space-sm))}.kf-quote-picker__result-field--primary{flex:none;min-width:0;max-width:100%}}.kf-quote-picker__fade{position:absolute;left:0;right:0;bottom:0;height:36px;background:linear-gradient(to top,var(--kf-color-surface),transparent);pointer-events:none}.kf-quote-picker__see-all{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:var(--kf-space-sm);width:100%;margin-top:var(--kf-space-sm);padding:var(--kf-space-sm);border:1.5px dashed var(--kf-color-primary);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-primary);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer}.kf-quote-picker__see-all:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__empty{grid-column:1 / -1;padding:var(--kf-space-md);text-align:center;color:var(--kf-color-text-muted);font-size:.85rem;margin:0}.kf-quote-picker__trigger--compact{position:relative;height:var(--kf-quote-picker-trigger-height, 48px);min-width:0;gap:var(--kf-space-sm);padding:0 var(--kf-space-sm) 0 var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:none;transition:border-color .14s,box-shadow .14s}.kf-quote-picker__trigger--compact:hover{border-color:var(--kf-color-border-strong);box-shadow:none}.kf-quote-picker__trigger--compact:focus-visible{border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__compact{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:2px;line-height:1.25}.kf-quote-picker__compact-line{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0}.kf-quote-picker__compact-id{flex:none;font-family:var(--kf-font-mono);font-size:var(--kf-font-size-body-sm);font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__compact-status{display:flex;min-width:0;overflow:hidden}.kf-quote-picker__compact-primary{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--kf-font-size-body-sm);color:var(--kf-color-text)}.kf-quote-picker__trigger--loading{display:flex;align-items:center;cursor:progress}.kf-quote-picker__trigger--compact .kf-quote-picker__chevron{flex:none;color:var(--kf-color-text)}@media(prefers-reduced-motion:reduce){.kf-quote-picker__trigger--compact{transition:none}}.kf-quote-picker__count{margin:0;font-size:var(--kf-font-size-body-sm);font-variant-numeric:tabular-nums;color:var(--kf-color-text)}.kf-quote-picker__count:empty{display:none}.kf-quote-picker__filters{container:kf-quote-picker-filters / inline-size;display:flex;flex-direction:column;gap:var(--kf-space-sm)}.kf-quote-picker__filters-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--kf-space-sm)}.kf-quote-picker__modal-search{flex:1 1 240px;min-width:240px}.kf-quote-picker__sort{flex:none}@container kf-quote-picker-filters (max-width: 520px){.kf-quote-picker__modal-search{flex-basis:100%;min-width:0}.kf-quote-picker__sort{width:100%}}.kf-quote-picker__list-wrap{container:kf-quote-picker-list / inline-size}.kf-quote-picker__list{display:grid;column-gap:var(--kf-space-md);row-gap:2px;margin:0;padding:0 0 2px;list-style:none}.kf-quote-picker__list-head,.kf-quote-picker__list-item{display:grid;grid-column:1 / -1;grid-template-columns:subgrid}.kf-quote-picker__list-head{position:sticky;top:0;z-index:1;align-items:end;padding:var(--kf-space-xs) var(--kf-space-md) var(--kf-space-sm);border-bottom:1px solid var(--kf-color-border);background:var(--kf-color-white)}.kf-quote-picker__row{grid-column:1 / -1;display:grid;grid-template-columns:subgrid;align-items:center;box-sizing:border-box;width:100%;min-height:52px;padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:var(--kf-font-size-body-sm);text-align:start;cursor:pointer;scroll-margin-top:48px;scroll-margin-bottom:var(--kf-space-sm)}.kf-quote-picker__row:hover{border-color:var(--kf-color-border);background:var(--kf-color-surface)}.kf-quote-picker__row:focus-visible{outline:2px solid var(--kf-color-primary);outline-offset:-2px}.kf-quote-picker__row--current,.kf-quote-picker__row--current:hover{border-color:var(--kf-color-primary);background:var(--kf-color-primary-soft)}.kf-quote-picker__row-head,.kf-quote-picker__row-body{display:contents}.kf-quote-picker__head-cell{min-width:0;white-space:nowrap}.kf-quote-picker__cell--title{order:0;white-space:nowrap}.kf-quote-picker__row-title{font-family:var(--kf-font-mono);font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__cell--primary{order:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kf-quote-picker__row-primary{color:var(--kf-color-ink)}.kf-quote-picker__cell--field{order:2;min-width:0;white-space:nowrap;font-variant-numeric:tabular-nums}.kf-quote-picker__cell--truncate{overflow:hidden;text-overflow:ellipsis}.kf-quote-picker__cell--end{justify-self:end;text-align:end}.kf-quote-picker__cell--center{justify-self:center;text-align:center}.kf-quote-picker__row-field--right{font-weight:600;color:var(--kf-color-ink)}.kf-quote-picker__cell--status{order:3;display:flex;min-width:0;overflow:hidden}.kf-quote-picker__cell--check{order:4;display:flex;justify-content:center;color:var(--kf-color-primary)}@container kf-quote-picker-list (max-width: 560px){.kf-quote-picker__list{display:flex;flex-direction:column}.kf-quote-picker__list-head{display:none}.kf-quote-picker__list-item{display:block}.kf-quote-picker__row{grid-template-columns:minmax(0,1fr) 1.25rem;grid-template-areas:\"head check\" \"body check\";column-gap:var(--kf-space-sm);row-gap:var(--kf-space-xs);padding:var(--kf-space-sm);scroll-margin-top:var(--kf-space-sm)}.kf-quote-picker__row-head,.kf-quote-picker__row-body{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0}.kf-quote-picker__row-head{grid-area:head;justify-content:space-between}.kf-quote-picker__row-body{grid-area:body;flex-wrap:wrap;row-gap:var(--kf-space-xs)}.kf-quote-picker__row-body .kf-quote-picker__cell--end{margin-left:auto}.kf-quote-picker__row-check{grid-area:check}.kf-quote-picker__cell--status{flex:0 1 auto;justify-content:flex-end;max-width:65%}.kf-quote-picker__cell--primary{flex:1 1 100%;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;white-space:normal;overflow-wrap:anywhere}.kf-quote-picker__cell--field{flex:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Button, selector: "kf-button", inputs: ["variant", "size", "shape", "disabled", "type", "icon", "block", "loading", "ariaLabel", "ariaExpanded", "ariaPressed", "ariaHaspopup"] }, { kind: "component", type: ContentState, selector: "kf-content-state", inputs: ["state", "load", "title", "subtitle", "loadingLabel"] }, { kind: "component", type: FilterChips, selector: "kf-filter-chips", inputs: ["options", "value", "ariaLabel", "layout", "deselectable"], outputs: ["valueChange"] }, { kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size", "rotate", "filled"] }, { kind: "component", type: Input, selector: "kf-input", inputs: ["label", "required", "icon", "suffix", "hint", "invalid", "errorMessage", "disabled", "readonly", "type", "placeholder", "autocomplete", "name", "inputmode", "toggleable", "ariaLabel", "clearable", "clearLabel"], outputs: ["cleared"] }, { kind: "component", type: Modal, selector: "kf-modal", inputs: ["title", "open", "dismissible", "width", "maxHeight", "fullscreenOnMobile", "initialFocus"], outputs: ["openChange"] }, { kind: "component", type: DataTable, selector: "kf-data-table", inputs: ["columns", "rows", "loading", "rowKey", "flush", "cardable", "defaultView", "showViewToggle", "cardMinWidth", "render", "cellTemplates", "density", "rowClickable", "ariaLabel", "currentRowKey", "pageSize", "pageSizeOptions", "page", "keepPageOnRowsChange", "itemLabel", "sort", "view"], outputs: ["rowClick", "pageChange", "pageSizeChange", "sortChange", "viewChange"] }, { kind: "component", type: EmptyState, selector: "kf-empty-state", inputs: ["icon", "tone", "variant", "title"] }, { kind: "component", type: Pagination, selector: "kf-pagination", inputs: ["page", "pages", "pageSizeOptions", "pageSize"], outputs: ["pageChange", "pageSizeChange"] }, { kind: "component", type: Select, selector: "kf-select", inputs: ["label", "options", "value", "placeholder", "disabled", "invalid", "errorMessage", "variant", "width", "searchable", "searchThreshold", "ariaLabel", "icon", "panelTitle", "panelWidth"], outputs: ["valueChange"] }, { kind: "component", type: Skeleton, selector: "kf-skeleton", inputs: ["variant", "width", "height"] }, { kind: "component", type: Text, selector: "kf-text", inputs: ["as", "variant", "tone"] }] });
347
584
  }
348
585
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: QuotePicker, decorators: [{
349
586
  type: Component,
350
- args: [{ selector: 'kf-quote-picker', imports: [NgTemplateOutlet, Icon, Modal, DataTable, EmptyState, Pagination], template: "@if (variant() === 'inline') {\n <div class=\"kf-quote-picker__inline\">\n <!-- Trigger colapsado (default): ancho completo, nunca una caja angosta. -->\n <button\n type=\"button\"\n class=\"kf-quote-picker__trigger\"\n [attr.aria-expanded]=\"expanded()\"\n (click)=\"toggleExpandedPanel()\"\n >\n <span class=\"kf-quote-picker__thumb\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: selectedItem(), index: 0 }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"md\" />\n }\n </span>\n @if (selectedItem(); as item) {\n <span class=\"kf-quote-picker__trigger-summary\">\n @if (titleColumn(); as title) {\n <span class=\"kf-quote-picker__trigger-field kf-quote-picker__trigger-field--title\">{{\n cellValue(item, title)\n }}</span>\n }\n <!--\n En mobile solo se muestra el t\u00EDtulo (ID) \u2014 mostrar los 5 campos\n en una sola l\u00EDnea de ~90px de ancho real corta el texto sin\n elipsis a mitad del segundo campo (bug real medido:\n scrollWidth=519 vs. clientWidth=87 en 390px). El resto de los\n campos siguen disponibles en el panel expandido y en el modal,\n donde s\u00ED hay espacio horizontal.\n -->\n @if (!isMobile()) {\n @for (col of fieldColumns(); track col.key) {\n <span class=\"kf-quote-picker__trigger-sep\" aria-hidden=\"true\">\u00B7</span>\n <span class=\"kf-quote-picker__trigger-field\">{{ cellValue(item, col) }}</span>\n }\n }\n </span>\n } @else {\n <span class=\"kf-quote-picker__trigger-summary kf-quote-picker__trigger-summary--empty\"\n >Sin selecci\u00F3n</span\n >\n }\n <span class=\"kf-quote-picker__trigger-action\">\n {{ expanded() ? 'Cerrar' : title() }}\n <kf-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"kf-quote-picker__chevron\"\n [class.kf-quote-picker__chevron--open]=\"expanded()\"\n />\n </span>\n </button>\n\n @if (expanded()) {\n <div class=\"kf-quote-picker__panel\">\n <div class=\"kf-quote-picker__toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n\n <div class=\"kf-quote-picker__results-wrap\">\n <div #previewScroll class=\"kf-quote-picker__results\" role=\"listbox\">\n @for (item of previewItems(); track trackItem(item, $index)) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__result\"\n [class.kf-quote-picker__result--active]=\"idKey()(item) === selectedId()\"\n role=\"option\"\n [attr.aria-selected]=\"idKey()(item) === selectedId()\"\n (click)=\"select(item)\"\n (keydown)=\"onItemKeydown($event, item)\"\n >\n <span class=\"kf-quote-picker__thumb kf-quote-picker__thumb--sm\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: $index }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"sm\" />\n }\n </span>\n @if (titleColumn(); as title) {\n <span\n class=\"kf-quote-picker__result-field kf-quote-picker__result-field--title\"\n >{{ cellValue(item, title) }}</span\n >\n }\n <!--\n Wrapper propio (no los spans sueltos) para los campos\n secundarios: en mobile necesita saltar de l\u00EDnea COMPLETO\n (mismo criterio que \"unidades\" no debe competir con\n \"modelo\" en la primera l\u00EDnea) \u2014 `flex-basis: 100%` en un\n solo elemento fuerza ese salto sin tener que asumir cu\u00E1l\n de los N campos gen\u00E9ricos es el m\u00E1s corto o el m\u00E1s largo.\n -->\n <span class=\"kf-quote-picker__result-fields\">\n @for (col of fieldColumns(); track col.key; let first = $first) {\n <span\n class=\"kf-quote-picker__result-field\"\n [class.kf-quote-picker__result-field--primary]=\"first\"\n >{{ cellValue(item, col) }}</span\n >\n }\n </span>\n </button>\n } @empty {\n <p class=\"kf-quote-picker__empty\">Sin resultados para esta b\u00FAsqueda.</p>\n }\n </div>\n @if (canFadeBottom()) {\n <div class=\"kf-quote-picker__fade\" aria-hidden=\"true\"></div>\n }\n </div>\n\n @if (isHighVolume()) {\n <button type=\"button\" class=\"kf-quote-picker__see-all\" (click)=\"onSeeAll()\">\n Ver las {{ sortedItems().length }} en pantalla completa\n </button>\n }\n </div>\n }\n </div>\n} @else {\n <kf-modal [(open)]=\"open\" [title]=\"title()\" [dismissible]=\"true\">\n <div class=\"kf-quote-picker__modal-toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n <div class=\"kf-quote-picker__modal-filters\">\n <div class=\"kf-quote-picker__modal-chips\">\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n @if (sortOptions().length > 0) {\n <div class=\"kf-quote-picker__sort\">\n <button\n type=\"button\"\n class=\"kf-quote-picker__sort-trigger\"\n [attr.aria-expanded]=\"sortOpen()\"\n (click)=\"toggleSort()\"\n >\n <kf-icon name=\"sort\" size=\"sm\" />\n {{ sortLabel() }}\n <kf-icon name=\"chevron-down\" size=\"sm\" />\n </button>\n @if (sortOpen()) {\n <div class=\"kf-quote-picker__sort-panel\">\n @for (option of sortOptions(); track option.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__sort-option\"\n [class.kf-quote-picker__sort-option--active]=\"sort() === option.value\"\n (click)=\"setSort(option.value)\"\n >\n {{ option.label }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <kf-data-table\n [columns]=\"columns()\"\n [rows]=\"pagedItems()\"\n [cardable]=\"true\"\n [defaultView]=\"isMobile() ? 'cards' : 'table'\"\n [cellTemplates]=\"cellTemplates()\"\n [rowKey]=\"rowKeyFn\"\n (rowClick)=\"onTableRowClick($event)\"\n >\n <kf-empty-state empty icon=\"file\">Sin resultados para esta b\u00FAsqueda.</kf-empty-state>\n @if (sortedItems().length > 0) {\n <kf-pagination footer [page]=\"page()\" [pages]=\"pages()\" (pageChange)=\"page.set($event)\">\n {{ sortedItems().length }} resultados\n </kf-pagination>\n }\n </kf-data-table>\n </kf-modal>\n}\n", styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-quote-picker__trigger{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-md);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-primary);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-sm);cursor:pointer;font:inherit;text-align:left}.kf-quote-picker__trigger:hover{box-shadow:var(--kf-shadow-md)}.kf-quote-picker__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__thumb{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:none;border-radius:var(--kf-radius-sm);background:var(--kf-color-surface);color:var(--kf-color-gray-400)}.kf-quote-picker__thumb--sm{width:36px;height:36px}.kf-quote-picker__result--active .kf-quote-picker__thumb{background:var(--kf-color-white);color:var(--kf-color-primary)}.kf-quote-picker__trigger-summary{flex:1;min-width:0;display:flex;align-items:baseline;gap:var(--kf-space-xs);overflow:hidden;white-space:nowrap;color:var(--kf-color-ink)}.kf-quote-picker__trigger-summary--empty{color:var(--kf-color-text-muted)}.kf-quote-picker__trigger-field--title{font-family:var(--kf-font-title);font-weight:700;font-size:1.05rem;color:var(--kf-color-ink)}.kf-quote-picker__trigger-field{font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__trigger-sep{color:var(--kf-color-border-strong)}.kf-quote-picker__trigger-action{flex:none;display:flex;align-items:center;gap:6px;color:var(--kf-color-primary);font-size:.85rem;font-weight:600}.kf-quote-picker__chevron{transition:transform .14s}.kf-quote-picker__chevron--open{transform:rotate(180deg)}.kf-quote-picker__panel{display:flex;flex-direction:column;gap:var(--kf-space-md);margin-top:var(--kf-space-sm);padding:var(--kf-space-md) var(--kf-space-lg);background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md)}.kf-quote-picker__toolbar{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}.kf-quote-picker__search{flex:1;min-width:200px;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);color:var(--kf-color-text-muted)}.kf-quote-picker__search:focus-within{border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__search-input{flex:1;border:none;outline:none;background:transparent;font:inherit;color:var(--kf-color-ink)}.kf-quote-picker__search-input::placeholder{color:var(--kf-color-text-muted)}.kf-quote-picker__chip{flex:none;padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__chip--active{background:var(--kf-color-primary);border-color:var(--kf-color-primary);color:var(--kf-color-white)}.kf-quote-picker__results-wrap{position:relative}.kf-quote-picker__results{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--kf-space-sm);max-height:340px;overflow-y:auto}@media(max-width:900px){.kf-quote-picker__results{grid-template-columns:1fr}}.kf-quote-picker__result{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);background:var(--kf-color-white);cursor:pointer;text-align:left;font:inherit;min-width:0;flex-wrap:wrap;row-gap:2px}.kf-quote-picker__result-fields{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0;flex:1}.kf-quote-picker__result:hover{border-color:var(--kf-color-border)}.kf-quote-picker__result--active{border-color:var(--kf-color-primary);background:var(--kf-color-primary-soft)}.kf-quote-picker__result-field{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__result-field--title{flex:none;min-width:92px;font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__result-field--primary{flex:1;min-width:0}@media(max-width:900px){.kf-quote-picker__result-fields{flex-basis:100%;flex-wrap:wrap;row-gap:2px;padding-left:calc(36px + var(--kf-space-sm))}.kf-quote-picker__result-field--primary{flex:none;min-width:0;max-width:100%}}.kf-quote-picker__fade{position:absolute;left:0;right:0;bottom:0;height:36px;background:linear-gradient(to top,var(--kf-color-surface),transparent);pointer-events:none}.kf-quote-picker__see-all{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:var(--kf-space-sm);width:100%;margin-top:var(--kf-space-sm);padding:var(--kf-space-sm);border:1.5px dashed var(--kf-color-primary);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-primary);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer}.kf-quote-picker__see-all:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__empty{grid-column:1 / -1;padding:var(--kf-space-md);text-align:center;color:var(--kf-color-text-muted);font-size:.85rem;margin:0}.kf-quote-picker__modal-toolbar{display:flex;flex-direction:column;gap:var(--kf-space-sm);margin-bottom:var(--kf-space-md)}.kf-quote-picker__modal-filters{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);flex-wrap:wrap}.kf-quote-picker__modal-chips{display:flex;gap:var(--kf-space-sm);flex-wrap:wrap}.kf-quote-picker__sort{position:relative;flex:none}.kf-quote-picker__sort-trigger{display:flex;align-items:center;gap:var(--kf-space-sm);padding:6px var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__sort-trigger:hover{border-color:var(--kf-color-border-strong)}.kf-quote-picker__sort-panel{position:absolute;top:calc(100% + 4px);right:0;z-index:1;min-width:180px;padding:var(--kf-space-xs) 0;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-md)}.kf-quote-picker__sort-option{display:block;width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;background:transparent;color:var(--kf-color-text);font:inherit;font-size:.85rem;text-align:left;cursor:pointer}.kf-quote-picker__sort-option:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__sort-option--active{color:var(--kf-color-primary);font-weight:600}\n"] }]
351
- }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], selectedId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedId", required: false }] }], idKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "idKey", required: true }] }], cellTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplates", required: false }] }], chips: [{ type: i0.Input, args: [{ isSignal: true, alias: "chips", required: false }] }], sortOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortOptions", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }, { type: i0.Output, args: ["sortChange"] }], expandThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandThreshold", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], placeholderIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderIcon", required: false }] }], selectedIdChange: [{ type: i0.Output, args: ["selectedIdChange"] }], expandToModal: [{ type: i0.Output, args: ["expandToModal"] }], previewScrollEl: [{ type: i0.ViewChild, args: ['previewScroll', { isSignal: true }] }] } });
587
+ args: [{ selector: 'kf-quote-picker', imports: [
588
+ NgTemplateOutlet,
589
+ FormsModule,
590
+ Button,
591
+ ContentState,
592
+ FilterChips,
593
+ Icon,
594
+ Input,
595
+ Modal,
596
+ DataTable,
597
+ EmptyState,
598
+ Pagination,
599
+ Select,
600
+ Skeleton,
601
+ Text,
602
+ ], template: "@if (variant() === 'inline') {\n <div class=\"kf-quote-picker__inline\">\n <!-- Trigger colapsado. `default`: tarjeta a ancho completo con miniatura\n (hist\u00F3rico). `compact`: control de barra de herramientas de alto fijo,\n sin miniatura. Con `inlinePanel` en false abre directo el modal. -->\n @if (triggerSize() === 'compact' && loading()) {\n <!-- Mismo alto que el trigger real: la barra no salta al llegar los datos. -->\n <div\n class=\"kf-quote-picker__trigger kf-quote-picker__trigger--compact kf-quote-picker__trigger--loading\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <span class=\"kf-quote-picker__sr-only\">{{ loadingLabel() }}</span>\n <span class=\"kf-quote-picker__compact\" aria-hidden=\"true\">\n <kf-skeleton width=\"35%\" height=\"12px\" />\n <kf-skeleton width=\"65%\" height=\"12px\" />\n </span>\n </div>\n } @else {\n <button\n type=\"button\"\n class=\"kf-quote-picker__trigger\"\n [class.kf-quote-picker__trigger--compact]=\"triggerSize() === 'compact'\"\n [attr.aria-expanded]=\"inlinePanel() ? expanded() : open()\"\n [attr.aria-haspopup]=\"inlinePanel() ? null : 'dialog'\"\n (click)=\"onTriggerClick()\"\n >\n @if (triggerSize() === 'compact') {\n <!-- Nombre accesible = texto visible con un prefijo y comas solo\n para lectores de pantalla: \"Cambiar cotizaci\u00F3n. Actual: <id>,\n <estado>, <modelo>\". -->\n <span class=\"kf-quote-picker__sr-only\">{{ title() }}. Actual: </span>\n @if (selectedItem(); as item) {\n <span class=\"kf-quote-picker__compact\">\n <span class=\"kf-quote-picker__compact-line\">\n @if (titleColumn(); as titleCol) {\n <span class=\"kf-quote-picker__compact-id\">{{ cellValue(item, titleCol) }}</span>\n }\n @for (col of statusColumns(); track col.key) {\n <span class=\"kf-quote-picker__sr-only\">, </span>\n <span class=\"kf-quote-picker__compact-status\">\n <ng-container\n *ngTemplateOutlet=\"cell; context: { $implicit: item, column: col, index: 0 }\"\n />\n </span>\n }\n </span>\n @if (primaryFieldColumn(); as primary) {\n <span class=\"kf-quote-picker__compact-line\">\n <span class=\"kf-quote-picker__sr-only\">, </span>\n <span class=\"kf-quote-picker__compact-primary\">{{\n cellValue(item, primary)\n }}</span>\n </span>\n }\n </span>\n } @else {\n <span class=\"kf-quote-picker__compact kf-quote-picker__trigger-summary--empty\"\n >Sin selecci\u00F3n</span\n >\n }\n <kf-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"kf-quote-picker__chevron\"\n [class.kf-quote-picker__chevron--open]=\"inlinePanel() && expanded()\"\n />\n } @else {\n <span class=\"kf-quote-picker__thumb\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: selectedItem(), index: 0 }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"md\" />\n }\n </span>\n @if (selectedItem(); as item) {\n <span class=\"kf-quote-picker__trigger-summary\">\n @if (titleColumn(); as title) {\n <span\n class=\"kf-quote-picker__trigger-field kf-quote-picker__trigger-field--title\"\n >{{ cellValue(item, title) }}</span\n >\n }\n <!--\n En mobile solo se muestra el t\u00EDtulo (ID) \u2014 mostrar los 5 campos\n en una sola l\u00EDnea de ~90px de ancho real corta el texto sin\n elipsis a mitad del segundo campo (bug real medido:\n scrollWidth=519 vs. clientWidth=87 en 390px). El resto de los\n campos siguen disponibles en el panel expandido y en el modal,\n donde s\u00ED hay espacio horizontal.\n -->\n @if (!isMobile()) {\n @for (col of fieldColumns(); track col.key) {\n <span class=\"kf-quote-picker__trigger-sep\" aria-hidden=\"true\">\u00B7</span>\n <span class=\"kf-quote-picker__trigger-field\">{{ cellValue(item, col) }}</span>\n }\n }\n </span>\n } @else {\n <span class=\"kf-quote-picker__trigger-summary kf-quote-picker__trigger-summary--empty\"\n >Sin selecci\u00F3n</span\n >\n }\n <span class=\"kf-quote-picker__trigger-action\">\n {{ inlinePanel() && expanded() ? 'Cerrar' : title() }}\n <kf-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"kf-quote-picker__chevron\"\n [class.kf-quote-picker__chevron--open]=\"inlinePanel() && expanded()\"\n />\n </span>\n }\n </button>\n }\n\n @if (inlinePanel() && expanded()) {\n <div class=\"kf-quote-picker__panel\">\n <div class=\"kf-quote-picker__toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"resolvedSearchAriaLabel()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n [attr.aria-pressed]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n\n <div class=\"kf-quote-picker__results-wrap\">\n <div #previewScroll class=\"kf-quote-picker__results\" role=\"listbox\">\n @for (item of previewItems(); track trackItem(item, $index)) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__result\"\n [class.kf-quote-picker__result--active]=\"idKey()(item) === selectedId()\"\n role=\"option\"\n [attr.aria-selected]=\"idKey()(item) === selectedId()\"\n (click)=\"select(item)\"\n (keydown)=\"onItemKeydown($event, item)\"\n >\n <span class=\"kf-quote-picker__thumb kf-quote-picker__thumb--sm\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: $index }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"sm\" />\n }\n </span>\n @if (titleColumn(); as title) {\n <span\n class=\"kf-quote-picker__result-field kf-quote-picker__result-field--title\"\n >{{ cellValue(item, title) }}</span\n >\n }\n <!--\n Wrapper propio (no los spans sueltos) para los campos\n secundarios: en mobile necesita saltar de l\u00EDnea COMPLETO\n (mismo criterio que \"unidades\" no debe competir con\n \"modelo\" en la primera l\u00EDnea) \u2014 `flex-basis: 100%` en un\n solo elemento fuerza ese salto sin tener que asumir cu\u00E1l\n de los N campos gen\u00E9ricos es el m\u00E1s corto o el m\u00E1s largo.\n -->\n <span class=\"kf-quote-picker__result-fields\">\n @for (col of fieldColumns(); track col.key; let first = $first) {\n <span\n class=\"kf-quote-picker__result-field\"\n [class.kf-quote-picker__result-field--primary]=\"first\"\n >{{ cellValue(item, col) }}</span\n >\n }\n </span>\n </button>\n } @empty {\n <p class=\"kf-quote-picker__empty\">Sin resultados para esta b\u00FAsqueda.</p>\n }\n </div>\n @if (canFadeBottom()) {\n <div class=\"kf-quote-picker__fade\" aria-hidden=\"true\"></div>\n }\n </div>\n\n @if (isHighVolume()) {\n <button type=\"button\" class=\"kf-quote-picker__see-all\" (click)=\"onSeeAll()\">\n Ver las {{ sortedItems().length }} en pantalla completa\n </button>\n }\n </div>\n }\n </div>\n}\n\n<!-- Modal: la variante `modal` y el que abre el trigger con `inlinePanel` en\n false. Zona de filtros FIJA (slot `kf-modal-toolbar`) y lista\n desplazable (cuerpo del modal). -->\n@if (variant() === 'modal' || !inlinePanel()) {\n <kf-modal\n class=\"kf-quote-picker__modal\"\n [(open)]=\"open\"\n [title]=\"title()\"\n [dismissible]=\"true\"\n [width]=\"modalWidth()\"\n maxHeight=\"85vh\"\n fullscreenOnMobile\n initialFocus=\".kf-quote-picker__modal-search\"\n >\n <p kf-modal-meta class=\"kf-quote-picker__count\" aria-live=\"polite\">{{ resultsLabel() }}</p>\n\n <div kf-modal-toolbar class=\"kf-quote-picker__filters\">\n <div class=\"kf-quote-picker__filters-row\">\n <kf-input\n class=\"kf-quote-picker__modal-search\"\n type=\"search\"\n icon=\"search\"\n clearable\n [placeholder]=\"searchPlaceholder()\"\n [ariaLabel]=\"resolvedSearchAriaLabel()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n />\n @if (sortOptions().length > 0) {\n <kf-select\n class=\"kf-quote-picker__sort\"\n icon=\"sort\"\n ariaLabel=\"Ordenar por\"\n panelTitle=\"Ordenar por\"\n placeholder=\"Ordenar por\"\n [options]=\"sortSelectOptions()\"\n [value]=\"sort() ?? undefined\"\n (valueChange)=\"onSortChange($event)\"\n />\n }\n </div>\n @if (chips().length > 0) {\n <kf-filter-chips\n class=\"kf-quote-picker__chips\"\n deselectable\n [ariaLabel]=\"chipsAriaLabel()\"\n [options]=\"chipOptions()\"\n [value]=\"activeChip() ?? ''\"\n [layout]=\"isNarrowViewport() ? 'scroll' : 'wrap'\"\n (valueChange)=\"onChipChange($event)\"\n />\n }\n </div>\n\n <kf-content-state\n [state]=\"modalState()\"\n load=\"spinner\"\n [loadingLabel]=\"loadingLabel()\"\n [title]=\"error() ?? ''\"\n >\n <kf-button actions type=\"button\" variant=\"outline\" icon=\"refresh\" (click)=\"retry.emit()\">\n Reintentar\n </kf-button>\n @if (modalLayout() === 'list') {\n @if (sortedItems().length > 0) {\n <div class=\"kf-quote-picker__list-wrap\">\n <ul\n #modalList\n class=\"kf-quote-picker__list\"\n [style.grid-template-columns]=\"listGridColumns()\"\n >\n @if (listHeader()) {\n <!-- Cabecera visual de columnas: cada fila (bot\u00F3n) ya se anuncia\n completa, as\u00ED que para lectores de pantalla ser\u00EDa ruido. -->\n <li class=\"kf-quote-picker__list-head\" aria-hidden=\"true\">\n @if (titleColumn(); as titleCol) {\n <span class=\"kf-quote-picker__head-cell kf-quote-picker__cell--title\">\n <kf-text as=\"span\" variant=\"overline\">{{ titleCol.label }}</kf-text>\n </span>\n }\n @if (primaryFieldColumn(); as primary) {\n <span class=\"kf-quote-picker__head-cell kf-quote-picker__cell--primary\">\n <kf-text as=\"span\" variant=\"overline\">{{ primary.label }}</kf-text>\n </span>\n }\n @for (col of restFieldColumns(); track col.key) {\n <span\n class=\"kf-quote-picker__head-cell kf-quote-picker__cell--field\"\n [class.kf-quote-picker__cell--end]=\"isEndAligned(col)\"\n [class.kf-quote-picker__cell--center]=\"col.align === 'center'\"\n >\n <kf-text as=\"span\" variant=\"overline\">{{ col.label }}</kf-text>\n </span>\n }\n @for (col of statusColumns(); track col.key) {\n <span class=\"kf-quote-picker__head-cell kf-quote-picker__cell--status\">\n <kf-text as=\"span\" variant=\"overline\">{{ col.label }}</kf-text>\n </span>\n }\n <span class=\"kf-quote-picker__head-cell kf-quote-picker__cell--check\"></span>\n </li>\n }\n @for (item of sortedItems(); track trackItem(item, $index); let i = $index) {\n <li class=\"kf-quote-picker__list-item\">\n <button\n type=\"button\"\n class=\"kf-quote-picker__row\"\n [class.kf-quote-picker__row--current]=\"isSelected(item)\"\n [attr.aria-current]=\"isSelected(item) ? 'true' : null\"\n (click)=\"select(item)\"\n >\n <span class=\"kf-quote-picker__row-head\">\n @if (titleColumn(); as titleCol) {\n <span class=\"kf-quote-picker__row-title kf-quote-picker__cell--title\">\n <ng-container\n *ngTemplateOutlet=\"\n cell;\n context: { $implicit: item, column: titleCol, index: i }\n \"\n />\n </span>\n }\n @for (col of statusColumns(); track col.key) {\n <span class=\"kf-quote-picker__row-status kf-quote-picker__cell--status\">\n <ng-container\n *ngTemplateOutlet=\"\n cell;\n context: { $implicit: item, column: col, index: i }\n \"\n />\n </span>\n }\n </span>\n <span class=\"kf-quote-picker__row-body\">\n @if (primaryFieldColumn(); as primary) {\n <span\n class=\"kf-quote-picker__row-primary kf-quote-picker__cell--primary\"\n [attr.title]=\"cellTitle(item, primary)\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n cell;\n context: { $implicit: item, column: primary, index: i }\n \"\n />\n </span>\n }\n @for (col of restFieldColumns(); track col.key) {\n <span\n class=\"kf-quote-picker__row-field kf-quote-picker__cell--field\"\n [class.kf-quote-picker__row-field--right]=\"isEndAligned(col)\"\n [class.kf-quote-picker__cell--end]=\"isEndAligned(col)\"\n [class.kf-quote-picker__cell--center]=\"col.align === 'center'\"\n [class.kf-quote-picker__cell--truncate]=\"col.truncate\"\n [attr.title]=\"col.truncate ? cellTitle(item, col) : null\"\n >\n <ng-container\n *ngTemplateOutlet=\"\n cell;\n context: { $implicit: item, column: col, index: i }\n \"\n />\n </span>\n }\n </span>\n <span class=\"kf-quote-picker__row-check kf-quote-picker__cell--check\">\n @if (isSelected(item)) {\n <kf-icon name=\"check\" size=\"sm\" />\n }\n </span>\n </button>\n </li>\n }\n </ul>\n </div>\n } @else {\n <kf-empty-state icon=\"file\">Sin resultados para esta b\u00FAsqueda.</kf-empty-state>\n }\n } @else {\n <kf-data-table\n [columns]=\"columns()\"\n [rows]=\"pagedItems()\"\n [cardable]=\"true\"\n [defaultView]=\"isMobile() ? 'cards' : 'table'\"\n [cellTemplates]=\"cellTemplates()\"\n [rowKey]=\"rowKeyFn\"\n [currentRowKey]=\"selectedId()\"\n (rowClick)=\"onTableRowClick($event)\"\n >\n <kf-empty-state empty icon=\"file\">Sin resultados para esta b\u00FAsqueda.</kf-empty-state>\n @if (sortedItems().length > 0) {\n <kf-pagination footer [page]=\"page()\" [pages]=\"pages()\" (pageChange)=\"page.set($event)\">\n {{ sortedItems().length }} resultados\n </kf-pagination>\n }\n </kf-data-table>\n }\n </kf-content-state>\n </kf-modal>\n}\n\n<!-- Celda de la lista compacta: el template del consumidor si existe (badge,\n monto formateado), si no el valor como texto. -->\n<ng-template #cell let-item let-column=\"column\" let-index=\"index\">\n @if (cellTemplates()?.[column.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: index }\"\n />\n } @else {\n {{ cellValue(item, column) }}\n }\n</ng-template>\n", styles: [":host{display:block;min-width:0;font-family:var(--kf-font-body)}.kf-quote-picker__sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.kf-quote-picker__trigger{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-md);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-primary);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-sm);cursor:pointer;font:inherit;text-align:left}.kf-quote-picker__trigger:hover{box-shadow:var(--kf-shadow-md)}.kf-quote-picker__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__thumb{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:none;border-radius:var(--kf-radius-sm);background:var(--kf-color-surface);color:var(--kf-color-gray-400)}.kf-quote-picker__thumb--sm{width:36px;height:36px}.kf-quote-picker__result--active .kf-quote-picker__thumb{background:var(--kf-color-white);color:var(--kf-color-primary)}.kf-quote-picker__trigger-summary{flex:1;min-width:0;display:flex;align-items:baseline;gap:var(--kf-space-xs);overflow:hidden;white-space:nowrap;color:var(--kf-color-ink)}.kf-quote-picker__trigger-summary--empty{color:var(--kf-color-text-muted)}.kf-quote-picker__trigger-field--title{font-family:var(--kf-font-title);font-weight:700;font-size:1.05rem;color:var(--kf-color-ink)}.kf-quote-picker__trigger-field{font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__trigger-sep{color:var(--kf-color-border-strong)}.kf-quote-picker__trigger-action{flex:none;display:flex;align-items:center;gap:6px;color:var(--kf-color-primary);font-size:.85rem;font-weight:600}.kf-quote-picker__chevron{transition:transform .14s}.kf-quote-picker__chevron--open{transform:rotate(180deg)}.kf-quote-picker__panel{display:flex;flex-direction:column;gap:var(--kf-space-md);margin-top:var(--kf-space-sm);padding:var(--kf-space-md) var(--kf-space-lg);background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md)}.kf-quote-picker__toolbar{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}.kf-quote-picker__search{flex:1;min-width:200px;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);color:var(--kf-color-text-muted)}.kf-quote-picker__search:focus-within{border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__search-input{flex:1;border:none;outline:none;background:transparent;font:inherit;color:var(--kf-color-ink)}.kf-quote-picker__search-input::placeholder{color:var(--kf-color-text-muted)}.kf-quote-picker__chip{flex:none;padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__chip--active{background:var(--kf-color-primary);border-color:var(--kf-color-primary);color:var(--kf-color-white)}.kf-quote-picker__results-wrap{position:relative}.kf-quote-picker__results{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--kf-space-sm);max-height:340px;overflow-y:auto}@media(max-width:900px){.kf-quote-picker__results{grid-template-columns:1fr}}.kf-quote-picker__result{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);background:var(--kf-color-white);cursor:pointer;text-align:left;font:inherit;min-width:0;flex-wrap:wrap;row-gap:2px}.kf-quote-picker__result-fields{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0;flex:1}.kf-quote-picker__result:hover{border-color:var(--kf-color-border)}.kf-quote-picker__result--active{border-color:var(--kf-color-primary);background:var(--kf-color-primary-soft)}.kf-quote-picker__result-field{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__result-field--title{flex:none;min-width:92px;font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__result-field--primary{flex:1;min-width:0}@media(max-width:900px){.kf-quote-picker__result-fields{flex-basis:100%;flex-wrap:wrap;row-gap:2px;padding-left:calc(36px + var(--kf-space-sm))}.kf-quote-picker__result-field--primary{flex:none;min-width:0;max-width:100%}}.kf-quote-picker__fade{position:absolute;left:0;right:0;bottom:0;height:36px;background:linear-gradient(to top,var(--kf-color-surface),transparent);pointer-events:none}.kf-quote-picker__see-all{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:var(--kf-space-sm);width:100%;margin-top:var(--kf-space-sm);padding:var(--kf-space-sm);border:1.5px dashed var(--kf-color-primary);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-primary);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer}.kf-quote-picker__see-all:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__empty{grid-column:1 / -1;padding:var(--kf-space-md);text-align:center;color:var(--kf-color-text-muted);font-size:.85rem;margin:0}.kf-quote-picker__trigger--compact{position:relative;height:var(--kf-quote-picker-trigger-height, 48px);min-width:0;gap:var(--kf-space-sm);padding:0 var(--kf-space-sm) 0 var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:none;transition:border-color .14s,box-shadow .14s}.kf-quote-picker__trigger--compact:hover{border-color:var(--kf-color-border-strong);box-shadow:none}.kf-quote-picker__trigger--compact:focus-visible{border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__compact{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:2px;line-height:1.25}.kf-quote-picker__compact-line{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0}.kf-quote-picker__compact-id{flex:none;font-family:var(--kf-font-mono);font-size:var(--kf-font-size-body-sm);font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__compact-status{display:flex;min-width:0;overflow:hidden}.kf-quote-picker__compact-primary{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--kf-font-size-body-sm);color:var(--kf-color-text)}.kf-quote-picker__trigger--loading{display:flex;align-items:center;cursor:progress}.kf-quote-picker__trigger--compact .kf-quote-picker__chevron{flex:none;color:var(--kf-color-text)}@media(prefers-reduced-motion:reduce){.kf-quote-picker__trigger--compact{transition:none}}.kf-quote-picker__count{margin:0;font-size:var(--kf-font-size-body-sm);font-variant-numeric:tabular-nums;color:var(--kf-color-text)}.kf-quote-picker__count:empty{display:none}.kf-quote-picker__filters{container:kf-quote-picker-filters / inline-size;display:flex;flex-direction:column;gap:var(--kf-space-sm)}.kf-quote-picker__filters-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--kf-space-sm)}.kf-quote-picker__modal-search{flex:1 1 240px;min-width:240px}.kf-quote-picker__sort{flex:none}@container kf-quote-picker-filters (max-width: 520px){.kf-quote-picker__modal-search{flex-basis:100%;min-width:0}.kf-quote-picker__sort{width:100%}}.kf-quote-picker__list-wrap{container:kf-quote-picker-list / inline-size}.kf-quote-picker__list{display:grid;column-gap:var(--kf-space-md);row-gap:2px;margin:0;padding:0 0 2px;list-style:none}.kf-quote-picker__list-head,.kf-quote-picker__list-item{display:grid;grid-column:1 / -1;grid-template-columns:subgrid}.kf-quote-picker__list-head{position:sticky;top:0;z-index:1;align-items:end;padding:var(--kf-space-xs) var(--kf-space-md) var(--kf-space-sm);border-bottom:1px solid var(--kf-color-border);background:var(--kf-color-white)}.kf-quote-picker__row{grid-column:1 / -1;display:grid;grid-template-columns:subgrid;align-items:center;box-sizing:border-box;width:100%;min-height:52px;padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:var(--kf-font-size-body-sm);text-align:start;cursor:pointer;scroll-margin-top:48px;scroll-margin-bottom:var(--kf-space-sm)}.kf-quote-picker__row:hover{border-color:var(--kf-color-border);background:var(--kf-color-surface)}.kf-quote-picker__row:focus-visible{outline:2px solid var(--kf-color-primary);outline-offset:-2px}.kf-quote-picker__row--current,.kf-quote-picker__row--current:hover{border-color:var(--kf-color-primary);background:var(--kf-color-primary-soft)}.kf-quote-picker__row-head,.kf-quote-picker__row-body{display:contents}.kf-quote-picker__head-cell{min-width:0;white-space:nowrap}.kf-quote-picker__cell--title{order:0;white-space:nowrap}.kf-quote-picker__row-title{font-family:var(--kf-font-mono);font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__cell--primary{order:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kf-quote-picker__row-primary{color:var(--kf-color-ink)}.kf-quote-picker__cell--field{order:2;min-width:0;white-space:nowrap;font-variant-numeric:tabular-nums}.kf-quote-picker__cell--truncate{overflow:hidden;text-overflow:ellipsis}.kf-quote-picker__cell--end{justify-self:end;text-align:end}.kf-quote-picker__cell--center{justify-self:center;text-align:center}.kf-quote-picker__row-field--right{font-weight:600;color:var(--kf-color-ink)}.kf-quote-picker__cell--status{order:3;display:flex;min-width:0;overflow:hidden}.kf-quote-picker__cell--check{order:4;display:flex;justify-content:center;color:var(--kf-color-primary)}@container kf-quote-picker-list (max-width: 560px){.kf-quote-picker__list{display:flex;flex-direction:column}.kf-quote-picker__list-head{display:none}.kf-quote-picker__list-item{display:block}.kf-quote-picker__row{grid-template-columns:minmax(0,1fr) 1.25rem;grid-template-areas:\"head check\" \"body check\";column-gap:var(--kf-space-sm);row-gap:var(--kf-space-xs);padding:var(--kf-space-sm);scroll-margin-top:var(--kf-space-sm)}.kf-quote-picker__row-head,.kf-quote-picker__row-body{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0}.kf-quote-picker__row-head{grid-area:head;justify-content:space-between}.kf-quote-picker__row-body{grid-area:body;flex-wrap:wrap;row-gap:var(--kf-space-xs)}.kf-quote-picker__row-body .kf-quote-picker__cell--end{margin-left:auto}.kf-quote-picker__row-check{grid-area:check}.kf-quote-picker__cell--status{flex:0 1 auto;justify-content:flex-end;max-width:65%}.kf-quote-picker__cell--primary{flex:1 1 100%;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;white-space:normal;overflow-wrap:anywhere}.kf-quote-picker__cell--field{flex:none}}\n"] }]
603
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], selectedId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedId", required: false }] }], idKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "idKey", required: true }] }], cellTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplates", required: false }] }], chips: [{ type: i0.Input, args: [{ isSignal: true, alias: "chips", required: false }] }], sortOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortOptions", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }, { type: i0.Output, args: ["sortChange"] }], expandThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandThreshold", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], placeholderIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderIcon", required: false }] }], modalLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "modalLayout", required: false }] }], searchAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchAriaLabel", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingLabel", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], triggerSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerSize", required: false }] }], inlinePanel: [{ type: i0.Input, args: [{ isSignal: true, alias: "inlinePanel", required: false }] }], modalWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "modalWidth", required: false }] }], itemLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemLabels", required: false }] }], allChipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "allChipLabel", required: false }] }], chipsAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "chipsAriaLabel", required: false }] }], listHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "listHeader", required: false }] }], selectedIdChange: [{ type: i0.Output, args: ["selectedIdChange"] }], retry: [{ type: i0.Output, args: ["retry"] }], expandToModal: [{ type: i0.Output, args: ["expandToModal"] }], previewScrollEl: [{ type: i0.ViewChild, args: ['previewScroll', { isSignal: true }] }], modalListEl: [{ type: i0.ViewChild, args: ['modalList', { isSignal: true }] }] } });
352
604
 
353
605
  /**
354
606
  * Estado vacío de reservas en taller con llamada a agendar (página 36):