@kaufmann-lab/ui 0.1.20 → 0.1.22
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.
- package/fesm2022/kaufmann-lab-ui-business.mjs +257 -17
- package/fesm2022/kaufmann-lab-ui-business.mjs.map +1 -1
- package/fesm2022/kaufmann-lab-ui-core.mjs +3452 -551
- package/fesm2022/kaufmann-lab-ui-core.mjs.map +1 -1
- package/fesm2022/kaufmann-lab-ui-map.mjs +1 -1
- package/fesm2022/kaufmann-lab-ui-map.mjs.map +1 -1
- package/package.json +1 -1
- package/types/kaufmann-lab-ui-business.d.ts +150 -13
- package/types/kaufmann-lab-ui-core.d.ts +1738 -164
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, output, Component, model, signal, inject, DestroyRef, viewChild, computed,
|
|
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
|
/**
|
|
@@ -45,7 +47,7 @@ class CompanySelector {
|
|
|
45
47
|
...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
|
|
46
48
|
valueChange = output();
|
|
47
49
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CompanySelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
48
|
-
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"] }] });
|
|
49
51
|
}
|
|
50
52
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CompanySelector, decorators: [{
|
|
51
53
|
type: Component,
|
|
@@ -95,10 +97,8 @@ const PAGE_SIZE = 10;
|
|
|
95
97
|
* este modo necesita. Compone también `kf-data-table` para la tabla
|
|
96
98
|
* completa (columnas configurables, fade de overflow horizontal ya
|
|
97
99
|
* resuelto ahí) y `kf-pagination` para paginar — ninguno de los tres se
|
|
98
|
-
* reimplementa
|
|
99
|
-
*
|
|
100
|
-
* fuerza un ancho fijo de 240px pensado para formularios, no para una
|
|
101
|
-
* 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`).
|
|
102
102
|
*
|
|
103
103
|
* En mobile, `kf-data-table[cardable]` con `defaultView` forzado a
|
|
104
104
|
* `'cards'` decide la vista (mismo criterio que `matchMediaSignal` ya
|
|
@@ -106,12 +106,70 @@ const PAGE_SIZE = 10;
|
|
|
106
106
|
* no tiene sentido ofrecer un toggle tabla/cards que en mobile daría una
|
|
107
107
|
* tabla angosta con scroll horizontal.
|
|
108
108
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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.
|
|
115
173
|
*
|
|
116
174
|
* <kf-quote-picker
|
|
117
175
|
* [items]="quotes()"
|
|
@@ -170,7 +228,47 @@ class QuotePicker {
|
|
|
170
228
|
* ícono fijo. Default `'file'` solo por no dejar el input sin valor. */
|
|
171
229
|
placeholderIcon = input('file', /* @ts-ignore */
|
|
172
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 });
|
|
173
269
|
selectedIdChange = output();
|
|
270
|
+
/** "Reintentar" del estado de error. */
|
|
271
|
+
retry = output();
|
|
174
272
|
/** Disparado al superar `expandThreshold` en modo inline; el consumidor decide pasar a `modal`. */
|
|
175
273
|
expandToModal = output();
|
|
176
274
|
searchQuery = signal('', /* @ts-ignore */
|
|
@@ -190,9 +288,15 @@ class QuotePicker {
|
|
|
190
288
|
* vs. cards apiladas dentro del modal.
|
|
191
289
|
*/
|
|
192
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)');
|
|
193
293
|
destroyRef = inject(DestroyRef);
|
|
194
294
|
previewScrollEl = viewChild('previewScroll', /* @ts-ignore */
|
|
195
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;
|
|
196
300
|
/** Mismo mecanismo de fade que `kf-data-table` — ver comentario en QuotePicker.css. */
|
|
197
301
|
canFadeBottom = signal(false, /* @ts-ignore */
|
|
198
302
|
...(ngDevMode ? [{ debugName: "canFadeBottom" }] : /* istanbul ignore next */ []));
|
|
@@ -262,12 +366,107 @@ class QuotePicker {
|
|
|
262
366
|
return this.sortedItems().slice(start, start + PAGE_SIZE);
|
|
263
367
|
}, /* @ts-ignore */
|
|
264
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
|
+
}
|
|
265
448
|
sortLabel = computed(() => {
|
|
266
449
|
const current = this.sortOptions().find((option) => option.value === this.sort());
|
|
267
450
|
return current?.label ?? 'Ordenar por';
|
|
268
451
|
}, /* @ts-ignore */
|
|
269
452
|
...(ngDevMode ? [{ debugName: "sortLabel" }] : /* istanbul ignore next */ []));
|
|
270
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
|
+
});
|
|
271
470
|
afterNextRender(() => {
|
|
272
471
|
const el = this.previewScrollEl()?.nativeElement;
|
|
273
472
|
if (!el) {
|
|
@@ -296,14 +495,32 @@ class QuotePicker {
|
|
|
296
495
|
toggleExpandedPanel() {
|
|
297
496
|
this.expanded.set(!this.expanded());
|
|
298
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
|
+
}
|
|
299
507
|
onSearchInput(event) {
|
|
300
|
-
this.
|
|
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 ?? '');
|
|
301
513
|
this.page.set(1);
|
|
302
514
|
}
|
|
303
515
|
selectChip(value) {
|
|
304
516
|
this.activeChip.set(this.activeChip() === value ? null : value);
|
|
305
517
|
this.page.set(1);
|
|
306
518
|
}
|
|
519
|
+
/** Chips del modal: `''` es "Todas" (sin filtro). */
|
|
520
|
+
onChipChange(value) {
|
|
521
|
+
this.activeChip.set(value || null);
|
|
522
|
+
this.page.set(1);
|
|
523
|
+
}
|
|
307
524
|
select(item) {
|
|
308
525
|
const id = this.idKey()(item);
|
|
309
526
|
// El panel se cierra siempre al elegir (haya cambiado el id o no) —
|
|
@@ -312,6 +529,9 @@ class QuotePicker {
|
|
|
312
529
|
// consumidor sería una carga que ningún otro componente de la
|
|
313
530
|
// librería le pide.
|
|
314
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);
|
|
315
535
|
if (id === this.selectedId()) {
|
|
316
536
|
return;
|
|
317
537
|
}
|
|
@@ -329,9 +549,14 @@ class QuotePicker {
|
|
|
329
549
|
onTableRowClick(event) {
|
|
330
550
|
this.select(event.row);
|
|
331
551
|
}
|
|
552
|
+
/** @deprecated El orden de la variante modal ahora es un `kf-select`; este método ya no lo usa la plantilla. */
|
|
332
553
|
toggleSort() {
|
|
333
554
|
this.sortOpen.update((open) => !open);
|
|
334
555
|
}
|
|
556
|
+
onSortChange(value) {
|
|
557
|
+
this.sort.set(value);
|
|
558
|
+
}
|
|
559
|
+
/** Alterna el orden (elegir el vigente lo quita). Se conserva por compatibilidad. */
|
|
335
560
|
setSort(value) {
|
|
336
561
|
this.sort.set(this.sort() === value ? null : value);
|
|
337
562
|
this.sortOpen.set(false);
|
|
@@ -355,12 +580,27 @@ class QuotePicker {
|
|
|
355
580
|
fieldColumns = computed(() => this.columns().filter((c) => !c.cardRole || c.cardRole === 'field'), /* @ts-ignore */
|
|
356
581
|
...(ngDevMode ? [{ debugName: "fieldColumns" }] : /* istanbul ignore next */ []));
|
|
357
582
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: QuotePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
358
|
-
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", "loadingTemplate", "loadingPreset"] }, { 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", "min", "max", "step", "maxlength", "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", "skeletonRows", "rowKey", "flush", "cardable", "defaultView", "showViewToggle", "cardMinWidth", "render", "cellTemplates", "density", "rowClickable", "cellAriaLabel", "ariaLabel", "currentRowKey", "pageSize", "pageSizeOptions", "page", "keepPageOnRowsChange", "itemLabel", "sort", "view"], outputs: ["rowClick", "cellClick", "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"] }] });
|
|
359
584
|
}
|
|
360
585
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: QuotePicker, decorators: [{
|
|
361
586
|
type: Component,
|
|
362
|
-
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"] }]
|
|
363
|
-
|
|
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 }] }] } });
|
|
364
604
|
|
|
365
605
|
/**
|
|
366
606
|
* Estado vacío de reservas en taller con llamada a agendar (página 36):
|