@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaufmann-lab/ui",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Shared Angular component library for the Kaufmann frontends",
5
5
  "license": "MIT",
6
6
  "author": "Kaufmann Lab",
@@ -1,6 +1,6 @@
1
1
  import * as _angular_core from '@angular/core';
2
2
  import { TemplateRef } from '@angular/core';
3
- import { DataTableColumn, IconName } from '@kaufmann-lab/ui/core';
3
+ import { DataTableColumn, IconName, ContentStateKind, FilterChipOption, SelectOption } from '@kaufmann-lab/ui/core';
4
4
  export { DataTableColumn } from '@kaufmann-lab/ui/core';
5
5
 
6
6
  /** Metadato clave mostrado junto al selector (RUC, identificación, sucursal). */
@@ -60,6 +60,19 @@ declare class CompanySelector {
60
60
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<CompanySelector, "kf-company-selector", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "companies": { "alias": "companies"; "required": true; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "meta": { "alias": "meta"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
61
61
  }
62
62
 
63
+ /**
64
+ * Alias del mismo `DataTableColumn`, para leer la API del picker sin ir a la
65
+ * tabla. En `modalLayout="list"` usa: `cardRole` (`title` → primera columna
66
+ * en mono; el primer campo sin rol → columna flexible con elipsis; `status`
67
+ * → última columna, hasta 13.75rem salvo `maxWidth`), `width` (ancho fijo de
68
+ * la pista), `maxWidth`, `align`/`right` y `truncate` (elipsis + `title`).
69
+ */
70
+ type QuotePickerColumn = DataTableColumn;
71
+ /** Sustantivo de los ítems para el conteo del encabezado ("86 cotizaciones"). */
72
+ interface QuotePickerItemLabels {
73
+ singular: string;
74
+ plural: string;
75
+ }
63
76
  /** Chip de filtro genérico con contador — nunca hardcodeado a un dominio
64
77
  * concreto (ej. estado de una cotización); el consumidor decide qué
65
78
  * representa cada `value` y cómo mapea sus items contra él vía `predicate`.
@@ -121,10 +134,8 @@ interface QuotePickerSortOption {
121
134
  * este modo necesita. Compone también `kf-data-table` para la tabla
122
135
  * completa (columnas configurables, fade de overflow horizontal ya
123
136
  * resuelto ahí) y `kf-pagination` para paginar — ninguno de los tres se
124
- * reimplementa. Agrega un dropdown propio de "Ordenar por" (calibrado
125
- * visualmente contra el trigger de `kf-select`, pero compacto: `kf-select`
126
- * fuerza un ancho fijo de 240px pensado para formularios, no para una
127
- * barra de herramientas de tabla — ver JSDoc de `sortOpen` más abajo).
137
+ * reimplementa, igual que el buscador (`kf-input`), el orden (`kf-select`)
138
+ * y los chips (`kf-filter-chips`).
128
139
  *
129
140
  * En mobile, `kf-data-table[cardable]` con `defaultView` forzado a
130
141
  * `'cards'` decide la vista (mismo criterio que `matchMediaSignal` ya
@@ -132,12 +143,70 @@ interface QuotePickerSortOption {
132
143
  * no tiene sentido ofrecer un toggle tabla/cards que en mobile daría una
133
144
  * tabla angosta con scroll horizontal.
134
145
  *
135
- * DEUDA TÉCNICA CONOCIDA (confirmada con el usuario, ver CHANGELOG.md): el
136
- * diseño validado con mockups pedía que en mobile el modal ocupe el 100%
137
- * de la pantalla (botón "atrás", sin overlay centrado) — `kf-modal` no
138
- * tiene ese modo, así que hoy en mobile se ve como un diálogo centrado
139
- * angosto (`width: min(480px, ...)`). Es funcional (cards legibles,
140
- * búsqueda, chips, paginación), solo visualmente distinto del mockup.
146
+ * El modal mide `min(modalWidth, 100vw - 32px)` (default 1040px) con alto
147
+ * máximo de 85vh: encabezado (título + conteo "12 de 86 cotizaciones" en
148
+ * región `aria-live`) y filtros FIJOS arriba, solo la lista se desplaza. En
149
+ * ≤640px de viewport ocupa toda la pantalla (`fullscreenOnMobile` de
150
+ * `kf-modal`). Filtros: fila 1 = buscador (`kf-input`, crece, mínimo 240px)
151
+ * + orden (`kf-select` estándar); fila 2 = `kf-filter-chips` con contador y
152
+ * un chip "Todas" (`allChipLabel`) — en varias líneas en escritorio, una
153
+ * sola línea con scroll horizontal en mobile. Elegir un ítem (o el actual)
154
+ * cierra el modal.
155
+ *
156
+ * ## Trigger (variante `inline`)
157
+ * - `triggerSize="default"` (histórico): tarjeta a ancho completo con
158
+ * miniatura y resumen.
159
+ * - `triggerSize="compact"`: control de barra de herramientas de alto FIJO
160
+ * (`--kf-quote-picker-trigger-height`, 48px), sin miniatura: línea 1 =
161
+ * título en mono + estado (columnas `status`), línea 2 = primer campo con
162
+ * elipsis, y caret. Ancho flexible (`min-width: 0`). Su nombre accesible
163
+ * es el texto visible con un prefijo: "Cambiar cotización. Actual: <id>,
164
+ * <estado>, <modelo>".
165
+ * - `inlinePanel` (default `true`): con `false`, el trigger abre DIRECTO el
166
+ * modal del picker (`aria-haspopup="dialog"`), sin panel inline — el mismo
167
+ * modal que la variante `modal`, así todas las pantallas comparten un
168
+ * único selector.
169
+ *
170
+ * Con `loading` el trigger compacto muestra un esqueleto del MISMO alto
171
+ * (región `role="status"` con `loadingLabel`), así la barra no salta.
172
+ *
173
+ * <kf-quote-picker [items]="quotes()" [columns]="columns" [idKey]="idKey"
174
+ * triggerSize="compact" [inlinePanel]="false" modalLayout="list"
175
+ * [selectedId]="id()" [chips]="chips()" [sortOptions]="sortOptions"
176
+ * [itemLabels]="{ singular: 'cotización', plural: 'cotizaciones' }"
177
+ * (selectedIdChange)="go($event)" />
178
+ *
179
+ * ## `modalLayout` (modal de la variante `modal` y el de `inlinePanel` en false)
180
+ * - `'table'` (default, comportamiento histórico): `kf-data-table` paginada
181
+ * con toggle Tabla/Tarjetas. La fila/tarjeta de `selectedId` lleva
182
+ * `aria-current="true"` y fondo `primary-soft` (`currentRowKey`).
183
+ * - `'list'`: lista de una línea por ítem con COLUMNAS ALINEADAS (CSS grid
184
+ * + `subgrid`: cada pista mide lo mismo en todas las filas), sin paginar
185
+ * ni toggle; se desplaza el cuerpo del modal. Orden de columnas: título
186
+ * (`cardRole: 'title'`, mono) | primer campo (flexible, una línea con
187
+ * elipsis y `title`) | resto de campos (`width` o su ancho de contenido;
188
+ * `align: 'end'` para montos, cifras tabulares) | `status` (hasta
189
+ * 13.75rem; el consumidor pasa `kf-badge truncate` con `title`) | check
190
+ * de la fila actual. Cabecera de columnas discreta y fija al desplazar
191
+ * (`listHeader`). Filas de 52px como mínimo; la de `selectedId` lleva
192
+ * `aria-current="true"`, fondo `primary-soft` y un check, y al abrir se
193
+ * desplaza a la vista. Con la lista de hasta 560px de ancho (container
194
+ * query, no viewport): 2 líneas (título + estado / campos), con el primer
195
+ * campo en hasta 2 líneas.
196
+ *
197
+ * Límite conocido: sin virtualización. Cada fila es un `<button>` en el
198
+ * DOM (búsqueda, foco y `aria-current` funcionan sin trucos); se probó
199
+ * fluido con 120 filas y sigue usable con unos cientos. Para miles,
200
+ * filtrar en el servidor antes de pasar `items`.
201
+ *
202
+ * En la variante modal, además: orden con `kf-select` estándar (ícono
203
+ * `sort`, "Ordenar por"), chips con `aria-pressed`, buscador con
204
+ * `searchAriaLabel` y foco inicial (`initialFocus` de `kf-modal`), conteo
205
+ * en una región `aria-live="polite"` (`itemLabels`), y estados
206
+ * `loading` (spinner con `loadingLabel`) y `error` (mensaje + botón
207
+ * "Reintentar" que emite `retry`) vía `kf-content-state`. El vacío "Sin
208
+ * resultados para esta búsqueda" solo aparece sin carga ni error. Escape
209
+ * con el panel del `kf-select` abierto cierra solo el select.
141
210
  *
142
211
  * <kf-quote-picker
143
212
  * [items]="quotes()"
@@ -185,7 +254,34 @@ declare class QuotePicker<T = unknown> {
185
254
  * consume (cotización, vehículo, cliente...), así que no hardcodea un
186
255
  * ícono fijo. Default `'file'` solo por no dejar el input sin valor. */
187
256
  readonly placeholderIcon: _angular_core.InputSignal<IconName>;
257
+ /** Disposición de la variante `modal`. Ver JSDoc de la clase. */
258
+ readonly modalLayout: _angular_core.InputSignal<"list" | "table">;
259
+ /** Nombre accesible del buscador. Default: `searchPlaceholder`. */
260
+ readonly searchAriaLabel: _angular_core.InputSignal<string | undefined>;
261
+ /** Estado de carga: spinner con `loadingLabel` en el modal y, con
262
+ * `triggerSize="compact"`, un esqueleto del trigger del mismo alto. */
263
+ readonly loading: _angular_core.InputSignal<boolean>;
264
+ /** Texto del estado de carga. */
265
+ readonly loadingLabel: _angular_core.InputSignal<string>;
266
+ /** Mensaje de error de la variante `modal`; con valor muestra el estado de error y "Reintentar". */
267
+ readonly error: _angular_core.InputSignal<string | null>;
268
+ /** Tamaño del trigger de la variante `inline`. Ver JSDoc de la clase. */
269
+ readonly triggerSize: _angular_core.InputSignal<"default" | "compact">;
270
+ /** Con `false`, el trigger abre directo el modal (sin panel inline). */
271
+ readonly inlinePanel: _angular_core.InputSignalWithTransform<boolean, unknown>;
272
+ /** Ancho máximo del modal (se topa con el viewport menos 32px). */
273
+ readonly modalWidth: _angular_core.InputSignal<string>;
274
+ /** Sustantivo del conteo del encabezado. Default "resultado/resultados". */
275
+ readonly itemLabels: _angular_core.InputSignal<QuotePickerItemLabels>;
276
+ /** Chip inicial "sin filtro" con el total. Vacío (`''`) lo oculta. */
277
+ readonly allChipLabel: _angular_core.InputSignal<string>;
278
+ /** Nombre accesible del grupo de chips del modal. */
279
+ readonly chipsAriaLabel: _angular_core.InputSignal<string>;
280
+ /** Cabecera de columnas en `modalLayout="list"` (solo visual). */
281
+ readonly listHeader: _angular_core.InputSignalWithTransform<boolean, unknown>;
188
282
  readonly selectedIdChange: _angular_core.OutputEmitterRef<string>;
283
+ /** "Reintentar" del estado de error. */
284
+ readonly retry: _angular_core.OutputEmitterRef<void>;
189
285
  /** Disparado al superar `expandThreshold` en modo inline; el consumidor decide pasar a `modal`. */
190
286
  readonly expandToModal: _angular_core.OutputEmitterRef<void>;
191
287
  protected readonly searchQuery: _angular_core.WritableSignal<string>;
@@ -200,8 +296,13 @@ declare class QuotePicker<T = unknown> {
200
296
  * vs. cards apiladas dentro del modal.
201
297
  */
202
298
  protected readonly isMobile: _angular_core.Signal<boolean>;
299
+ /** Mismo corte que el modal a pantalla completa: chips en una sola línea. */
300
+ protected readonly isNarrowViewport: _angular_core.Signal<boolean>;
203
301
  private readonly destroyRef;
204
302
  private readonly previewScrollEl;
303
+ private readonly modalListEl;
304
+ /** Para desplazar la fila actual a la vista UNA vez por apertura, no en cada render. */
305
+ private scrolledToCurrent;
205
306
  /** Mismo mecanismo de fade que `kf-data-table` — ver comentario en QuotePicker.css. */
206
307
  protected readonly canFadeBottom: _angular_core.WritableSignal<boolean>;
207
308
  readonly selectedItem: _angular_core.Signal<T | undefined>;
@@ -219,13 +320,46 @@ declare class QuotePicker<T = unknown> {
219
320
  protected readonly previewItems: _angular_core.Signal<T[]>;
220
321
  protected readonly pages: _angular_core.Signal<number>;
221
322
  protected readonly pagedItems: _angular_core.Signal<T[]>;
323
+ protected readonly resolvedSearchAriaLabel: _angular_core.Signal<string>;
324
+ /** Estado del cuerpo del modal para `kf-content-state`. */
325
+ protected readonly modalState: _angular_core.Signal<ContentStateKind>;
326
+ /**
327
+ * Conteo del encabezado, en región `aria-live`: "86 cotizaciones" sin
328
+ * filtros, "12 de 86 cotizaciones" con búsqueda o chip. Vacío mientras
329
+ * carga o con error.
330
+ */
331
+ protected readonly resultsLabel: _angular_core.Signal<string>;
332
+ /** Chips del modal en el formato de `kf-filter-chips`, con "Todas" al inicio. */
333
+ protected readonly chipOptions: _angular_core.Signal<FilterChipOption[]>;
334
+ /**
335
+ * Pistas de la grilla de `modalLayout="list"`, en el orden visual: título,
336
+ * primer campo (flexible), resto, estados y el check. Todas las filas
337
+ * comparten estas pistas vía `subgrid`, por eso las columnas alinean.
338
+ */
339
+ protected readonly listGridColumns: _angular_core.Signal<string>;
340
+ protected readonly sortSelectOptions: _angular_core.Signal<SelectOption[]>;
341
+ protected readonly statusColumns: _angular_core.Signal<DataTableColumn[]>;
342
+ /** Primer campo (ej. modelo): ocupa el espacio libre de la fila en `modalLayout="list"`. */
343
+ protected readonly primaryFieldColumn: _angular_core.Signal<DataTableColumn>;
344
+ protected readonly restFieldColumns: _angular_core.Signal<DataTableColumn[]>;
345
+ /** Alineada al final: `align: 'end'` o el histórico `right: true`. */
346
+ protected isEndAligned(column: DataTableColumn): boolean;
347
+ /** `title` con el valor completo de una celda que puede cortarse con elipsis. */
348
+ protected cellTitle(item: T, column: DataTableColumn): string | null;
349
+ protected isSelected(item: T): boolean;
222
350
  protected readonly sortLabel: _angular_core.Signal<string>;
223
351
  constructor();
224
352
  private updateFadeState;
225
353
  private cellText;
226
354
  toggleExpandedPanel(): void;
355
+ /** Click en el trigger: alterna el panel inline o, sin él, abre el modal. */
356
+ protected onTriggerClick(): void;
227
357
  onSearchInput(event: Event): void;
358
+ /** Buscador del modal (`kf-input` con `ngModel`), incluido el botón "×". */
359
+ protected onSearchChange(value: string): void;
228
360
  selectChip(value: string): void;
361
+ /** Chips del modal: `''` es "Todas" (sin filtro). */
362
+ protected onChipChange(value: string): void;
229
363
  select(item: T): void;
230
364
  onItemKeydown(event: KeyboardEvent, item: T): void;
231
365
  onSeeAll(): void;
@@ -233,7 +367,10 @@ declare class QuotePicker<T = unknown> {
233
367
  row: T;
234
368
  index: number;
235
369
  }): void;
370
+ /** @deprecated El orden de la variante modal ahora es un `kf-select`; este método ya no lo usa la plantilla. */
236
371
  toggleSort(): void;
372
+ protected onSortChange(value: string): void;
373
+ /** Alterna el orden (elegir el vigente lo quita). Se conserva por compatibilidad. */
237
374
  setSort(value: string): void;
238
375
  trackItem(item: T, index: number): string | number;
239
376
  /** Arrow function como campo (no un método) para que `[rowKey]` reciba
@@ -246,7 +383,7 @@ declare class QuotePicker<T = unknown> {
246
383
  protected readonly titleColumn: _angular_core.Signal<DataTableColumn | undefined>;
247
384
  protected readonly fieldColumns: _angular_core.Signal<DataTableColumn[]>;
248
385
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<QuotePicker<any>, never>;
249
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<QuotePicker<any>, "kf-quote-picker", never, { "items": { "alias": "items"; "required": true; "isSignal": true; }; "columns": { "alias": "columns"; "required": true; "isSignal": true; }; "selectedId": { "alias": "selectedId"; "required": false; "isSignal": true; }; "idKey": { "alias": "idKey"; "required": true; "isSignal": true; }; "cellTemplates": { "alias": "cellTemplates"; "required": false; "isSignal": true; }; "chips": { "alias": "chips"; "required": false; "isSignal": true; }; "sortOptions": { "alias": "sortOptions"; "required": false; "isSignal": true; }; "sort": { "alias": "sort"; "required": false; "isSignal": true; }; "expandThreshold": { "alias": "expandThreshold"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "open": { "alias": "open"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "searchPlaceholder": { "alias": "searchPlaceholder"; "required": false; "isSignal": true; }; "placeholderIcon": { "alias": "placeholderIcon"; "required": false; "isSignal": true; }; }, { "sort": "sortChange"; "open": "openChange"; "selectedIdChange": "selectedIdChange"; "expandToModal": "expandToModal"; }, never, never, true, never>;
386
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<QuotePicker<any>, "kf-quote-picker", never, { "items": { "alias": "items"; "required": true; "isSignal": true; }; "columns": { "alias": "columns"; "required": true; "isSignal": true; }; "selectedId": { "alias": "selectedId"; "required": false; "isSignal": true; }; "idKey": { "alias": "idKey"; "required": true; "isSignal": true; }; "cellTemplates": { "alias": "cellTemplates"; "required": false; "isSignal": true; }; "chips": { "alias": "chips"; "required": false; "isSignal": true; }; "sortOptions": { "alias": "sortOptions"; "required": false; "isSignal": true; }; "sort": { "alias": "sort"; "required": false; "isSignal": true; }; "expandThreshold": { "alias": "expandThreshold"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "open": { "alias": "open"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "searchPlaceholder": { "alias": "searchPlaceholder"; "required": false; "isSignal": true; }; "placeholderIcon": { "alias": "placeholderIcon"; "required": false; "isSignal": true; }; "modalLayout": { "alias": "modalLayout"; "required": false; "isSignal": true; }; "searchAriaLabel": { "alias": "searchAriaLabel"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "loadingLabel": { "alias": "loadingLabel"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "triggerSize": { "alias": "triggerSize"; "required": false; "isSignal": true; }; "inlinePanel": { "alias": "inlinePanel"; "required": false; "isSignal": true; }; "modalWidth": { "alias": "modalWidth"; "required": false; "isSignal": true; }; "itemLabels": { "alias": "itemLabels"; "required": false; "isSignal": true; }; "allChipLabel": { "alias": "allChipLabel"; "required": false; "isSignal": true; }; "chipsAriaLabel": { "alias": "chipsAriaLabel"; "required": false; "isSignal": true; }; "listHeader": { "alias": "listHeader"; "required": false; "isSignal": true; }; }, { "sort": "sortChange"; "open": "openChange"; "selectedIdChange": "selectedIdChange"; "retry": "retry"; "expandToModal": "expandToModal"; }, never, never, true, never>;
250
387
  }
251
388
 
252
389
  /**
@@ -334,4 +471,4 @@ declare class VehicleHeader {
334
471
  }
335
472
 
336
473
  export { CompanySelector, QuotePicker, ReserveBlock, VehicleHeader };
337
- export type { CompanySelectorMeta, CompanySelectorOption, QuotePickerChip, QuotePickerSortOption, VehicleHeaderStatus };
474
+ export type { CompanySelectorMeta, CompanySelectorOption, QuotePickerChip, QuotePickerColumn, QuotePickerItemLabels, QuotePickerSortOption, VehicleHeaderStatus };