@kaufmann-lab/ui 0.1.17 → 0.1.18
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/README.md +1 -1
- package/fesm2022/kaufmann-lab-ui-auth.mjs +322 -0
- package/fesm2022/kaufmann-lab-ui-auth.mjs.map +1 -0
- package/fesm2022/kaufmann-lab-ui-business.mjs +453 -0
- package/fesm2022/kaufmann-lab-ui-business.mjs.map +1 -0
- package/fesm2022/kaufmann-lab-ui-core.mjs +5906 -0
- package/fesm2022/kaufmann-lab-ui-core.mjs.map +1 -0
- package/fesm2022/kaufmann-lab-ui-map.mjs +1637 -0
- package/fesm2022/kaufmann-lab-ui-map.mjs.map +1 -0
- package/fesm2022/kaufmann-lab-ui-mobile.mjs +92 -0
- package/fesm2022/kaufmann-lab-ui-mobile.mjs.map +1 -0
- package/fesm2022/kaufmann-lab-ui-shared.mjs +269 -0
- package/fesm2022/kaufmann-lab-ui-shared.mjs.map +1 -0
- package/fesm2022/kaufmann-lab-ui.mjs +23 -8472
- package/fesm2022/kaufmann-lab-ui.mjs.map +1 -1
- package/kaufmann-lab-ui-0.1.18.tgz +0 -0
- package/package.json +25 -1
- package/types/kaufmann-lab-ui-auth.d.ts +240 -0
- package/types/kaufmann-lab-ui-business.d.ts +298 -0
- package/types/kaufmann-lab-ui-core.d.ts +3288 -0
- package/types/kaufmann-lab-ui-map.d.ts +631 -0
- package/types/kaufmann-lab-ui-mobile.d.ts +53 -0
- package/types/kaufmann-lab-ui-shared.d.ts +154 -0
- package/types/kaufmann-lab-ui.d.ts +5 -4631
- package/fesm2022/mapbox-gl-csp.worker.js +0 -6
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, output, Component, model, signal, inject, DestroyRef, viewChild, computed, afterNextRender, booleanAttribute } from '@angular/core';
|
|
3
|
+
import { Divider, Select, Text, Icon, Modal, DataTable, EmptyState, Pagination, Link } from '@kaufmann-lab/ui/core';
|
|
4
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
5
|
+
import { matchMediaSignal, normalizeForSearch } from '@kaufmann-lab/ui/shared';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Conmutador de empresa activa con metadatos clave (página 32) — encabeza
|
|
9
|
+
* la pantalla de inicio del portal. Compone `kf-select` para el propio
|
|
10
|
+
* selector (mismo panel propio, sin `<select>` nativo) y proyecta hasta 3
|
|
11
|
+
* pares label/valor (RUC, Identificación, Sucursal en el ejemplo del PDF).
|
|
12
|
+
*
|
|
13
|
+
* <kf-company-selector
|
|
14
|
+
* label="Selecciona empresa"
|
|
15
|
+
* [companies]="['TRANSPORTES DEL SUR S.A.C.', 'ANDES MAQ LTDA.']"
|
|
16
|
+
* [value]="activeCompany()"
|
|
17
|
+
* (valueChange)="activeCompany.set($event)"
|
|
18
|
+
* [meta]="[
|
|
19
|
+
* { label: 'RUC', value: '20448712359' },
|
|
20
|
+
* { label: 'Identificación', value: '0004002232' },
|
|
21
|
+
* { label: 'Sucursal', value: 'Sucursal Norte' },
|
|
22
|
+
* ]"
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
class CompanySelector {
|
|
26
|
+
label = input('Selecciona empresa', /* @ts-ignore */
|
|
27
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
28
|
+
companies = input.required(/* @ts-ignore */
|
|
29
|
+
...(ngDevMode ? [{ debugName: "companies" }] : /* istanbul ignore next */ []));
|
|
30
|
+
value = input(/* @ts-ignore */
|
|
31
|
+
...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
32
|
+
meta = input([], /* @ts-ignore */
|
|
33
|
+
...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
|
|
34
|
+
valueChange = output();
|
|
35
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CompanySelector, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
36
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: CompanySelector, isStandalone: true, selector: "kf-company-selector", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, companies: { classPropertyName: "companies", publicName: "companies", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<kf-select\n class=\"kf-company-selector__select\"\n [label]=\"label()\"\n [options]=\"companies()\"\n [value]=\"value()\"\n (valueChange)=\"valueChange.emit($event)\"\n/>\n@if (meta().length) {\n <div class=\"kf-company-selector__meta\">\n @for (item of meta(); track item.label; let last = $last) {\n <div class=\"kf-company-selector__meta-item\">\n <kf-text as=\"span\" variant=\"overline\">{{ item.label }}</kf-text>\n <span class=\"kf-company-selector__meta-value\">{{ item.value }}</span>\n </div>\n @if (!last) {\n <kf-divider orientation=\"vertical\" />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;align-items:center;gap:var(--kf-space-lg);flex-wrap:wrap;padding:var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);background:var(--kf-color-white);font-family:var(--kf-font-body)}.kf-company-selector__select{flex:1 1 260px;min-width:0}.kf-company-selector__meta{display:flex;gap:var(--kf-space-lg);flex-wrap:wrap}.kf-company-selector__meta-item{display:flex;flex-direction:column;gap:2px}.kf-company-selector__meta-value{font-family:var(--kf-font-mono);font-size:.875rem;color:var(--kf-color-ink)}\n"], dependencies: [{ kind: "component", type: Divider, selector: "kf-divider", inputs: ["orientation"] }, { kind: "component", type: Select, selector: "kf-select", inputs: ["label", "options", "value", "placeholder", "disabled", "invalid", "errorMessage", "variant", "width", "searchable", "searchThreshold"], outputs: ["valueChange"] }, { kind: "component", type: Text, selector: "kf-text", inputs: ["as", "variant", "tone"] }] });
|
|
37
|
+
}
|
|
38
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CompanySelector, decorators: [{
|
|
39
|
+
type: Component,
|
|
40
|
+
args: [{ selector: 'kf-company-selector', imports: [Divider, Select, Text], 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"] }]
|
|
41
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], companies: [{ type: i0.Input, args: [{ isSignal: true, alias: "companies", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
|
|
42
|
+
|
|
43
|
+
/** Filas visibles en el preview acotado (modo `inline`, volumen alto) antes del fade + CTA. */
|
|
44
|
+
const PREVIEW_ROWS = 5;
|
|
45
|
+
/** Filas por página en la tabla del modo `modal`. */
|
|
46
|
+
const PAGE_SIZE = 10;
|
|
47
|
+
/**
|
|
48
|
+
* Selector de un ítem "rico" de una lista buscable/filtrable — generalización
|
|
49
|
+
* del patrón "elegir una cotización" de Pagos y Facturas de OTD, pero sin
|
|
50
|
+
* ningún acoplamiento a `Quote`: todo el contenido de cada fila/card llega
|
|
51
|
+
* por `columns` (mismo `DataTableColumn` de `kf-data-table`, reexportado acá
|
|
52
|
+
* en vez de duplicar el concepto) + `cellTemplates` para contenido custom
|
|
53
|
+
* (thumbnail, monto formateado, etc. — mismo mecanismo `TemplateRef` +
|
|
54
|
+
* `ngTemplateOutlet` que ya usa `kf-data-table`).
|
|
55
|
+
*
|
|
56
|
+
* Un solo componente, dos contenedores (`variant: 'inline' | 'modal'`), UNA
|
|
57
|
+
* sola lógica de búsqueda/filtro/orden interna. Nunca dos implementaciones
|
|
58
|
+
* paralelas: el riesgo real de eso es que un fix de filtro se aplique en un
|
|
59
|
+
* modo y se olvide en el otro. Los `computed()` de más abajo
|
|
60
|
+
* (`filteredItems`, `sortedItems`) son la ÚNICA fuente de verdad para ambos
|
|
61
|
+
* `variant`; lo que cambia entre inline y modal es solo qué contenedor
|
|
62
|
+
* envuelve esos datos ya resueltos.
|
|
63
|
+
*
|
|
64
|
+
* ## Modo `inline` (default)
|
|
65
|
+
* Trigger a ANCHO COMPLETO (nunca una caja angosta de ancho fijo) que
|
|
66
|
+
* muestra el ítem activo colapsado — thumbnail, id + modelo, unidades +
|
|
67
|
+
* fecha + monto, CTA "Cambiar cotización" con chevron-down. Al click, el
|
|
68
|
+
* panel se expande INLINE (empuja el contenido de abajo, nunca flota
|
|
69
|
+
* encima) con buscador propio + chips de filtro + un grid de 2 columnas (1
|
|
70
|
+
* en mobile, breakpoint `max-width: 900px` vía `matchMediaSignal` — mismo
|
|
71
|
+
* mecanismo que ya usa el resto de la librería para decisiones de layout en
|
|
72
|
+
* TypeScript, no solo CSS). Por encima de `expandThreshold` resultados
|
|
73
|
+
* visibles, el panel recorta a un preview de `PREVIEW_ROWS` filas con un
|
|
74
|
+
* fade inferior y un botón dashed "Ver las N en pantalla completa" que
|
|
75
|
+
* emite `expandToModal` — el picker no decide qué pasa después (típicamente
|
|
76
|
+
* el consumidor pone `variant` en `'modal'` y `open` en `true`).
|
|
77
|
+
*
|
|
78
|
+
* ## Modo `modal`
|
|
79
|
+
* Mismo buscador, mismos chips, misma lógica de filtrado — compone
|
|
80
|
+
* `kf-modal` (proyectando el contenido) en vez de reimplementar un
|
|
81
|
+
* `<dialog>` a mano: la API de `Modal` (`[(open)]`, cierre con
|
|
82
|
+
* Escape/backdrop/botón vía `<dialog>` nativo) ya cubre exactamente lo que
|
|
83
|
+
* este modo necesita. Compone también `kf-data-table` para la tabla
|
|
84
|
+
* completa (columnas configurables, fade de overflow horizontal ya
|
|
85
|
+
* resuelto ahí) y `kf-pagination` para paginar — ninguno de los tres se
|
|
86
|
+
* reimplementa. Agrega un dropdown propio de "Ordenar por" (calibrado
|
|
87
|
+
* visualmente contra el trigger de `kf-select`, pero compacto: `kf-select`
|
|
88
|
+
* fuerza un ancho fijo de 240px pensado para formularios, no para una
|
|
89
|
+
* barra de herramientas de tabla — ver JSDoc de `sortOpen` más abajo).
|
|
90
|
+
*
|
|
91
|
+
* En mobile, `kf-data-table[cardable]` con `defaultView` forzado a
|
|
92
|
+
* `'cards'` decide la vista (mismo criterio que `matchMediaSignal` ya
|
|
93
|
+
* resolvió en otros consumidores): dentro de un modal a pantalla completa
|
|
94
|
+
* no tiene sentido ofrecer un toggle tabla/cards que en mobile daría una
|
|
95
|
+
* tabla angosta con scroll horizontal.
|
|
96
|
+
*
|
|
97
|
+
* DEUDA TÉCNICA CONOCIDA (confirmada con el usuario, ver CHANGELOG.md): el
|
|
98
|
+
* diseño validado con mockups pedía que en mobile el modal ocupe el 100%
|
|
99
|
+
* de la pantalla (botón "atrás", sin overlay centrado) — `kf-modal` no
|
|
100
|
+
* tiene ese modo, así que hoy en mobile se ve como un diálogo centrado
|
|
101
|
+
* angosto (`width: min(480px, ...)`). Es funcional (cards legibles,
|
|
102
|
+
* búsqueda, chips, paginación), solo visualmente distinto del mockup.
|
|
103
|
+
*
|
|
104
|
+
* <kf-quote-picker
|
|
105
|
+
* [items]="quotes()"
|
|
106
|
+
* [columns]="columns"
|
|
107
|
+
* [selectedId]="selectedQuoteId()"
|
|
108
|
+
* [idKey]="(q) => q.id"
|
|
109
|
+
* [chips]="statusChips()"
|
|
110
|
+
* [cellTemplates]="{ photo: photoCell(), amount: amountCell() }"
|
|
111
|
+
* (selectedIdChange)="selectedQuoteId.set($event)"
|
|
112
|
+
* (expandToModal)="variant.set('modal'); open.set(true)"
|
|
113
|
+
* />
|
|
114
|
+
*/
|
|
115
|
+
class QuotePicker {
|
|
116
|
+
items = input.required(/* @ts-ignore */
|
|
117
|
+
...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
118
|
+
columns = input.required(/* @ts-ignore */
|
|
119
|
+
...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
120
|
+
selectedId = input(null, /* @ts-ignore */
|
|
121
|
+
...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
|
|
122
|
+
idKey = input.required(/* @ts-ignore */
|
|
123
|
+
...(ngDevMode ? [{ debugName: "idKey" }] : /* istanbul ignore next */ []));
|
|
124
|
+
/**
|
|
125
|
+
* Mapa `columnKey -> TemplateRef`, mismo mecanismo que `kf-data-table`
|
|
126
|
+
* (`ngTemplateOutlet` + contexto `{ $implicit: item, index }`) — se
|
|
127
|
+
* reenvía tal cual a la tabla interna del modo `modal`, y se usa
|
|
128
|
+
* directamente en el grid de cards del modo `inline`. Un solo mapa para
|
|
129
|
+
* ambos modos, consistente con la regla de "una sola lógica interna".
|
|
130
|
+
*/
|
|
131
|
+
cellTemplates = input(/* @ts-ignore */
|
|
132
|
+
...(ngDevMode ? [undefined, { debugName: "cellTemplates" }] : /* istanbul ignore next */ []));
|
|
133
|
+
chips = input([], /* @ts-ignore */
|
|
134
|
+
...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
|
|
135
|
+
sortOptions = input([], /* @ts-ignore */
|
|
136
|
+
...(ngDevMode ? [{ debugName: "sortOptions" }] : /* istanbul ignore next */ []));
|
|
137
|
+
sort = model(null, /* @ts-ignore */
|
|
138
|
+
...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
|
|
139
|
+
/**
|
|
140
|
+
* Umbral de volumen alto — el consumidor decide el número exacto, nunca
|
|
141
|
+
* hardcodeado a "cotizaciones" ni a ningún caso de uso puntual. Default
|
|
142
|
+
* 15 según el diseño validado con mockups.
|
|
143
|
+
*/
|
|
144
|
+
expandThreshold = input(15, /* @ts-ignore */
|
|
145
|
+
...(ngDevMode ? [{ debugName: "expandThreshold" }] : /* istanbul ignore next */ []));
|
|
146
|
+
variant = input('inline', /* @ts-ignore */
|
|
147
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
148
|
+
/** Visibilidad del modo `modal`, two-way — mismo patrón que `[(open)]` de `kf-modal`. */
|
|
149
|
+
open = model(false, /* @ts-ignore */
|
|
150
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
151
|
+
title = input('Cambiar cotización', /* @ts-ignore */
|
|
152
|
+
...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
153
|
+
searchPlaceholder = input('Buscar…', /* @ts-ignore */
|
|
154
|
+
...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
|
|
155
|
+
/** Ícono de placeholder cuando ninguna columna `cardRole: 'media'` resuelve
|
|
156
|
+
* un thumbnail real vía `cellTemplates` — el picker no asume qué dominio
|
|
157
|
+
* consume (cotización, vehículo, cliente...), así que no hardcodea un
|
|
158
|
+
* ícono fijo. Default `'file'` solo por no dejar el input sin valor. */
|
|
159
|
+
placeholderIcon = input('file', /* @ts-ignore */
|
|
160
|
+
...(ngDevMode ? [{ debugName: "placeholderIcon" }] : /* istanbul ignore next */ []));
|
|
161
|
+
selectedIdChange = output();
|
|
162
|
+
/** Disparado al superar `expandThreshold` en modo inline; el consumidor decide pasar a `modal`. */
|
|
163
|
+
expandToModal = output();
|
|
164
|
+
searchQuery = signal('', /* @ts-ignore */
|
|
165
|
+
...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
|
|
166
|
+
activeChip = signal(null, /* @ts-ignore */
|
|
167
|
+
...(ngDevMode ? [{ debugName: "activeChip" }] : /* istanbul ignore next */ []));
|
|
168
|
+
expanded = signal(false, /* @ts-ignore */
|
|
169
|
+
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
170
|
+
page = signal(1, /* @ts-ignore */
|
|
171
|
+
...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
|
|
172
|
+
sortOpen = signal(false, /* @ts-ignore */
|
|
173
|
+
...(ngDevMode ? [{ debugName: "sortOpen" }] : /* istanbul ignore next */ []));
|
|
174
|
+
/**
|
|
175
|
+
* Mismo breakpoint (`max-width: 900px`) que el resto de la librería usa
|
|
176
|
+
* para decisiones de layout en TypeScript (no solo CSS) — ver
|
|
177
|
+
* `matchMediaSignal`. Decide 1 vs. 2 columnas en el grid inline, y tabla
|
|
178
|
+
* vs. cards apiladas dentro del modal.
|
|
179
|
+
*/
|
|
180
|
+
isMobile = matchMediaSignal('(max-width: 900px)');
|
|
181
|
+
destroyRef = inject(DestroyRef);
|
|
182
|
+
previewScrollEl = viewChild('previewScroll', /* @ts-ignore */
|
|
183
|
+
...(ngDevMode ? [{ debugName: "previewScrollEl" }] : /* istanbul ignore next */ []));
|
|
184
|
+
/** Mismo mecanismo de fade que `kf-data-table` — ver comentario en QuotePicker.css. */
|
|
185
|
+
canFadeBottom = signal(false, /* @ts-ignore */
|
|
186
|
+
...(ngDevMode ? [{ debugName: "canFadeBottom" }] : /* istanbul ignore next */ []));
|
|
187
|
+
selectedItem = computed(() => this.items().find((item) => this.idKey()(item) === this.selectedId()), /* @ts-ignore */
|
|
188
|
+
...(ngDevMode ? [{ debugName: "selectedItem" }] : /* istanbul ignore next */ []));
|
|
189
|
+
/** Filtro por texto (buscador propio) + filtro por chip activo, en ese orden. */
|
|
190
|
+
filteredItems = computed(() => {
|
|
191
|
+
const query = normalizeForSearch(this.searchQuery().trim());
|
|
192
|
+
const chipValue = this.activeChip();
|
|
193
|
+
const chip = chipValue ? this.chips().find((c) => c.value === chipValue) : undefined;
|
|
194
|
+
const idKeyFn = this.idKey();
|
|
195
|
+
const cols = this.columns();
|
|
196
|
+
return this.items().filter((item) => {
|
|
197
|
+
if (chip && !chip.predicate(item)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (!query) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
const haystack = cols
|
|
204
|
+
.map((column) => this.cellText(item, column))
|
|
205
|
+
.concat(idKeyFn(item))
|
|
206
|
+
.join(' ');
|
|
207
|
+
return normalizeForSearch(haystack).includes(query);
|
|
208
|
+
});
|
|
209
|
+
}, /* @ts-ignore */
|
|
210
|
+
...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
|
|
211
|
+
/**
|
|
212
|
+
* Orden solo tiene efecto visible en modo `modal` (el diseño validado no
|
|
213
|
+
* pide dropdown de orden en inline) — se calcula igual en ambos casos
|
|
214
|
+
* porque es la MISMA lógica interna; en inline `sort()` simplemente se
|
|
215
|
+
* queda en `null` salvo que el consumidor lo setee explícitamente.
|
|
216
|
+
*/
|
|
217
|
+
sortedItems = computed(() => {
|
|
218
|
+
const sortValue = this.sort();
|
|
219
|
+
const items = this.filteredItems();
|
|
220
|
+
if (!sortValue) {
|
|
221
|
+
return items;
|
|
222
|
+
}
|
|
223
|
+
const [key, direction] = sortValue.split(':');
|
|
224
|
+
const sorted = [...items].sort((a, b) => {
|
|
225
|
+
const va = a[key];
|
|
226
|
+
const vb = b[key];
|
|
227
|
+
if (va === vb) {
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
return va > vb ? 1 : -1;
|
|
231
|
+
});
|
|
232
|
+
return direction === 'desc' ? sorted.reverse() : sorted;
|
|
233
|
+
}, /* @ts-ignore */
|
|
234
|
+
...(ngDevMode ? [{ debugName: "sortedItems" }] : /* istanbul ignore next */ []));
|
|
235
|
+
/** Por encima del umbral, el preview inline se recorta salvo que el usuario ya haya expandido. */
|
|
236
|
+
isHighVolume = computed(() => this.sortedItems().length > this.expandThreshold(), /* @ts-ignore */
|
|
237
|
+
...(ngDevMode ? [{ debugName: "isHighVolume" }] : /* istanbul ignore next */ []));
|
|
238
|
+
previewItems = computed(() => {
|
|
239
|
+
const all = this.sortedItems();
|
|
240
|
+
if (!this.isHighVolume() || this.expanded()) {
|
|
241
|
+
return all;
|
|
242
|
+
}
|
|
243
|
+
return all.slice(0, PREVIEW_ROWS);
|
|
244
|
+
}, /* @ts-ignore */
|
|
245
|
+
...(ngDevMode ? [{ debugName: "previewItems" }] : /* istanbul ignore next */ []));
|
|
246
|
+
pages = computed(() => Math.max(Math.ceil(this.sortedItems().length / PAGE_SIZE), 1), /* @ts-ignore */
|
|
247
|
+
...(ngDevMode ? [{ debugName: "pages" }] : /* istanbul ignore next */ []));
|
|
248
|
+
pagedItems = computed(() => {
|
|
249
|
+
const start = (this.page() - 1) * PAGE_SIZE;
|
|
250
|
+
return this.sortedItems().slice(start, start + PAGE_SIZE);
|
|
251
|
+
}, /* @ts-ignore */
|
|
252
|
+
...(ngDevMode ? [{ debugName: "pagedItems" }] : /* istanbul ignore next */ []));
|
|
253
|
+
sortLabel = computed(() => {
|
|
254
|
+
const current = this.sortOptions().find((option) => option.value === this.sort());
|
|
255
|
+
return current?.label ?? 'Ordenar por';
|
|
256
|
+
}, /* @ts-ignore */
|
|
257
|
+
...(ngDevMode ? [{ debugName: "sortLabel" }] : /* istanbul ignore next */ []));
|
|
258
|
+
constructor() {
|
|
259
|
+
afterNextRender(() => {
|
|
260
|
+
const el = this.previewScrollEl()?.nativeElement;
|
|
261
|
+
if (!el) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const update = () => this.updateFadeState(el);
|
|
265
|
+
update();
|
|
266
|
+
el.addEventListener('scroll', update, { passive: true });
|
|
267
|
+
const observer = new ResizeObserver(update);
|
|
268
|
+
observer.observe(el);
|
|
269
|
+
this.destroyRef.onDestroy(() => {
|
|
270
|
+
el.removeEventListener('scroll', update);
|
|
271
|
+
observer.disconnect();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
updateFadeState(el) {
|
|
276
|
+
// 1px de margen por el mismo motivo que en DataTable: subpíxeles de
|
|
277
|
+
// scroll dejarían el fade "prendido" en un preview que ya llegó al final.
|
|
278
|
+
this.canFadeBottom.set(el.scrollHeight - el.clientHeight - el.scrollTop > 1);
|
|
279
|
+
}
|
|
280
|
+
cellText(item, column) {
|
|
281
|
+
const value = item[column.key];
|
|
282
|
+
return value === null || value === undefined ? '' : String(value);
|
|
283
|
+
}
|
|
284
|
+
toggleExpandedPanel() {
|
|
285
|
+
this.expanded.set(!this.expanded());
|
|
286
|
+
}
|
|
287
|
+
onSearchInput(event) {
|
|
288
|
+
this.searchQuery.set(event.target.value);
|
|
289
|
+
this.page.set(1);
|
|
290
|
+
}
|
|
291
|
+
selectChip(value) {
|
|
292
|
+
this.activeChip.set(this.activeChip() === value ? null : value);
|
|
293
|
+
this.page.set(1);
|
|
294
|
+
}
|
|
295
|
+
select(item) {
|
|
296
|
+
const id = this.idKey()(item);
|
|
297
|
+
// El panel se cierra siempre al elegir (haya cambiado el id o no) —
|
|
298
|
+
// mismo comportamiento esperado de cualquier selector, ej. `kf-select`
|
|
299
|
+
// cierra su panel al elegir opción; dejarle esa responsabilidad al
|
|
300
|
+
// consumidor sería una carga que ningún otro componente de la
|
|
301
|
+
// librería le pide.
|
|
302
|
+
this.expanded.set(false);
|
|
303
|
+
if (id === this.selectedId()) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.selectedIdChange.emit(id);
|
|
307
|
+
}
|
|
308
|
+
onItemKeydown(event, item) {
|
|
309
|
+
if (event.key === 'Enter') {
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
this.select(item);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
onSeeAll() {
|
|
315
|
+
this.expandToModal.emit();
|
|
316
|
+
}
|
|
317
|
+
onTableRowClick(event) {
|
|
318
|
+
this.select(event.row);
|
|
319
|
+
}
|
|
320
|
+
toggleSort() {
|
|
321
|
+
this.sortOpen.update((open) => !open);
|
|
322
|
+
}
|
|
323
|
+
setSort(value) {
|
|
324
|
+
this.sort.set(this.sort() === value ? null : value);
|
|
325
|
+
this.sortOpen.set(false);
|
|
326
|
+
}
|
|
327
|
+
trackItem(item, index) {
|
|
328
|
+
const idKeyFn = this.idKey();
|
|
329
|
+
return idKeyFn ? idKeyFn(item) : index;
|
|
330
|
+
}
|
|
331
|
+
/** Arrow function como campo (no un método) para que `[rowKey]` reciba
|
|
332
|
+
* una referencia ESTABLE entre change detections — `trackItem.bind(this)`
|
|
333
|
+
* en el template recrearía una función nueva en cada ciclo, invalidando
|
|
334
|
+
* cualquier optimización de `kf-data-table` que dependa de identidad. */
|
|
335
|
+
rowKeyFn = (item, index) => this.trackItem(item, index);
|
|
336
|
+
cellValue(item, column) {
|
|
337
|
+
return this.cellText(item, column);
|
|
338
|
+
}
|
|
339
|
+
mediaColumn = computed(() => this.columns().find((c) => c.cardRole === 'media'), /* @ts-ignore */
|
|
340
|
+
...(ngDevMode ? [{ debugName: "mediaColumn" }] : /* istanbul ignore next */ []));
|
|
341
|
+
titleColumn = computed(() => this.columns().find((c) => c.cardRole === 'title'), /* @ts-ignore */
|
|
342
|
+
...(ngDevMode ? [{ debugName: "titleColumn" }] : /* istanbul ignore next */ []));
|
|
343
|
+
fieldColumns = computed(() => this.columns().filter((c) => !c.cardRole || c.cardRole === 'field'), /* @ts-ignore */
|
|
344
|
+
...(ngDevMode ? [{ debugName: "fieldColumns" }] : /* istanbul ignore next */ []));
|
|
345
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: QuotePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
346
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: QuotePicker, isStandalone: true, selector: "kf-quote-picker", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, selectedId: { classPropertyName: "selectedId", publicName: "selectedId", isSignal: true, isRequired: false, transformFunction: null }, idKey: { classPropertyName: "idKey", publicName: "idKey", isSignal: true, isRequired: true, transformFunction: null }, cellTemplates: { classPropertyName: "cellTemplates", publicName: "cellTemplates", isSignal: true, isRequired: false, transformFunction: null }, chips: { classPropertyName: "chips", publicName: "chips", isSignal: true, isRequired: false, transformFunction: null }, sortOptions: { classPropertyName: "sortOptions", publicName: "sortOptions", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, expandThreshold: { classPropertyName: "expandThreshold", publicName: "expandThreshold", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, placeholderIcon: { classPropertyName: "placeholderIcon", publicName: "placeholderIcon", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sort: "sortChange", open: "openChange", selectedIdChange: "selectedIdChange", expandToModal: "expandToModal" }, viewQueries: [{ propertyName: "previewScrollEl", first: true, predicate: ["previewScroll"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (variant() === 'inline') {\n <div class=\"kf-quote-picker__inline\">\n <!-- Trigger colapsado (default): ancho completo, nunca una caja angosta. -->\n <button\n type=\"button\"\n class=\"kf-quote-picker__trigger\"\n [attr.aria-expanded]=\"expanded()\"\n (click)=\"toggleExpandedPanel()\"\n >\n <span class=\"kf-quote-picker__thumb\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: selectedItem(), index: 0 }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"md\" />\n }\n </span>\n @if (selectedItem(); as item) {\n <span class=\"kf-quote-picker__trigger-summary\">\n @if (titleColumn(); as title) {\n <span class=\"kf-quote-picker__trigger-field kf-quote-picker__trigger-field--title\">{{\n cellValue(item, title)\n }}</span>\n }\n <!--\n En mobile solo se muestra el t\u00EDtulo (ID) \u2014 mostrar los 5 campos\n en una sola l\u00EDnea de ~90px de ancho real corta el texto sin\n elipsis a mitad del segundo campo (bug real medido:\n scrollWidth=519 vs. clientWidth=87 en 390px). El resto de los\n campos siguen disponibles en el panel expandido y en el modal,\n donde s\u00ED hay espacio horizontal.\n -->\n @if (!isMobile()) {\n @for (col of fieldColumns(); track col.key) {\n <span class=\"kf-quote-picker__trigger-sep\" aria-hidden=\"true\">\u00B7</span>\n <span class=\"kf-quote-picker__trigger-field\">{{ cellValue(item, col) }}</span>\n }\n }\n </span>\n } @else {\n <span class=\"kf-quote-picker__trigger-summary kf-quote-picker__trigger-summary--empty\"\n >Sin selecci\u00F3n</span\n >\n }\n <span class=\"kf-quote-picker__trigger-action\">\n {{ expanded() ? 'Cerrar' : title() }}\n <kf-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"kf-quote-picker__chevron\"\n [class.kf-quote-picker__chevron--open]=\"expanded()\"\n />\n </span>\n </button>\n\n @if (expanded()) {\n <div class=\"kf-quote-picker__panel\">\n <div class=\"kf-quote-picker__toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n\n <div class=\"kf-quote-picker__results-wrap\">\n <div #previewScroll class=\"kf-quote-picker__results\" role=\"listbox\">\n @for (item of previewItems(); track trackItem(item, $index)) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__result\"\n [class.kf-quote-picker__result--active]=\"idKey()(item) === selectedId()\"\n role=\"option\"\n [attr.aria-selected]=\"idKey()(item) === selectedId()\"\n (click)=\"select(item)\"\n (keydown)=\"onItemKeydown($event, item)\"\n >\n <span class=\"kf-quote-picker__thumb kf-quote-picker__thumb--sm\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: $index }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"sm\" />\n }\n </span>\n @if (titleColumn(); as title) {\n <span\n class=\"kf-quote-picker__result-field kf-quote-picker__result-field--title\"\n >{{ cellValue(item, title) }}</span\n >\n }\n <!--\n Wrapper propio (no los spans sueltos) para los campos\n secundarios: en mobile necesita saltar de l\u00EDnea COMPLETO\n (mismo criterio que \"unidades\" no debe competir con\n \"modelo\" en la primera l\u00EDnea) \u2014 `flex-basis: 100%` en un\n solo elemento fuerza ese salto sin tener que asumir cu\u00E1l\n de los N campos gen\u00E9ricos es el m\u00E1s corto o el m\u00E1s largo.\n -->\n <span class=\"kf-quote-picker__result-fields\">\n @for (col of fieldColumns(); track col.key; let first = $first) {\n <span\n class=\"kf-quote-picker__result-field\"\n [class.kf-quote-picker__result-field--primary]=\"first\"\n >{{ cellValue(item, col) }}</span\n >\n }\n </span>\n </button>\n } @empty {\n <p class=\"kf-quote-picker__empty\">Sin resultados para esta b\u00FAsqueda.</p>\n }\n </div>\n @if (canFadeBottom()) {\n <div class=\"kf-quote-picker__fade\" aria-hidden=\"true\"></div>\n }\n </div>\n\n @if (isHighVolume()) {\n <button type=\"button\" class=\"kf-quote-picker__see-all\" (click)=\"onSeeAll()\">\n Ver las {{ sortedItems().length }} en pantalla completa\n </button>\n }\n </div>\n }\n </div>\n} @else {\n <kf-modal [(open)]=\"open\" [title]=\"title()\" [dismissible]=\"true\">\n <div class=\"kf-quote-picker__modal-toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n <div class=\"kf-quote-picker__modal-filters\">\n <div class=\"kf-quote-picker__modal-chips\">\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n @if (sortOptions().length > 0) {\n <div class=\"kf-quote-picker__sort\">\n <button\n type=\"button\"\n class=\"kf-quote-picker__sort-trigger\"\n [attr.aria-expanded]=\"sortOpen()\"\n (click)=\"toggleSort()\"\n >\n <kf-icon name=\"sort\" size=\"sm\" />\n {{ sortLabel() }}\n <kf-icon name=\"chevron-down\" size=\"sm\" />\n </button>\n @if (sortOpen()) {\n <div class=\"kf-quote-picker__sort-panel\">\n @for (option of sortOptions(); track option.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__sort-option\"\n [class.kf-quote-picker__sort-option--active]=\"sort() === option.value\"\n (click)=\"setSort(option.value)\"\n >\n {{ option.label }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <kf-data-table\n [columns]=\"columns()\"\n [rows]=\"pagedItems()\"\n [cardable]=\"true\"\n [defaultView]=\"isMobile() ? 'cards' : 'table'\"\n [cellTemplates]=\"cellTemplates()\"\n [rowKey]=\"rowKeyFn\"\n (rowClick)=\"onTableRowClick($event)\"\n >\n <kf-empty-state empty icon=\"file\">Sin resultados para esta b\u00FAsqueda.</kf-empty-state>\n @if (sortedItems().length > 0) {\n <kf-pagination footer [page]=\"page()\" [pages]=\"pages()\" (pageChange)=\"page.set($event)\">\n {{ sortedItems().length }} resultados\n </kf-pagination>\n }\n </kf-data-table>\n </kf-modal>\n}\n", styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-quote-picker__trigger{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-md);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-primary);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-sm);cursor:pointer;font:inherit;text-align:left}.kf-quote-picker__trigger:hover{box-shadow:var(--kf-shadow-md)}.kf-quote-picker__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__thumb{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:none;border-radius:var(--kf-radius-sm);background:var(--kf-color-surface);color:var(--kf-color-gray-400)}.kf-quote-picker__thumb--sm{width:36px;height:36px}.kf-quote-picker__result--active .kf-quote-picker__thumb{background:var(--kf-color-white);color:var(--kf-color-primary)}.kf-quote-picker__trigger-summary{flex:1;min-width:0;display:flex;align-items:baseline;gap:var(--kf-space-xs);overflow:hidden;white-space:nowrap;color:var(--kf-color-ink)}.kf-quote-picker__trigger-summary--empty{color:var(--kf-color-text-muted)}.kf-quote-picker__trigger-field--title{font-family:var(--kf-font-title);font-weight:700;font-size:1.05rem;color:var(--kf-color-ink)}.kf-quote-picker__trigger-field{font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__trigger-sep{color:var(--kf-color-border-strong)}.kf-quote-picker__trigger-action{flex:none;display:flex;align-items:center;gap:6px;color:var(--kf-color-primary);font-size:.85rem;font-weight:600}.kf-quote-picker__chevron{transition:transform .14s}.kf-quote-picker__chevron--open{transform:rotate(180deg)}.kf-quote-picker__panel{display:flex;flex-direction:column;gap:var(--kf-space-md);margin-top:var(--kf-space-sm);padding:var(--kf-space-md) var(--kf-space-lg);background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md)}.kf-quote-picker__toolbar{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}.kf-quote-picker__search{flex:1;min-width:200px;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);color:var(--kf-color-text-muted)}.kf-quote-picker__search:focus-within{border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__search-input{flex:1;border:none;outline:none;background:transparent;font:inherit;color:var(--kf-color-ink)}.kf-quote-picker__search-input::placeholder{color:var(--kf-color-text-muted)}.kf-quote-picker__chip{flex:none;padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__chip--active{background:var(--kf-color-primary);border-color:var(--kf-color-primary);color:var(--kf-color-white)}.kf-quote-picker__results-wrap{position:relative}.kf-quote-picker__results{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--kf-space-sm);max-height:340px;overflow-y:auto}@media(max-width:900px){.kf-quote-picker__results{grid-template-columns:1fr}}.kf-quote-picker__result{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);background:var(--kf-color-white);cursor:pointer;text-align:left;font:inherit;min-width:0;flex-wrap:wrap;row-gap:2px}.kf-quote-picker__result-fields{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0;flex:1}.kf-quote-picker__result:hover{border-color:var(--kf-color-border)}.kf-quote-picker__result--active{border-color:var(--kf-color-primary);background:var(--kf-color-primary-soft)}.kf-quote-picker__result-field{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__result-field--title{flex:none;min-width:92px;font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__result-field--primary{flex:1;min-width:0}@media(max-width:900px){.kf-quote-picker__result-fields{flex-basis:100%;flex-wrap:wrap;row-gap:2px;padding-left:calc(36px + var(--kf-space-sm))}.kf-quote-picker__result-field--primary{flex:none;min-width:0;max-width:100%}}.kf-quote-picker__fade{position:absolute;left:0;right:0;bottom:0;height:36px;background:linear-gradient(to top,var(--kf-color-surface),transparent);pointer-events:none}.kf-quote-picker__see-all{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:var(--kf-space-sm);width:100%;margin-top:var(--kf-space-sm);padding:var(--kf-space-sm);border:1.5px dashed var(--kf-color-primary);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-primary);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer}.kf-quote-picker__see-all:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__empty{grid-column:1 / -1;padding:var(--kf-space-md);text-align:center;color:var(--kf-color-text-muted);font-size:.85rem;margin:0}.kf-quote-picker__modal-toolbar{display:flex;flex-direction:column;gap:var(--kf-space-sm);margin-bottom:var(--kf-space-md)}.kf-quote-picker__modal-filters{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);flex-wrap:wrap}.kf-quote-picker__modal-chips{display:flex;gap:var(--kf-space-sm);flex-wrap:wrap}.kf-quote-picker__sort{position:relative;flex:none}.kf-quote-picker__sort-trigger{display:flex;align-items:center;gap:var(--kf-space-sm);padding:6px var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__sort-trigger:hover{border-color:var(--kf-color-border-strong)}.kf-quote-picker__sort-panel{position:absolute;top:calc(100% + 4px);right:0;z-index:1;min-width:180px;padding:var(--kf-space-xs) 0;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-md)}.kf-quote-picker__sort-option{display:block;width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;background:transparent;color:var(--kf-color-text);font:inherit;font-size:.85rem;text-align:left;cursor:pointer}.kf-quote-picker__sort-option:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__sort-option--active{color:var(--kf-color-primary);font-weight:600}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size", "rotate", "filled"] }, { kind: "component", type: Modal, selector: "kf-modal", inputs: ["title", "open", "dismissible", "width"], outputs: ["openChange"] }, { kind: "component", type: DataTable, selector: "kf-data-table", inputs: ["columns", "rows", "loading", "rowKey", "flush", "cardable", "defaultView", "render", "cellTemplates", "density", "rowClickable", "pageSize", "pageSizeOptions", "itemLabel", "sort"], outputs: ["rowClick", "pageChange", "pageSizeChange", "sortChange"] }, { kind: "component", type: EmptyState, selector: "kf-empty-state", inputs: ["icon", "tone"] }, { kind: "component", type: Pagination, selector: "kf-pagination", inputs: ["page", "pages", "pageSizeOptions", "pageSize"], outputs: ["pageChange", "pageSizeChange"] }] });
|
|
347
|
+
}
|
|
348
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: QuotePicker, decorators: [{
|
|
349
|
+
type: Component,
|
|
350
|
+
args: [{ selector: 'kf-quote-picker', imports: [NgTemplateOutlet, Icon, Modal, DataTable, EmptyState, Pagination], template: "@if (variant() === 'inline') {\n <div class=\"kf-quote-picker__inline\">\n <!-- Trigger colapsado (default): ancho completo, nunca una caja angosta. -->\n <button\n type=\"button\"\n class=\"kf-quote-picker__trigger\"\n [attr.aria-expanded]=\"expanded()\"\n (click)=\"toggleExpandedPanel()\"\n >\n <span class=\"kf-quote-picker__thumb\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: selectedItem(), index: 0 }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"md\" />\n }\n </span>\n @if (selectedItem(); as item) {\n <span class=\"kf-quote-picker__trigger-summary\">\n @if (titleColumn(); as title) {\n <span class=\"kf-quote-picker__trigger-field kf-quote-picker__trigger-field--title\">{{\n cellValue(item, title)\n }}</span>\n }\n <!--\n En mobile solo se muestra el t\u00EDtulo (ID) \u2014 mostrar los 5 campos\n en una sola l\u00EDnea de ~90px de ancho real corta el texto sin\n elipsis a mitad del segundo campo (bug real medido:\n scrollWidth=519 vs. clientWidth=87 en 390px). El resto de los\n campos siguen disponibles en el panel expandido y en el modal,\n donde s\u00ED hay espacio horizontal.\n -->\n @if (!isMobile()) {\n @for (col of fieldColumns(); track col.key) {\n <span class=\"kf-quote-picker__trigger-sep\" aria-hidden=\"true\">\u00B7</span>\n <span class=\"kf-quote-picker__trigger-field\">{{ cellValue(item, col) }}</span>\n }\n }\n </span>\n } @else {\n <span class=\"kf-quote-picker__trigger-summary kf-quote-picker__trigger-summary--empty\"\n >Sin selecci\u00F3n</span\n >\n }\n <span class=\"kf-quote-picker__trigger-action\">\n {{ expanded() ? 'Cerrar' : title() }}\n <kf-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"kf-quote-picker__chevron\"\n [class.kf-quote-picker__chevron--open]=\"expanded()\"\n />\n </span>\n </button>\n\n @if (expanded()) {\n <div class=\"kf-quote-picker__panel\">\n <div class=\"kf-quote-picker__toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n\n <div class=\"kf-quote-picker__results-wrap\">\n <div #previewScroll class=\"kf-quote-picker__results\" role=\"listbox\">\n @for (item of previewItems(); track trackItem(item, $index)) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__result\"\n [class.kf-quote-picker__result--active]=\"idKey()(item) === selectedId()\"\n role=\"option\"\n [attr.aria-selected]=\"idKey()(item) === selectedId()\"\n (click)=\"select(item)\"\n (keydown)=\"onItemKeydown($event, item)\"\n >\n <span class=\"kf-quote-picker__thumb kf-quote-picker__thumb--sm\">\n @if (mediaColumn(); as media) {\n @if (cellTemplates()?.[media.key]; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: $index }\"\n />\n }\n } @else {\n <kf-icon [name]=\"placeholderIcon()\" size=\"sm\" />\n }\n </span>\n @if (titleColumn(); as title) {\n <span\n class=\"kf-quote-picker__result-field kf-quote-picker__result-field--title\"\n >{{ cellValue(item, title) }}</span\n >\n }\n <!--\n Wrapper propio (no los spans sueltos) para los campos\n secundarios: en mobile necesita saltar de l\u00EDnea COMPLETO\n (mismo criterio que \"unidades\" no debe competir con\n \"modelo\" en la primera l\u00EDnea) \u2014 `flex-basis: 100%` en un\n solo elemento fuerza ese salto sin tener que asumir cu\u00E1l\n de los N campos gen\u00E9ricos es el m\u00E1s corto o el m\u00E1s largo.\n -->\n <span class=\"kf-quote-picker__result-fields\">\n @for (col of fieldColumns(); track col.key; let first = $first) {\n <span\n class=\"kf-quote-picker__result-field\"\n [class.kf-quote-picker__result-field--primary]=\"first\"\n >{{ cellValue(item, col) }}</span\n >\n }\n </span>\n </button>\n } @empty {\n <p class=\"kf-quote-picker__empty\">Sin resultados para esta b\u00FAsqueda.</p>\n }\n </div>\n @if (canFadeBottom()) {\n <div class=\"kf-quote-picker__fade\" aria-hidden=\"true\"></div>\n }\n </div>\n\n @if (isHighVolume()) {\n <button type=\"button\" class=\"kf-quote-picker__see-all\" (click)=\"onSeeAll()\">\n Ver las {{ sortedItems().length }} en pantalla completa\n </button>\n }\n </div>\n }\n </div>\n} @else {\n <kf-modal [(open)]=\"open\" [title]=\"title()\" [dismissible]=\"true\">\n <div class=\"kf-quote-picker__modal-toolbar\">\n <div class=\"kf-quote-picker__search\">\n <kf-icon name=\"search\" size=\"sm\" />\n <input\n type=\"text\"\n class=\"kf-quote-picker__search-input\"\n [placeholder]=\"searchPlaceholder()\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n </div>\n <div class=\"kf-quote-picker__modal-filters\">\n <div class=\"kf-quote-picker__modal-chips\">\n @for (chip of chips(); track chip.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__chip\"\n [class.kf-quote-picker__chip--active]=\"activeChip() === chip.value\"\n (click)=\"selectChip(chip.value)\"\n >\n {{ chip.label }} ({{ chip.count }})\n </button>\n }\n </div>\n @if (sortOptions().length > 0) {\n <div class=\"kf-quote-picker__sort\">\n <button\n type=\"button\"\n class=\"kf-quote-picker__sort-trigger\"\n [attr.aria-expanded]=\"sortOpen()\"\n (click)=\"toggleSort()\"\n >\n <kf-icon name=\"sort\" size=\"sm\" />\n {{ sortLabel() }}\n <kf-icon name=\"chevron-down\" size=\"sm\" />\n </button>\n @if (sortOpen()) {\n <div class=\"kf-quote-picker__sort-panel\">\n @for (option of sortOptions(); track option.value) {\n <button\n type=\"button\"\n class=\"kf-quote-picker__sort-option\"\n [class.kf-quote-picker__sort-option--active]=\"sort() === option.value\"\n (click)=\"setSort(option.value)\"\n >\n {{ option.label }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <kf-data-table\n [columns]=\"columns()\"\n [rows]=\"pagedItems()\"\n [cardable]=\"true\"\n [defaultView]=\"isMobile() ? 'cards' : 'table'\"\n [cellTemplates]=\"cellTemplates()\"\n [rowKey]=\"rowKeyFn\"\n (rowClick)=\"onTableRowClick($event)\"\n >\n <kf-empty-state empty icon=\"file\">Sin resultados para esta b\u00FAsqueda.</kf-empty-state>\n @if (sortedItems().length > 0) {\n <kf-pagination footer [page]=\"page()\" [pages]=\"pages()\" (pageChange)=\"page.set($event)\">\n {{ sortedItems().length }} resultados\n </kf-pagination>\n }\n </kf-data-table>\n </kf-modal>\n}\n", styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-quote-picker__trigger{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-md);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-primary);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-sm);cursor:pointer;font:inherit;text-align:left}.kf-quote-picker__trigger:hover{box-shadow:var(--kf-shadow-md)}.kf-quote-picker__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__thumb{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:none;border-radius:var(--kf-radius-sm);background:var(--kf-color-surface);color:var(--kf-color-gray-400)}.kf-quote-picker__thumb--sm{width:36px;height:36px}.kf-quote-picker__result--active .kf-quote-picker__thumb{background:var(--kf-color-white);color:var(--kf-color-primary)}.kf-quote-picker__trigger-summary{flex:1;min-width:0;display:flex;align-items:baseline;gap:var(--kf-space-xs);overflow:hidden;white-space:nowrap;color:var(--kf-color-ink)}.kf-quote-picker__trigger-summary--empty{color:var(--kf-color-text-muted)}.kf-quote-picker__trigger-field--title{font-family:var(--kf-font-title);font-weight:700;font-size:1.05rem;color:var(--kf-color-ink)}.kf-quote-picker__trigger-field{font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__trigger-sep{color:var(--kf-color-border-strong)}.kf-quote-picker__trigger-action{flex:none;display:flex;align-items:center;gap:6px;color:var(--kf-color-primary);font-size:.85rem;font-weight:600}.kf-quote-picker__chevron{transition:transform .14s}.kf-quote-picker__chevron--open{transform:rotate(180deg)}.kf-quote-picker__panel{display:flex;flex-direction:column;gap:var(--kf-space-md);margin-top:var(--kf-space-sm);padding:var(--kf-space-md) var(--kf-space-lg);background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md)}.kf-quote-picker__toolbar{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}.kf-quote-picker__search{flex:1;min-width:200px;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1.5px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);color:var(--kf-color-text-muted)}.kf-quote-picker__search:focus-within{border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-quote-picker__search-input{flex:1;border:none;outline:none;background:transparent;font:inherit;color:var(--kf-color-ink)}.kf-quote-picker__search-input::placeholder{color:var(--kf-color-text-muted)}.kf-quote-picker__chip{flex:none;padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__chip--active{background:var(--kf-color-primary);border-color:var(--kf-color-primary);color:var(--kf-color-white)}.kf-quote-picker__results-wrap{position:relative}.kf-quote-picker__results{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--kf-space-sm);max-height:340px;overflow-y:auto}@media(max-width:900px){.kf-quote-picker__results{grid-template-columns:1fr}}.kf-quote-picker__result{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);background:var(--kf-color-white);cursor:pointer;text-align:left;font:inherit;min-width:0;flex-wrap:wrap;row-gap:2px}.kf-quote-picker__result-fields{display:flex;align-items:center;gap:var(--kf-space-sm);min-width:0;flex:1}.kf-quote-picker__result:hover{border-color:var(--kf-color-border)}.kf-quote-picker__result--active{border-color:var(--kf-color-primary);background:var(--kf-color-primary-soft)}.kf-quote-picker__result-field{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.85rem;color:var(--kf-color-text)}.kf-quote-picker__result-field--title{flex:none;min-width:92px;font-weight:700;color:var(--kf-color-ink)}.kf-quote-picker__result-field--primary{flex:1;min-width:0}@media(max-width:900px){.kf-quote-picker__result-fields{flex-basis:100%;flex-wrap:wrap;row-gap:2px;padding-left:calc(36px + var(--kf-space-sm))}.kf-quote-picker__result-field--primary{flex:none;min-width:0;max-width:100%}}.kf-quote-picker__fade{position:absolute;left:0;right:0;bottom:0;height:36px;background:linear-gradient(to top,var(--kf-color-surface),transparent);pointer-events:none}.kf-quote-picker__see-all{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:var(--kf-space-sm);width:100%;margin-top:var(--kf-space-sm);padding:var(--kf-space-sm);border:1.5px dashed var(--kf-color-primary);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-primary);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer}.kf-quote-picker__see-all:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__empty{grid-column:1 / -1;padding:var(--kf-space-md);text-align:center;color:var(--kf-color-text-muted);font-size:.85rem;margin:0}.kf-quote-picker__modal-toolbar{display:flex;flex-direction:column;gap:var(--kf-space-sm);margin-bottom:var(--kf-space-md)}.kf-quote-picker__modal-filters{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);flex-wrap:wrap}.kf-quote-picker__modal-chips{display:flex;gap:var(--kf-space-sm);flex-wrap:wrap}.kf-quote-picker__sort{position:relative;flex:none}.kf-quote-picker__sort-trigger{display:flex;align-items:center;gap:var(--kf-space-sm);padding:6px var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);background:var(--kf-color-white);color:var(--kf-color-text);font:inherit;font-size:.8rem;font-weight:600;cursor:pointer}.kf-quote-picker__sort-trigger:hover{border-color:var(--kf-color-border-strong)}.kf-quote-picker__sort-panel{position:absolute;top:calc(100% + 4px);right:0;z-index:1;min-width:180px;padding:var(--kf-space-xs) 0;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-md)}.kf-quote-picker__sort-option{display:block;width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;background:transparent;color:var(--kf-color-text);font:inherit;font-size:.85rem;text-align:left;cursor:pointer}.kf-quote-picker__sort-option:hover{background:var(--kf-color-primary-soft)}.kf-quote-picker__sort-option--active{color:var(--kf-color-primary);font-weight:600}\n"] }]
|
|
351
|
+
}], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], selectedId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedId", required: false }] }], idKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "idKey", required: true }] }], cellTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplates", required: false }] }], chips: [{ type: i0.Input, args: [{ isSignal: true, alias: "chips", required: false }] }], sortOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortOptions", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }, { type: i0.Output, args: ["sortChange"] }], expandThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandThreshold", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], placeholderIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderIcon", required: false }] }], selectedIdChange: [{ type: i0.Output, args: ["selectedIdChange"] }], expandToModal: [{ type: i0.Output, args: ["expandToModal"] }], previewScrollEl: [{ type: i0.ViewChild, args: ['previewScroll', { isSignal: true }] }] } });
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Estado vacío de reservas en taller con llamada a agendar (página 36):
|
|
355
|
+
* barra de estado vacío arriba ("No tienes reservas de taller"), panel de
|
|
356
|
+
* invitación al medio (ícono en círculo azul, título, descripción, link) y
|
|
357
|
+
* una fila de dos acciones al pie — un slot `[actions]` proyectado, para no
|
|
358
|
+
* acoplar el componente a botones fijos.
|
|
359
|
+
*
|
|
360
|
+
* Distinto de `kf-empty-state` genérico: este combina la barra + la
|
|
361
|
+
* invitación + las acciones como un bloque compuesto propio del dominio
|
|
362
|
+
* "taller", no un placeholder de una sola pieza.
|
|
363
|
+
*
|
|
364
|
+
* `grow` estira el panel de invitación para llenar el espacio vertical
|
|
365
|
+
* disponible — solo cuando el consumidor comparte fila con una tarjeta más
|
|
366
|
+
* alta (ej. junto a "Total de vehículos"); en un lugar sin esa presión
|
|
367
|
+
* (home, debajo del carousel) se deja en su alto natural.
|
|
368
|
+
*
|
|
369
|
+
* <kf-reserve-block emptyLabel="No tienes reservas de taller" grow
|
|
370
|
+
* title="¿Necesitas mantención?" description="Tenemos la mejor red de talleres."
|
|
371
|
+
* actionLabel="Agenda tu reserva aquí" actionHref="/taller/agendar">
|
|
372
|
+
* <kf-button actions icon="calendar">Reservar hora</kf-button>
|
|
373
|
+
* <kf-button actions variant="outline" icon="list">Ver agenda</kf-button>
|
|
374
|
+
* </kf-reserve-block>
|
|
375
|
+
*/
|
|
376
|
+
class ReserveBlock {
|
|
377
|
+
emptyLabel = input('No tienes reservas de taller', /* @ts-ignore */
|
|
378
|
+
...(ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []));
|
|
379
|
+
title = input.required(/* @ts-ignore */
|
|
380
|
+
...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
381
|
+
description = input(/* @ts-ignore */
|
|
382
|
+
...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
|
|
383
|
+
actionLabel = input(/* @ts-ignore */
|
|
384
|
+
...(ngDevMode ? [undefined, { debugName: "actionLabel" }] : /* istanbul ignore next */ []));
|
|
385
|
+
actionHref = input(/* @ts-ignore */
|
|
386
|
+
...(ngDevMode ? [undefined, { debugName: "actionHref" }] : /* istanbul ignore next */ []));
|
|
387
|
+
/** La invitación se estira para llenar la tarjeta (ej. junto a "Total de vehículos"). */
|
|
388
|
+
grow = input(false, { ...(ngDevMode ? { debugName: "grow" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
389
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ReserveBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
390
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: ReserveBlock, isStandalone: true, selector: "kf-reserve-block", inputs: { emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, actionLabel: { classPropertyName: "actionLabel", publicName: "actionLabel", isSignal: true, isRequired: false, transformFunction: null }, actionHref: { classPropertyName: "actionHref", publicName: "actionHref", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"kf-reserve-block__empty\">\n <kf-icon name=\"calendar\" size=\"sm\" />\n <span>{{ emptyLabel() }}</span>\n</div>\n<div class=\"kf-reserve-block__invite\" [class.kf-reserve-block__invite--grow]=\"grow()\">\n <span class=\"kf-reserve-block__invite-icon\">\n <kf-icon name=\"tool\" size=\"md\" />\n </span>\n <div class=\"kf-reserve-block__invite-text\">\n <span class=\"kf-reserve-block__invite-title\">{{ title() }}</span>\n @if (description()) {\n <span class=\"kf-reserve-block__invite-description\">{{ description() }}</span>\n }\n @if (actionLabel()) {\n <kf-link class=\"kf-reserve-block__invite-action\" [href]=\"actionHref() || '#'\" showArrow>\n {{ actionLabel() }}\n </kf-link>\n }\n </div>\n</div>\n<div class=\"kf-reserve-block__actions\">\n <ng-content select=\"[actions]\" />\n</div>\n", styles: [":host{display:flex;flex-direction:column;gap:var(--kf-space-sm);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-reserve-block__empty{display:flex;align-items:center;gap:var(--kf-space-xs);padding-bottom:var(--kf-space-sm);border-bottom:1px solid var(--kf-color-border);color:var(--kf-color-text-muted);font-size:.875rem}.kf-reserve-block__invite{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm);border-radius:var(--kf-radius-sm);background:var(--kf-color-primary-soft)}.kf-reserve-block__invite--grow{flex:1}.kf-reserve-block__invite-icon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.25rem;height:2.25rem;border-radius:var(--kf-radius-pill);background:var(--kf-color-primary);color:var(--kf-color-white)}.kf-reserve-block__invite-text{display:flex;flex-direction:column;gap:2px;min-width:0}.kf-reserve-block__invite-title{font-family:var(--kf-font-title);font-weight:700;color:var(--kf-color-ink)}.kf-reserve-block__invite-description{font-size:.8125rem;color:var(--kf-color-text-muted)}.kf-reserve-block__invite-action{margin-top:var(--kf-space-xs);font-size:.8125rem;font-weight:600}.kf-reserve-block__actions{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size", "rotate", "filled"] }, { kind: "component", type: Link, selector: "kf-link", inputs: ["variant", "disabled", "href", "showArrow"] }] });
|
|
391
|
+
}
|
|
392
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ReserveBlock, decorators: [{
|
|
393
|
+
type: Component,
|
|
394
|
+
args: [{ selector: 'kf-reserve-block', imports: [Icon, Link], template: "<div class=\"kf-reserve-block__empty\">\n <kf-icon name=\"calendar\" size=\"sm\" />\n <span>{{ emptyLabel() }}</span>\n</div>\n<div class=\"kf-reserve-block__invite\" [class.kf-reserve-block__invite--grow]=\"grow()\">\n <span class=\"kf-reserve-block__invite-icon\">\n <kf-icon name=\"tool\" size=\"md\" />\n </span>\n <div class=\"kf-reserve-block__invite-text\">\n <span class=\"kf-reserve-block__invite-title\">{{ title() }}</span>\n @if (description()) {\n <span class=\"kf-reserve-block__invite-description\">{{ description() }}</span>\n }\n @if (actionLabel()) {\n <kf-link class=\"kf-reserve-block__invite-action\" [href]=\"actionHref() || '#'\" showArrow>\n {{ actionLabel() }}\n </kf-link>\n }\n </div>\n</div>\n<div class=\"kf-reserve-block__actions\">\n <ng-content select=\"[actions]\" />\n</div>\n", styles: [":host{display:flex;flex-direction:column;gap:var(--kf-space-sm);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-reserve-block__empty{display:flex;align-items:center;gap:var(--kf-space-xs);padding-bottom:var(--kf-space-sm);border-bottom:1px solid var(--kf-color-border);color:var(--kf-color-text-muted);font-size:.875rem}.kf-reserve-block__invite{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm);border-radius:var(--kf-radius-sm);background:var(--kf-color-primary-soft)}.kf-reserve-block__invite--grow{flex:1}.kf-reserve-block__invite-icon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.25rem;height:2.25rem;border-radius:var(--kf-radius-pill);background:var(--kf-color-primary);color:var(--kf-color-white)}.kf-reserve-block__invite-text{display:flex;flex-direction:column;gap:2px;min-width:0}.kf-reserve-block__invite-title{font-family:var(--kf-font-title);font-weight:700;color:var(--kf-color-ink)}.kf-reserve-block__invite-description{font-size:.8125rem;color:var(--kf-color-text-muted)}.kf-reserve-block__invite-action{margin-top:var(--kf-space-xs);font-size:.8125rem;font-weight:600}.kf-reserve-block__actions{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}\n"] }]
|
|
395
|
+
}], propDecorators: { emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], actionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionLabel", required: false }] }], actionHref: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionHref", required: false }] }], grow: [{ type: i0.Input, args: [{ isSignal: true, alias: "grow", required: false }] }] } });
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Header de identidad de un vehículo de flota: unidad (patente/id), modelo
|
|
399
|
+
* (como badge, no texto plano — página 24: "ABC-123" junto a un badge
|
|
400
|
+
* "TITAN 4X2"), VIN en fuente mono y un estado opcional. Un área de acciones
|
|
401
|
+
* opcional se proyecta vía `<ng-content select="[actions]">` para no acoplar
|
|
402
|
+
* el componente a un set fijo de botones — el patrón real de la spec es
|
|
403
|
+
* `kf-button variant="outline"` (ej. "Llamar", con ícono) + un `primary`
|
|
404
|
+
* (ej. "Crear Ticket", con ícono "+"):
|
|
405
|
+
*
|
|
406
|
+
* <kf-vehicle-header unit="AB123CD" model="Sprinter 415" vin="9BW..."
|
|
407
|
+
* [status]="{ label: 'En ruta', tone: 'ok' }">
|
|
408
|
+
* <kf-button actions variant="outline" icon="phone">Llamar</kf-button>
|
|
409
|
+
* <kf-button actions icon="plus">Crear Ticket</kf-button>
|
|
410
|
+
* </kf-vehicle-header>
|
|
411
|
+
*/
|
|
412
|
+
class VehicleHeader {
|
|
413
|
+
unit = input.required(/* @ts-ignore */
|
|
414
|
+
...(ngDevMode ? [{ debugName: "unit" }] : /* istanbul ignore next */ []));
|
|
415
|
+
model = input(/* @ts-ignore */
|
|
416
|
+
...(ngDevMode ? [undefined, { debugName: "model" }] : /* istanbul ignore next */ []));
|
|
417
|
+
vin = input(/* @ts-ignore */
|
|
418
|
+
...(ngDevMode ? [undefined, { debugName: "vin" }] : /* istanbul ignore next */ []));
|
|
419
|
+
status = input(/* @ts-ignore */
|
|
420
|
+
...(ngDevMode ? [undefined, { debugName: "status" }] : /* istanbul ignore next */ []));
|
|
421
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: VehicleHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
422
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: VehicleHeader, isStandalone: true, selector: "kf-vehicle-header", inputs: { unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: true, transformFunction: null }, model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: false, transformFunction: null }, vin: { classPropertyName: "vin", publicName: "vin", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"kf-vehicle-header__identity\">\n <span class=\"kf-vehicle-header__unit\">{{ unit() }}</span>\n @if (model()) {\n <span class=\"kf-vehicle-header__model\">\n <kf-text as=\"span\" variant=\"overline-strong\">{{ model() }}</kf-text>\n </span>\n }\n @if (vin()) {\n <span class=\"kf-vehicle-header__vin\">VIN: {{ vin() }}</span>\n }\n</div>\n\n@if (status(); as currentStatus) {\n <span\n class=\"kf-vehicle-header__status\"\n [class]=\"'kf-vehicle-header__status--' + (currentStatus.tone ?? 'ok')\"\n >\n @if (currentStatus.icon; as iconName) {\n <kf-icon [name]=\"iconName\" size=\"sm\" />\n }\n {{ currentStatus.label }}\n </span>\n}\n\n<div class=\"kf-vehicle-header__actions\">\n <ng-content select=\"[actions]\" />\n</div>\n", styles: [":host{display:flex;align-items:center;gap:var(--kf-space-md);padding:var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);background:var(--kf-color-surface);font-family:var(--kf-font-body)}.kf-vehicle-header__identity{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--kf-space-sm);flex:1 1 auto;min-width:0}.kf-vehicle-header__unit{font-family:var(--kf-font-title);font-size:1.125rem;font-weight:700;color:var(--kf-color-ink)}.kf-vehicle-header__model{padding:2px var(--kf-space-sm);border-radius:var(--kf-radius-xs);background:var(--kf-color-border)}.kf-vehicle-header__vin{font-family:var(--kf-font-mono);font-size:.8125rem;color:var(--kf-color-text-muted)}.kf-vehicle-header__status{flex:none;display:inline-flex;align-items:center;gap:var(--kf-space-xs);padding:var(--kf-space-xs) var(--kf-space-sm);border-radius:var(--kf-radius-pill);font-size:.75rem;font-weight:600;line-height:1;white-space:nowrap}.kf-vehicle-header__status--ok{background:var(--kf-color-success-soft);color:var(--kf-color-success)}.kf-vehicle-header__status--warn{background:var(--kf-color-warning-soft);color:var(--kf-color-warning)}.kf-vehicle-header__status--crit{background:var(--kf-color-error-soft);color:var(--kf-color-error)}.kf-vehicle-header__actions{flex:none;display:flex;align-items:center;gap:var(--kf-space-sm)}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size", "rotate", "filled"] }, { kind: "component", type: Text, selector: "kf-text", inputs: ["as", "variant", "tone"] }] });
|
|
423
|
+
}
|
|
424
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: VehicleHeader, decorators: [{
|
|
425
|
+
type: Component,
|
|
426
|
+
args: [{ selector: 'kf-vehicle-header', imports: [Icon, Text], template: "<div class=\"kf-vehicle-header__identity\">\n <span class=\"kf-vehicle-header__unit\">{{ unit() }}</span>\n @if (model()) {\n <span class=\"kf-vehicle-header__model\">\n <kf-text as=\"span\" variant=\"overline-strong\">{{ model() }}</kf-text>\n </span>\n }\n @if (vin()) {\n <span class=\"kf-vehicle-header__vin\">VIN: {{ vin() }}</span>\n }\n</div>\n\n@if (status(); as currentStatus) {\n <span\n class=\"kf-vehicle-header__status\"\n [class]=\"'kf-vehicle-header__status--' + (currentStatus.tone ?? 'ok')\"\n >\n @if (currentStatus.icon; as iconName) {\n <kf-icon [name]=\"iconName\" size=\"sm\" />\n }\n {{ currentStatus.label }}\n </span>\n}\n\n<div class=\"kf-vehicle-header__actions\">\n <ng-content select=\"[actions]\" />\n</div>\n", styles: [":host{display:flex;align-items:center;gap:var(--kf-space-md);padding:var(--kf-space-md);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);background:var(--kf-color-surface);font-family:var(--kf-font-body)}.kf-vehicle-header__identity{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--kf-space-sm);flex:1 1 auto;min-width:0}.kf-vehicle-header__unit{font-family:var(--kf-font-title);font-size:1.125rem;font-weight:700;color:var(--kf-color-ink)}.kf-vehicle-header__model{padding:2px var(--kf-space-sm);border-radius:var(--kf-radius-xs);background:var(--kf-color-border)}.kf-vehicle-header__vin{font-family:var(--kf-font-mono);font-size:.8125rem;color:var(--kf-color-text-muted)}.kf-vehicle-header__status{flex:none;display:inline-flex;align-items:center;gap:var(--kf-space-xs);padding:var(--kf-space-xs) var(--kf-space-sm);border-radius:var(--kf-radius-pill);font-size:.75rem;font-weight:600;line-height:1;white-space:nowrap}.kf-vehicle-header__status--ok{background:var(--kf-color-success-soft);color:var(--kf-color-success)}.kf-vehicle-header__status--warn{background:var(--kf-color-warning-soft);color:var(--kf-color-warning)}.kf-vehicle-header__status--crit{background:var(--kf-color-error-soft);color:var(--kf-color-error)}.kf-vehicle-header__actions{flex:none;display:flex;align-items:center;gap:var(--kf-space-sm)}\n"] }]
|
|
427
|
+
}], propDecorators: { unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: true }] }], model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: false }] }], vin: [{ type: i0.Input, args: [{ isSignal: true, alias: "vin", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }] } });
|
|
428
|
+
|
|
429
|
+
/*
|
|
430
|
+
* Public API Surface of ui/business — entry point secundario, `@kaufmann-lab/ui/business`.
|
|
431
|
+
*
|
|
432
|
+
* Componentes que modelan un concepto de negocio real de Kaufmann (una
|
|
433
|
+
* cotización, una reserva de taller, un vehículo de flota, la empresa activa
|
|
434
|
+
* de una sesión), no una preocupación de UI transversal — ver
|
|
435
|
+
* `ENTRY-POINTS.md` en la raíz del repo para el criterio.
|
|
436
|
+
*
|
|
437
|
+
* Consume sus piezas base (`Icon`, `Text`, `Select`, `DataTable`, etc.) desde
|
|
438
|
+
* `'@kaufmann-lab/ui/core'` y los helpers desde `'@kaufmann-lab/ui/shared'`,
|
|
439
|
+
* nunca desde la raíz `'@kaufmann-lab/ui'` (agregador que depende de todos
|
|
440
|
+
* los entry points — importarla desde acá cerraría un ciclo) ni por ruta
|
|
441
|
+
* relativa cruzando la frontera entre entry points (ng-packagr calcula un
|
|
442
|
+
* `rootDir` propio por entry point y rechaza archivos fuente fuera de él).
|
|
443
|
+
*
|
|
444
|
+
* La raíz `'@kaufmann-lab/ui'` los reexporta, así que
|
|
445
|
+
* `import { QuotePicker } from '@kaufmann-lab/ui'` sigue funcionando igual.
|
|
446
|
+
*/
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Generated bundle index. Do not edit.
|
|
450
|
+
*/
|
|
451
|
+
|
|
452
|
+
export { CompanySelector, QuotePicker, ReserveBlock, VehicleHeader };
|
|
453
|
+
//# sourceMappingURL=kaufmann-lab-ui-business.mjs.map
|