@kaufmann-lab/ui 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, inject, computed, effect, Component, booleanAttribute, model, output, numberAttribute, contentChildren, signal, DestroyRef, viewChild, forwardRef, ViewContainerRef, HostListener, TemplateRef, contentChild, ElementRef, Directive, ChangeDetectionStrategy, afterRenderEffect, afterNextRender, Injectable } from '@angular/core';
2
+ import { input, inject, computed, effect, Component, booleanAttribute, model, output, numberAttribute, contentChildren, signal, DestroyRef, viewChild, forwardRef, ViewContainerRef, afterRenderEffect, HostListener, TemplateRef, contentChild, ElementRef, Directive, ChangeDetectionStrategy, Injector, Input as Input$1, afterNextRender, Injectable } from '@angular/core';
3
3
  import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
4
4
  import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
5
5
  import { Overlay } from '@angular/cdk/overlay';
6
- import { TemplatePortal } from '@angular/cdk/portal';
6
+ import { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
7
+ import { FocusTrapFactory } from '@angular/cdk/a11y';
8
+ import { Subscription } from 'rxjs';
7
9
 
8
10
  /** Símbolo de trazo "activity" (grilla 24×24) — ver `icons/index.ts` para el registro completo. */
9
11
  const activity = {
@@ -328,6 +330,15 @@ const history = {
328
330
  `,
329
331
  };
330
332
 
333
+ /** Símbolo de trazo "home" (grilla 24×24) — ver `icons/index.ts` para el registro completo. */
334
+ const home = {
335
+ viewBox: '0 0 24 24',
336
+ markup: `
337
+ <path d="M3 10.5 12 3l9 7.5" />
338
+ <path d="M5 9.5V20a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1V9.5" />
339
+ `,
340
+ };
341
+
331
342
  /** Símbolo de trazo "inbox" (grilla 24×24) — ver `icons/index.ts` para el registro completo. */
332
343
  const inbox = {
333
344
  viewBox: '0 0 24 24',
@@ -360,6 +371,16 @@ const list = {
360
371
  `,
361
372
  };
362
373
 
374
+ /** Símbolo de trazo "log-out" (grilla 24×24) — ver `icons/index.ts` para el registro completo. */
375
+ const logOut = {
376
+ viewBox: '0 0 24 24',
377
+ markup: `
378
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
379
+ <path d="M16 17l5-5-5-5" />
380
+ <path d="M21 12H9" />
381
+ `,
382
+ };
383
+
363
384
  /** Símbolo de trazo "map" (grilla 24×24) — ver `icons/index.ts` para el registro completo. */
364
385
  const map = {
365
386
  viewBox: '0 0 24 24',
@@ -636,9 +657,11 @@ const ICON_DEFINITIONS = {
636
657
  gift: gift,
637
658
  grid: grid,
638
659
  history: history,
660
+ home: home,
639
661
  inbox: inbox,
640
662
  layers: layers,
641
663
  list: list,
664
+ 'log-out': logOut,
642
665
  map: map,
643
666
  'map-pin': mapPin,
644
667
  menu: menu,
@@ -713,7 +736,7 @@ function buildIconSprite() {
713
736
  * nunca se inyecta. Comprobar el propio DOM es la fuente de verdad real. */
714
737
  const SPRITE_ROOT_ID = 'kf-icon-sprite-root';
715
738
  /**
716
- * Ícono de trazo del design system — 60 glyphs (59 de la página 5 + `eye-off`), grilla 24×24,
739
+ * Ícono de trazo del design system — 61 glyphs (59 de la página 5 + `eye-off` y `log-out`), grilla 24×24,
717
740
  * grosor de trazo 1.9px. Referencia un sprite SVG embebido (ver
718
741
  * `icon-sprite.ts`) vía `<use>`, así que no hay peso de red adicional por
719
742
  * cada instancia: el sprite se inyecta al DOM una única vez, la primera
@@ -1138,6 +1161,139 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
1138
1161
  `, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-alert{display:flex;align-items:flex-start;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border:1px solid transparent;border-radius:var(--kf-radius-sm);font-size:.9rem}.kf-alert--info{background:var(--kf-color-info-soft);border-color:var(--kf-color-info);color:var(--kf-color-info-strong)}.kf-alert--ok{background:var(--kf-color-success-soft);border-color:var(--kf-color-success);color:var(--kf-color-success-strong)}.kf-alert--warn{background:var(--kf-color-warning-soft);border-color:var(--kf-color-warning);color:var(--kf-color-warning-strong)}.kf-alert--error{background:var(--kf-color-error-soft);border-color:var(--kf-color-error);color:var(--kf-color-error-strong)}.kf-alert__icon,.kf-alert__close svg{flex:none;width:1.15em;height:1.15em;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}.kf-alert__body{flex:1;min-width:0}.kf-alert__title{margin:0;font-family:var(--kf-font-title);font-weight:700}.kf-alert__message{color:inherit}.kf-alert__close{flex:none;display:inline-flex;padding:2px;border:none;background:transparent;color:inherit;border-radius:var(--kf-radius-xs);cursor:pointer;opacity:.7}.kf-alert__close:hover{opacity:1}.kf-alert__close:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}\n"] }]
1139
1162
  }], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }] } });
1140
1163
 
1164
+ /**
1165
+ * Layout a pantalla completa para cualquier pantalla de autenticación
1166
+ * (login, recuperar contraseña, confirmación de envío) — split marca a la
1167
+ * izquierda / contenido libre a la derecha en desktop, formulario a
1168
+ * pantalla completa en mobile (el panel de marca completo se oculta, no se
1169
+ * apila arriba: en una pantalla angosta el usuario vino a loguearse, no a
1170
+ * leer la tagline). Un logo compacto (o wordmark de respaldo) sí se
1171
+ * mantiene visible arriba del formulario en mobile — el usuario nunca
1172
+ * pierde de vista qué portal está usando, mismo criterio que ya sigue el
1173
+ * resto del ecosistema (ver la topbar mobile de `css/otd.css`, que muestra
1174
+ * el logo aunque el sidebar completo se oculte).
1175
+ *
1176
+ * Este componente resuelve el "cascarón" — nunca el formulario en sí. Cada
1177
+ * portal arma su propio `<form>` con los componentes de la librería
1178
+ * (`kf-input`, `kf-checkbox`, `kf-button`, `kf-alert`) y lo proyecta en el
1179
+ * slot por defecto. Es la misma separación de responsabilidad que ya usa
1180
+ * `kf-card`: un componente da la estructura visual, el consumidor decide
1181
+ * qué va adentro.
1182
+ *
1183
+ * Antes de este componente, cada portal (LATAM primero, ahora OTD) hubiera
1184
+ * tenido que reimplementar este mismo split desde cero — la librería
1185
+ * había pospuesto esta abstracción a propósito hasta tener un segundo
1186
+ * consumidor real que confirmara qué parte es realmente compartida (el
1187
+ * cascarón) y qué parte es de cada portal (el formulario). OTD es ese
1188
+ * segundo consumidor.
1189
+ *
1190
+ * Ningún input tiene valor por defecto propio de ningún portal — `brand`
1191
+ * es obligatorio (`input.required`) y el resto de props de marca son
1192
+ * opcionales sin fallback hardcodeado, precisamente para que cada
1193
+ * consumidor (LATAM, OTD, Star, o el ERP de Asistencia en Ruta con sus
1194
+ * propios temas) traiga su propio logo/tagline sin que la librería asuma
1195
+ * cuál.
1196
+ *
1197
+ * <kf-auth-layout
1198
+ * brand="Mi Portal"
1199
+ * brandLogoSrc="images/mi-portal-logo-white.png"
1200
+ * brandLogoMobileSrc="images/mi-portal-logo-black.png"
1201
+ * brandTagline="Frase corta de marca, propia de cada portal."
1202
+ * >
1203
+ * <form (ngSubmit)="onSubmit()">
1204
+ * <kf-input label="Correo" type="email" />
1205
+ * <kf-input label="Contraseña" type="password" toggleable />
1206
+ * <kf-button type="submit">Ingresar</kf-button>
1207
+ * </form>
1208
+ * </kf-auth-layout>
1209
+ */
1210
+ class AuthLayout {
1211
+ /** Nombre de marca/portal, usado como wordmark de respaldo si no hay ningún logo, y como `alt` de ambos logos. */
1212
+ brand = input.required(/* @ts-ignore */
1213
+ ...(ngDevMode ? [{ debugName: "brand" }] : /* istanbul ignore next */ []));
1214
+ /** Logo en blanco para el panel oscuro de desktop — si no se pasa, se muestra `brand()` como wordmark tipográfico. */
1215
+ brandLogoSrc = input(/* @ts-ignore */
1216
+ ...(ngDevMode ? [undefined, { debugName: "brandLogoSrc" }] : /* istanbul ignore next */ []));
1217
+ /** Frase corta bajo el logo — opcional, cada portal decide su propio texto de marca. */
1218
+ brandTagline = input(/* @ts-ignore */
1219
+ ...(ngDevMode ? [undefined, { debugName: "brandTagline" }] : /* istanbul ignore next */ []));
1220
+ /**
1221
+ * Logo compacto para mobile, sobre el fondo claro del panel de contenido
1222
+ * — normalmente la variante oscura/negra del mismo logo, ya que
1223
+ * `brandLogoSrc` suele ser la variante blanca pensada para el gradiente
1224
+ * oscuro. Si no se pasa, reutiliza `brandLogoSrc`; si tampoco hay logo en
1225
+ * absoluto, cae al wordmark tipográfico igual que en desktop.
1226
+ */
1227
+ brandLogoMobileSrc = input(/* @ts-ignore */
1228
+ ...(ngDevMode ? [undefined, { debugName: "brandLogoMobileSrc" }] : /* istanbul ignore next */ []));
1229
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: AuthLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
1230
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: AuthLayout, isStandalone: true, selector: "kf-auth-layout", inputs: { brand: { classPropertyName: "brand", publicName: "brand", isSignal: true, isRequired: true, transformFunction: null }, brandLogoSrc: { classPropertyName: "brandLogoSrc", publicName: "brandLogoSrc", isSignal: true, isRequired: false, transformFunction: null }, brandTagline: { classPropertyName: "brandTagline", publicName: "brandTagline", isSignal: true, isRequired: false, transformFunction: null }, brandLogoMobileSrc: { classPropertyName: "brandLogoMobileSrc", publicName: "brandLogoMobileSrc", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1231
+ <aside class="kf-auth-layout__brand">
1232
+ @if (brandLogoSrc()) {
1233
+ <img class="kf-auth-layout__logo" [src]="brandLogoSrc()" [alt]="brand()" />
1234
+ } @else {
1235
+ <span class="kf-auth-layout__wordmark">{{ brand() }}</span>
1236
+ }
1237
+ @if (brandTagline()) {
1238
+ <p class="kf-auth-layout__tagline">{{ brandTagline() }}</p>
1239
+ }
1240
+ </aside>
1241
+
1242
+ <main class="kf-auth-layout__content">
1243
+ <div class="kf-auth-layout__panel">
1244
+ <div class="kf-auth-layout__mobile-brand">
1245
+ @if (brandLogoMobileSrc() || brandLogoSrc()) {
1246
+ <img
1247
+ class="kf-auth-layout__logo kf-auth-layout__logo--mobile"
1248
+ [src]="brandLogoMobileSrc() || brandLogoSrc()"
1249
+ [alt]="brand()"
1250
+ />
1251
+ } @else {
1252
+ <span class="kf-auth-layout__wordmark kf-auth-layout__wordmark--mobile">{{
1253
+ brand()
1254
+ }}</span>
1255
+ }
1256
+ </div>
1257
+ <ng-content />
1258
+ </div>
1259
+ </main>
1260
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);min-height:100dvh;background:var(--kf-color-surface)}.kf-auth-layout__brand{display:flex;flex-direction:column;justify-content:center;gap:var(--kf-space-md);padding:var(--kf-space-xl);background:linear-gradient(190deg,var(--kf-color-primary-active) 0%,var(--kf-color-tertiary-deep) 100%);color:var(--kf-color-white)}.kf-auth-layout__logo{height:1.75rem;width:fit-content}.kf-auth-layout__wordmark{font-family:var(--kf-font-title);font-weight:var(--kf-font-weight-title);font-size:var(--kf-font-size-title-lg);letter-spacing:-.01em}.kf-auth-layout__tagline{max-width:34ch;margin:0;font-family:var(--kf-font-body);font-size:var(--kf-font-size-body);color:#ffffffb8}.kf-auth-layout__content{display:flex;align-items:center;justify-content:center;padding:var(--kf-space-xl)}.kf-auth-layout__panel{width:100%;max-width:360px;display:flex;flex-direction:column;gap:var(--kf-space-lg)}.kf-auth-layout__mobile-brand{display:none}.kf-auth-layout__logo--mobile{height:1.5rem}.kf-auth-layout__wordmark--mobile{color:var(--kf-color-ink)}@media(max-width:900px){:host{grid-template-columns:1fr}.kf-auth-layout__brand{display:none}.kf-auth-layout__content{padding:var(--kf-space-lg)}.kf-auth-layout__mobile-brand{display:block;margin-bottom:var(--kf-space-md)}}@media(max-width:480px){.kf-auth-layout__panel{max-width:none}}\n"] });
1261
+ }
1262
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: AuthLayout, decorators: [{
1263
+ type: Component,
1264
+ args: [{ selector: 'kf-auth-layout', template: `
1265
+ <aside class="kf-auth-layout__brand">
1266
+ @if (brandLogoSrc()) {
1267
+ <img class="kf-auth-layout__logo" [src]="brandLogoSrc()" [alt]="brand()" />
1268
+ } @else {
1269
+ <span class="kf-auth-layout__wordmark">{{ brand() }}</span>
1270
+ }
1271
+ @if (brandTagline()) {
1272
+ <p class="kf-auth-layout__tagline">{{ brandTagline() }}</p>
1273
+ }
1274
+ </aside>
1275
+
1276
+ <main class="kf-auth-layout__content">
1277
+ <div class="kf-auth-layout__panel">
1278
+ <div class="kf-auth-layout__mobile-brand">
1279
+ @if (brandLogoMobileSrc() || brandLogoSrc()) {
1280
+ <img
1281
+ class="kf-auth-layout__logo kf-auth-layout__logo--mobile"
1282
+ [src]="brandLogoMobileSrc() || brandLogoSrc()"
1283
+ [alt]="brand()"
1284
+ />
1285
+ } @else {
1286
+ <span class="kf-auth-layout__wordmark kf-auth-layout__wordmark--mobile">{{
1287
+ brand()
1288
+ }}</span>
1289
+ }
1290
+ </div>
1291
+ <ng-content />
1292
+ </div>
1293
+ </main>
1294
+ `, styles: [":host{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);min-height:100dvh;background:var(--kf-color-surface)}.kf-auth-layout__brand{display:flex;flex-direction:column;justify-content:center;gap:var(--kf-space-md);padding:var(--kf-space-xl);background:linear-gradient(190deg,var(--kf-color-primary-active) 0%,var(--kf-color-tertiary-deep) 100%);color:var(--kf-color-white)}.kf-auth-layout__logo{height:1.75rem;width:fit-content}.kf-auth-layout__wordmark{font-family:var(--kf-font-title);font-weight:var(--kf-font-weight-title);font-size:var(--kf-font-size-title-lg);letter-spacing:-.01em}.kf-auth-layout__tagline{max-width:34ch;margin:0;font-family:var(--kf-font-body);font-size:var(--kf-font-size-body);color:#ffffffb8}.kf-auth-layout__content{display:flex;align-items:center;justify-content:center;padding:var(--kf-space-xl)}.kf-auth-layout__panel{width:100%;max-width:360px;display:flex;flex-direction:column;gap:var(--kf-space-lg)}.kf-auth-layout__mobile-brand{display:none}.kf-auth-layout__logo--mobile{height:1.5rem}.kf-auth-layout__wordmark--mobile{color:var(--kf-color-ink)}@media(max-width:900px){:host{grid-template-columns:1fr}.kf-auth-layout__brand{display:none}.kf-auth-layout__content{padding:var(--kf-space-lg)}.kf-auth-layout__mobile-brand{display:block;margin-bottom:var(--kf-space-md)}}@media(max-width:480px){.kf-auth-layout__panel{max-width:none}}\n"] }]
1295
+ }], propDecorators: { brand: [{ type: i0.Input, args: [{ isSignal: true, alias: "brand", required: true }] }], brandLogoSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandLogoSrc", required: false }] }], brandTagline: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandTagline", required: false }] }], brandLogoMobileSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandLogoMobileSrc", required: false }] }] } });
1296
+
1141
1297
  /**
1142
1298
  * Avatar de usuario: muestra una foto (`src`) o, si no hay foto, las
1143
1299
  * iniciales (`initials`) sobre un fondo con gradiente de marca. `round`
@@ -1423,11 +1579,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
1423
1579
  */
1424
1580
  class CarouselSlide {
1425
1581
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CarouselSlide, deps: [], target: i0.ɵɵFactoryTarget.Component });
1426
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: CarouselSlide, isStandalone: true, selector: "kf-carousel-slide", ngImport: i0, template: `<ng-content />`, isInline: true, styles: [":host{display:block;flex:0 0 100%;width:100%;height:100%}\n"] });
1582
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: CarouselSlide, isStandalone: true, selector: "kf-carousel-slide", ngImport: i0, template: `<ng-content />`, isInline: true, styles: [":host{display:block;flex:0 0 100%;width:100%;height:100%;min-height:0}\n"] });
1427
1583
  }
1428
1584
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CarouselSlide, decorators: [{
1429
1585
  type: Component,
1430
- args: [{ selector: 'kf-carousel-slide', template: `<ng-content />`, styles: [":host{display:block;flex:0 0 100%;width:100%;height:100%}\n"] }]
1586
+ args: [{ selector: 'kf-carousel-slide', template: `<ng-content />`, styles: [":host{display:block;flex:0 0 100%;width:100%;height:100%;min-height:0}\n"] }]
1431
1587
  }] });
1432
1588
  /**
1433
1589
  * Carrusel de slides con flechas, dots y autoplay opcional.
@@ -1858,6 +2014,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
1858
2014
  `, styles: [":host{display:contents}.kf-field-label{font-family:var(--kf-font-title);font-size:.75rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--kf-color-text-muted)}.kf-field-label__required{color:var(--kf-color-error);margin-inline-start:2px}\n"] }]
1859
2015
  }], propDecorators: { for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }] } });
1860
2016
 
2017
+ /**
2018
+ * Normaliza texto para comparación de búsqueda insensible a mayúsculas y
2019
+ * diacríticos — así "cor" encuentre "Córdoba" sin necesidad de tildes.
2020
+ *
2021
+ * Compartida entre `Select` y `MultiSelect` (los dos componentes con
2022
+ * buscador interno auto-activado por `searchThreshold`): antes vivía
2023
+ * duplicada letra por letra en ambos archivos.
2024
+ */
2025
+ function normalizeForSearch(text) {
2026
+ return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
2027
+ }
2028
+
1861
2029
  let nextId$1 = 0;
1862
2030
  /**
1863
2031
  * Dropdown de selección simple con panel propio — NUNCA usa el `<select>`
@@ -1893,21 +2061,48 @@ class Select {
1893
2061
  placeholder = input('Seleccionar', /* @ts-ignore */
1894
2062
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1895
2063
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2064
+ /**
2065
+ * Tri-state: si el consumidor no lo especifica (`undefined`), el buscador
2066
+ * se activa solo cuando `options().length` supera `searchThreshold()`. Si
2067
+ * el consumidor define `true`/`false` explícitamente, ese valor manda por
2068
+ * sobre el umbral automático.
2069
+ */
2070
+ searchable = input(undefined, /* @ts-ignore */
2071
+ ...(ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []));
2072
+ searchThreshold = input(7, /* @ts-ignore */
2073
+ ...(ngDevMode ? [{ debugName: "searchThreshold" }] : /* istanbul ignore next */ []));
1896
2074
  valueChange = output();
1897
2075
  instanceId = `kf-select-${nextId$1++}`;
1898
2076
  triggerId = `${this.instanceId}-trigger`;
1899
2077
  listboxId = `${this.instanceId}-listbox`;
1900
2078
  isOpen = signal(false, /* @ts-ignore */
1901
2079
  ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2080
+ searchQuery = signal('', /* @ts-ignore */
2081
+ ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
2082
+ activeIndex = signal(0, /* @ts-ignore */
2083
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1902
2084
  /** Resolves the union (`string[] | { value; label }[]`) into a single shape for the template. */
1903
2085
  normalizedOptions = computed(() => this.options().map((option) => typeof option === 'string' ? { value: option, label: option } : option), /* @ts-ignore */
1904
2086
  ...(ngDevMode ? [{ debugName: "normalizedOptions" }] : /* istanbul ignore next */ []));
1905
2087
  selectedOption = computed(() => this.normalizedOptions().find((option) => option.value === this.value()), /* @ts-ignore */
1906
2088
  ...(ngDevMode ? [{ debugName: "selectedOption" }] : /* istanbul ignore next */ []));
2089
+ showSearch = computed(() => this.searchable() ?? this.normalizedOptions().length > this.searchThreshold(), /* @ts-ignore */
2090
+ ...(ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []));
2091
+ /** Opciones visibles tras aplicar el filtro de búsqueda (case/acento-insensitive). */
2092
+ filteredOptions = computed(() => {
2093
+ const query = normalizeForSearch(this.searchQuery().trim());
2094
+ if (!query) {
2095
+ return this.normalizedOptions();
2096
+ }
2097
+ return this.normalizedOptions().filter((option) => normalizeForSearch(option.label).includes(query));
2098
+ }, /* @ts-ignore */
2099
+ ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
1907
2100
  triggerRef = viewChild.required('trigger', /* @ts-ignore */
1908
2101
  ...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
1909
2102
  panelTemplate = viewChild.required('panelTemplate', /* @ts-ignore */
1910
2103
  ...(ngDevMode ? [{ debugName: "panelTemplate" }] : /* istanbul ignore next */ []));
2104
+ searchInputRef = viewChild('searchInput', /* @ts-ignore */
2105
+ ...(ngDevMode ? [{ debugName: "searchInputRef" }] : /* istanbul ignore next */ []));
1911
2106
  overlayService = inject(Overlay);
1912
2107
  viewContainerRef = inject(ViewContainerRef);
1913
2108
  destroyRef = inject(DestroyRef);
@@ -1919,6 +2114,17 @@ class Select {
1919
2114
  }
1920
2115
  else {
1921
2116
  this.detach();
2117
+ this.searchQuery.set('');
2118
+ this.activeIndex.set(0);
2119
+ }
2120
+ });
2121
+ // Autofocus del buscador al abrir. afterNextRender() NO puede invocarse
2122
+ // dentro de un effect() (NG0203: pierde el contexto de inyección) — hay
2123
+ // que usar afterRenderEffect(), que sí es reactivo a signals y corre
2124
+ // después de que Angular actualizó el DOM del panel recién adjuntado.
2125
+ afterRenderEffect(() => {
2126
+ if (this.isOpen() && this.showSearch()) {
2127
+ this.searchInputRef()?.nativeElement.focus();
1922
2128
  }
1923
2129
  });
1924
2130
  this.destroyRef.onDestroy(() => this.overlayRef?.dispose());
@@ -1944,6 +2150,30 @@ class Select {
1944
2150
  this.valueChange.emit(option.value);
1945
2151
  }
1946
2152
  }
2153
+ onSearchInput(event) {
2154
+ this.searchQuery.set(event.target.value);
2155
+ // Cada cambio de filtro vuelve a arrancar la navegación desde arriba.
2156
+ this.activeIndex.set(0);
2157
+ }
2158
+ /**
2159
+ * Mueve `activeIndex` sobre las opciones YA FILTRADAS visibles. Clamp
2160
+ * simple (sin wraparound): llegar al final o al principio de la lista se
2161
+ * queda ahí en vez de dar la vuelta — es el comportamiento más predecible
2162
+ * cuando se combina con un buscador que puede reducir la lista a 1 opción.
2163
+ */
2164
+ moveActive(delta) {
2165
+ const count = this.filteredOptions().length;
2166
+ if (count === 0) {
2167
+ return;
2168
+ }
2169
+ this.activeIndex.update((current) => Math.min(Math.max(current + delta, 0), count - 1));
2170
+ }
2171
+ selectActive() {
2172
+ const option = this.filteredOptions()[this.activeIndex()];
2173
+ if (option) {
2174
+ this.select(option);
2175
+ }
2176
+ }
1947
2177
  attach() {
1948
2178
  if (this.overlayRef?.hasAttached()) {
1949
2179
  return;
@@ -1956,7 +2186,21 @@ class Select {
1956
2186
  overlayRef.updateSize({ width: this.triggerRef().nativeElement.getBoundingClientRect().width });
1957
2187
  const portal = new TemplatePortal(this.panelTemplate(), this.viewContainerRef);
1958
2188
  overlayRef.attach(portal);
1959
- overlayRef.outsidePointerEvents().subscribe(() => this.isOpen.set(false));
2189
+ // outsidePointerEvents() dispara en la fase de CAPTURA de `click` sobre
2190
+ // `body` (ver OverlayOutsideClickDispatcher en @angular/cdk), es decir
2191
+ // ANTES de que el evento llegue al trigger y corra (click)="toggle()".
2192
+ // Sin este filtro, un click en el trigger para CERRAR el panel ya abierto
2193
+ // hacía: 1) este subscribe lo cierra primero (el trigger no es parte del
2194
+ // overlay, cuenta como "afuera"), 2) toggle() corre después y lo vuelve a
2195
+ // abrir porque ve isOpen ya en false — un solo click quedaba en no-op y
2196
+ // solo cerraba de verdad un click fuera del trigger.
2197
+ overlayRef.outsidePointerEvents().subscribe((event) => {
2198
+ const target = event.target;
2199
+ if (target && this.triggerRef().nativeElement.contains(target)) {
2200
+ return;
2201
+ }
2202
+ this.isOpen.set(false);
2203
+ });
1960
2204
  overlayRef.keydownEvents().subscribe((event) => {
1961
2205
  if (event.key === 'Escape') {
1962
2206
  this.isOpen.set(false);
@@ -1990,7 +2234,7 @@ class Select {
1990
2234
  return this.overlayRef;
1991
2235
  }
1992
2236
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Select, deps: [], target: i0.ɵɵFactoryTarget.Component });
1993
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: Select, isStandalone: true, selector: "kf-select", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { listeners: { "document:keydown.escape": "onDocumentEscape()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelTemplate", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: `
2237
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: Select, isStandalone: true, selector: "kf-select", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchThreshold: { classPropertyName: "searchThreshold", publicName: "searchThreshold", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { listeners: { "document:keydown.escape": "onDocumentEscape()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelTemplate", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }, { propertyName: "searchInputRef", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
1994
2238
  @if (label()) {
1995
2239
  <kf-field-label [for]="triggerId">{{ label() }}</kf-field-label>
1996
2240
  }
@@ -2016,31 +2260,52 @@ class Select {
2016
2260
  ></span>
2017
2261
  </button>
2018
2262
  <ng-template #panelTemplate>
2019
- <ul
2020
- class="kf-select__panel"
2021
- [id]="listboxId"
2022
- role="listbox"
2023
- [attr.aria-labelledby]="triggerId"
2024
- >
2025
- @for (option of normalizedOptions(); track option.value) {
2026
- <li
2027
- class="kf-select__option"
2028
- [class.kf-select__option--selected]="option.value === value()"
2029
- role="option"
2030
- tabindex="0"
2031
- [attr.aria-selected]="option.value === value()"
2032
- (click)="select(option)"
2033
- (keydown.enter)="select(option)"
2034
- (keydown.space)="$event.preventDefault(); select(option)"
2035
- >
2036
- {{ option.label }}
2037
- </li>
2038
- } @empty {
2039
- <li class="kf-select__empty">Sin opciones disponibles</li>
2263
+ <div class="kf-select__panel">
2264
+ @if (showSearch()) {
2265
+ <input
2266
+ #searchInput
2267
+ type="text"
2268
+ class="kf-select__search"
2269
+ placeholder="Buscar…"
2270
+ [value]="searchQuery()"
2271
+ (input)="onSearchInput($event)"
2272
+ (keydown.arrowdown)="$event.preventDefault(); moveActive(1)"
2273
+ (keydown.arrowup)="$event.preventDefault(); moveActive(-1)"
2274
+ (keydown.enter)="$event.preventDefault(); selectActive()"
2275
+ />
2040
2276
  }
2041
- </ul>
2277
+ <ul
2278
+ class="kf-select__list"
2279
+ [id]="listboxId"
2280
+ role="listbox"
2281
+ [attr.aria-labelledby]="triggerId"
2282
+ >
2283
+ @for (option of filteredOptions(); track option.value; let i = $index) {
2284
+ <li
2285
+ class="kf-select__option"
2286
+ [class.kf-select__option--selected]="option.value === value()"
2287
+ [class.kf-select__option--active]="i === activeIndex()"
2288
+ role="option"
2289
+ tabindex="0"
2290
+ [attr.aria-selected]="option.value === value()"
2291
+ (click)="select(option)"
2292
+ (mouseenter)="activeIndex.set(i)"
2293
+ (keydown.enter)="select(option)"
2294
+ (keydown.space)="$event.preventDefault(); select(option)"
2295
+ (keydown.arrowdown)="$event.preventDefault(); moveActive(1)"
2296
+ (keydown.arrowup)="$event.preventDefault(); moveActive(-1)"
2297
+ >
2298
+ {{ option.label }}
2299
+ </li>
2300
+ } @empty {
2301
+ <li class="kf-select__empty">
2302
+ {{ searchQuery() ? 'Sin resultados' : 'Sin opciones disponibles' }}
2303
+ </li>
2304
+ }
2305
+ </ul>
2306
+ </div>
2042
2307
  </ng-template>
2043
- `, isInline: true, styles: [":host{display:flex;flex-direction:column;gap:var(--kf-space-xs);width:240px;font-family:var(--kf-font-body)}.kf-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);width:100%;font:inherit;color:var(--kf-color-text);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);cursor:pointer;transition:border-color .14s,box-shadow .14s}.kf-select__trigger:hover:not(:disabled){border-color:var(--kf-color-border-strong)}.kf-select__trigger:focus-visible{outline:none;border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-select__trigger--disabled{opacity:.5;cursor:not-allowed}.kf-select__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kf-select__value--placeholder{color:var(--kf-color-text-muted)}.kf-select__caret{flex-shrink:0;width:8px;height:8px;border-right:2px solid var(--kf-color-text-muted);border-bottom:2px solid var(--kf-color-text-muted);transform:rotate(45deg);transition:transform .14s;margin-top:-4px}.kf-select__caret--open{transform:rotate(-135deg);margin-top:4px}.kf-select__panel{box-sizing:border-box;width:100%;margin:0;padding:var(--kf-space-xs) 0;list-style:none;max-height:240px;overflow-y:auto;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-select__option{padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-select__option:hover{background:var(--kf-color-primary-soft)}.kf-select__option--selected{background:var(--kf-color-primary-soft);color:var(--kf-color-primary);font-weight:600}.kf-select__empty{padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text-muted)}\n"], dependencies: [{ kind: "component", type: FieldLabel, selector: "kf-field-label", inputs: ["for", "required"] }] });
2308
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;gap:var(--kf-space-xs);width:240px;font-family:var(--kf-font-body)}.kf-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);width:100%;font:inherit;color:var(--kf-color-text);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);cursor:pointer;transition:border-color .14s,box-shadow .14s}.kf-select__trigger:hover:not(:disabled){border-color:var(--kf-color-border-strong)}.kf-select__trigger:focus-visible{outline:none;border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-select__trigger--disabled{opacity:.5;cursor:not-allowed}.kf-select__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kf-select__value--placeholder{color:var(--kf-color-text-muted)}.kf-select__caret{flex-shrink:0;width:8px;height:8px;border-right:2px solid var(--kf-color-text-muted);border-bottom:2px solid var(--kf-color-text-muted);transform:rotate(45deg);transition:transform .14s;margin-top:-4px}.kf-select__caret--open{transform:rotate(-135deg);margin-top:4px}.kf-select__panel{box-sizing:border-box;width:100%;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-md);overflow:hidden}.kf-select__search{box-sizing:border-box;width:100%;font:inherit;font-family:var(--kf-font-body);color:var(--kf-color-text);padding:var(--kf-space-sm) var(--kf-space-md);border:none;border-bottom:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm) var(--kf-radius-sm) 0 0;outline:none}.kf-select__search:focus-visible{box-shadow:inset 0 0 0 2px var(--kf-color-primary)}.kf-select__list{box-sizing:border-box;width:100%;margin:0;padding:var(--kf-space-xs) 0;list-style:none;max-height:240px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--kf-color-gray-400) transparent}.kf-select__list::-webkit-scrollbar{width:6px}.kf-select__list::-webkit-scrollbar-track{background:transparent}.kf-select__list::-webkit-scrollbar-thumb{background-color:var(--kf-color-gray-400);border-radius:var(--kf-radius-pill)}.kf-select__list::-webkit-scrollbar-thumb:hover{background-color:var(--kf-color-gray-500)}.kf-select__option{padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-select__option:hover{background:var(--kf-color-primary-soft)}.kf-select__option--selected{background:var(--kf-color-primary-soft);color:var(--kf-color-primary);font-weight:600}.kf-select__option--active{background:var(--kf-color-primary-soft)}.kf-select__empty{text-align:center;padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text-muted)}\n"], dependencies: [{ kind: "component", type: FieldLabel, selector: "kf-field-label", inputs: ["for", "required"] }] });
2044
2309
  }
2045
2310
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Select, decorators: [{
2046
2311
  type: Component,
@@ -2070,32 +2335,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
2070
2335
  ></span>
2071
2336
  </button>
2072
2337
  <ng-template #panelTemplate>
2073
- <ul
2074
- class="kf-select__panel"
2075
- [id]="listboxId"
2076
- role="listbox"
2077
- [attr.aria-labelledby]="triggerId"
2078
- >
2079
- @for (option of normalizedOptions(); track option.value) {
2080
- <li
2081
- class="kf-select__option"
2082
- [class.kf-select__option--selected]="option.value === value()"
2083
- role="option"
2084
- tabindex="0"
2085
- [attr.aria-selected]="option.value === value()"
2086
- (click)="select(option)"
2087
- (keydown.enter)="select(option)"
2088
- (keydown.space)="$event.preventDefault(); select(option)"
2089
- >
2090
- {{ option.label }}
2091
- </li>
2092
- } @empty {
2093
- <li class="kf-select__empty">Sin opciones disponibles</li>
2338
+ <div class="kf-select__panel">
2339
+ @if (showSearch()) {
2340
+ <input
2341
+ #searchInput
2342
+ type="text"
2343
+ class="kf-select__search"
2344
+ placeholder="Buscar…"
2345
+ [value]="searchQuery()"
2346
+ (input)="onSearchInput($event)"
2347
+ (keydown.arrowdown)="$event.preventDefault(); moveActive(1)"
2348
+ (keydown.arrowup)="$event.preventDefault(); moveActive(-1)"
2349
+ (keydown.enter)="$event.preventDefault(); selectActive()"
2350
+ />
2094
2351
  }
2095
- </ul>
2352
+ <ul
2353
+ class="kf-select__list"
2354
+ [id]="listboxId"
2355
+ role="listbox"
2356
+ [attr.aria-labelledby]="triggerId"
2357
+ >
2358
+ @for (option of filteredOptions(); track option.value; let i = $index) {
2359
+ <li
2360
+ class="kf-select__option"
2361
+ [class.kf-select__option--selected]="option.value === value()"
2362
+ [class.kf-select__option--active]="i === activeIndex()"
2363
+ role="option"
2364
+ tabindex="0"
2365
+ [attr.aria-selected]="option.value === value()"
2366
+ (click)="select(option)"
2367
+ (mouseenter)="activeIndex.set(i)"
2368
+ (keydown.enter)="select(option)"
2369
+ (keydown.space)="$event.preventDefault(); select(option)"
2370
+ (keydown.arrowdown)="$event.preventDefault(); moveActive(1)"
2371
+ (keydown.arrowup)="$event.preventDefault(); moveActive(-1)"
2372
+ >
2373
+ {{ option.label }}
2374
+ </li>
2375
+ } @empty {
2376
+ <li class="kf-select__empty">
2377
+ {{ searchQuery() ? 'Sin resultados' : 'Sin opciones disponibles' }}
2378
+ </li>
2379
+ }
2380
+ </ul>
2381
+ </div>
2096
2382
  </ng-template>
2097
- `, styles: [":host{display:flex;flex-direction:column;gap:var(--kf-space-xs);width:240px;font-family:var(--kf-font-body)}.kf-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);width:100%;font:inherit;color:var(--kf-color-text);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);cursor:pointer;transition:border-color .14s,box-shadow .14s}.kf-select__trigger:hover:not(:disabled){border-color:var(--kf-color-border-strong)}.kf-select__trigger:focus-visible{outline:none;border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-select__trigger--disabled{opacity:.5;cursor:not-allowed}.kf-select__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kf-select__value--placeholder{color:var(--kf-color-text-muted)}.kf-select__caret{flex-shrink:0;width:8px;height:8px;border-right:2px solid var(--kf-color-text-muted);border-bottom:2px solid var(--kf-color-text-muted);transform:rotate(45deg);transition:transform .14s;margin-top:-4px}.kf-select__caret--open{transform:rotate(-135deg);margin-top:4px}.kf-select__panel{box-sizing:border-box;width:100%;margin:0;padding:var(--kf-space-xs) 0;list-style:none;max-height:240px;overflow-y:auto;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-select__option{padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-select__option:hover{background:var(--kf-color-primary-soft)}.kf-select__option--selected{background:var(--kf-color-primary-soft);color:var(--kf-color-primary);font-weight:600}.kf-select__empty{padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text-muted)}\n"] }]
2098
- }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelTemplate: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], onDocumentEscape: [{
2383
+ `, styles: [":host{display:flex;flex-direction:column;gap:var(--kf-space-xs);width:240px;font-family:var(--kf-font-body)}.kf-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);width:100%;font:inherit;color:var(--kf-color-text);padding:var(--kf-space-sm) var(--kf-space-md);background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);cursor:pointer;transition:border-color .14s,box-shadow .14s}.kf-select__trigger:hover:not(:disabled){border-color:var(--kf-color-border-strong)}.kf-select__trigger:focus-visible{outline:none;border-color:var(--kf-color-primary);box-shadow:var(--kf-shadow-focus)}.kf-select__trigger--disabled{opacity:.5;cursor:not-allowed}.kf-select__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kf-select__value--placeholder{color:var(--kf-color-text-muted)}.kf-select__caret{flex-shrink:0;width:8px;height:8px;border-right:2px solid var(--kf-color-text-muted);border-bottom:2px solid var(--kf-color-text-muted);transform:rotate(45deg);transition:transform .14s;margin-top:-4px}.kf-select__caret--open{transform:rotate(-135deg);margin-top:4px}.kf-select__panel{box-sizing:border-box;width:100%;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-md);overflow:hidden}.kf-select__search{box-sizing:border-box;width:100%;font:inherit;font-family:var(--kf-font-body);color:var(--kf-color-text);padding:var(--kf-space-sm) var(--kf-space-md);border:none;border-bottom:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm) var(--kf-radius-sm) 0 0;outline:none}.kf-select__search:focus-visible{box-shadow:inset 0 0 0 2px var(--kf-color-primary)}.kf-select__list{box-sizing:border-box;width:100%;margin:0;padding:var(--kf-space-xs) 0;list-style:none;max-height:240px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--kf-color-gray-400) transparent}.kf-select__list::-webkit-scrollbar{width:6px}.kf-select__list::-webkit-scrollbar-track{background:transparent}.kf-select__list::-webkit-scrollbar-thumb{background-color:var(--kf-color-gray-400);border-radius:var(--kf-radius-pill)}.kf-select__list::-webkit-scrollbar-thumb:hover{background-color:var(--kf-color-gray-500)}.kf-select__option{padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-select__option:hover{background:var(--kf-color-primary-soft)}.kf-select__option--selected{background:var(--kf-color-primary-soft);color:var(--kf-color-primary);font-weight:600}.kf-select__option--active{background:var(--kf-color-primary-soft)}.kf-select__empty{text-align:center;padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text-muted)}\n"] }]
2384
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchThreshold", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelTemplate: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], searchInputRef: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }], onDocumentEscape: [{
2099
2385
  type: HostListener,
2100
2386
  args: ['document:keydown.escape']
2101
2387
  }] } });
@@ -2150,7 +2436,7 @@ class CompanySelector {
2150
2436
  }
2151
2437
  </div>
2152
2438
  }
2153
- `, isInline: true, 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-label{font-family:var(--kf-font-title);font-size:.72rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--kf-color-text-muted)}.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"], outputs: ["valueChange"] }] });
2439
+ `, isInline: true, 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-label{font-family:var(--kf-font-title);font-size:.72rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--kf-color-text-muted)}.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", "searchable", "searchThreshold"], outputs: ["valueChange"] }] });
2154
2440
  }
2155
2441
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CompanySelector, decorators: [{
2156
2442
  type: Component,
@@ -2894,110 +3180,449 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
2894
3180
  `, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-data-table{background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-sm);overflow:hidden}.kf-data-table--flush{border:none;box-shadow:none;border-radius:0}.kf-data-table__toolbar{display:flex;justify-content:flex-end;padding:var(--kf-space-sm) var(--kf-space-md);border-bottom:1px solid var(--kf-color-border)}.kf-data-table__view-toggle{display:inline-flex;gap:2px;padding:2px;background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm)}.kf-data-table__view-option{font:inherit;font-size:.875em;padding:var(--kf-space-xs) var(--kf-space-sm);border:none;background:transparent;color:var(--kf-color-text-muted);border-radius:var(--kf-radius-xs);cursor:pointer;transition:background .14s,color .14s}.kf-data-table__view-option:hover:not(.kf-data-table__view-option--active){color:var(--kf-color-text)}.kf-data-table__view-option--active{background:var(--kf-color-white);color:var(--kf-color-ink);box-shadow:var(--kf-shadow-xs)}.kf-data-table__view-option:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-data-table__scroll{overflow-x:auto}.kf-data-table__table{width:100%;border-collapse:collapse;text-align:left}.kf-data-table__table th,.kf-data-table__table td{padding:var(--kf-space-sm) var(--kf-space-md);border-bottom:1px solid var(--kf-color-border);white-space:nowrap}.kf-data-table__table th{font-family:var(--kf-font-title);font-size:.8125em;text-transform:uppercase;letter-spacing:.02em;color:var(--kf-color-text-muted);background:var(--kf-color-surface)}.kf-data-table__table td{color:var(--kf-color-text)}.kf-data-table__table tbody tr:last-child td{border-bottom:none}.kf-data-table__row{cursor:pointer;transition:background .14s}.kf-data-table__row:hover{background:var(--kf-color-primary-soft)}.kf-data-table__cell--right{text-align:right}.kf-data-table__empty{padding:var(--kf-space-md)}.kf-data-table__footer{padding:var(--kf-space-sm) var(--kf-space-md);border-top:1px solid var(--kf-color-border)}.kf-data-table__footer:empty{display:none}.kf-data-table__cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--kf-space-md);padding:var(--kf-space-md)}.kf-data-table__card{display:flex;flex-direction:column;border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);overflow:hidden;transition:border-color .14s,box-shadow .14s}.kf-data-table__card--interactive{cursor:pointer}.kf-data-table__card:hover{border-color:var(--kf-color-border-strong);box-shadow:var(--kf-shadow-xs)}.kf-data-table__card--interactive:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-data-table__card-header{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-md)}.kf-data-table__card-media{flex:none}.kf-data-table__card-media img,.kf-data-table__thumb{display:block;width:48px;height:48px;border-radius:var(--kf-radius-xs);object-fit:cover;background:var(--kf-color-surface)}.kf-data-table__card-heading{display:flex;flex-direction:column;gap:var(--kf-space-xs);min-width:0}.kf-data-table__card-title{font-family:var(--kf-font-title);font-weight:600;color:var(--kf-color-ink);overflow-wrap:anywhere}.kf-data-table__card-statuses,.kf-data-table__card-actions{display:flex;flex-wrap:wrap;gap:var(--kf-space-xs)}.kf-data-table__card-fields{display:flex;flex-direction:column;gap:var(--kf-space-xs);padding:var(--kf-space-md)}.kf-data-table__card-header+.kf-data-table__card-fields{padding-top:0}.kf-data-table__card-field{display:flex;justify-content:space-between;gap:var(--kf-space-sm)}.kf-data-table__card-actions{justify-content:flex-end;margin-top:auto;padding:var(--kf-space-sm) var(--kf-space-md);border-top:1px solid var(--kf-color-border);background:var(--kf-color-surface)}.kf-data-table__thumb{width:32px;height:32px}.kf-data-table__card-label{font-family:var(--kf-font-title);font-size:.8125em;text-transform:uppercase;letter-spacing:.02em;color:var(--kf-color-text-muted)}.kf-data-table__card-value{color:var(--kf-color-text);text-align:right}\n"] }]
2895
3181
  }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], rowKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowKey", required: false }] }], flush: [{ type: i0.Input, args: [{ isSignal: true, alias: "flush", required: false }] }], cardable: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardable", required: false }] }], defaultView: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultView", required: false }] }], render: [{ type: i0.Input, args: [{ isSignal: true, alias: "render", required: false }] }], cellTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellTemplates", required: false }] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }] } });
2896
3182
 
3183
+ /**
3184
+ * Panel lateral genérico, agnóstico de contenido — no sabe qué es navegación
3185
+ * ni qué es sesión, solo da tres lugares para proyectar contenido
3186
+ * (`[kf-drawer-header]`, default, `[kf-drawer-footer]`) y resuelve lo que
3187
+ * cualquier drawer necesita resolver igual sin importar qué haya adentro:
3188
+ * overlay con blur que cierra al click, `Escape`, atrapado de foco real y
3189
+ * devolución de foco al cerrar.
3190
+ *
3191
+ * Extraído de `PortalHeader`, que hasta acá reimplementaba su propio drawer
3192
+ * embebido (navegación hardcodeada + slot único al final). Cualquier
3193
+ * consumidor que necesite un panel lateral — no solo `PortalHeader` — puede
3194
+ * usar este componente directamente.
3195
+ *
3196
+ * `open` es two-way, mismo patrón que `Dropdown`/`Modal`: el consumidor
3197
+ * decide qué dispara la apertura (un botón de hamburguesa, por ejemplo) y
3198
+ * qué hace con el cierre.
3199
+ *
3200
+ * Foco: al abrir, se crea un `FocusTrap` del CDK sobre el panel (la misma
3201
+ * pieza que usa Angular Material para `mat-sidenav`/`mat-dialog`) — Tab y
3202
+ * Shift+Tab ciclan solo entre los elementos focuseables del drawer, sin
3203
+ * escapar al contenido de fondo. Al cerrar, el foco vuelve al elemento que
3204
+ * tenía el foco antes de abrir (guardado en `document.activeElement`), no
3205
+ * se pierde en el `<body>`.
3206
+ *
3207
+ * `[kf-drawer-header]` tiene fondo oscuro propio (`--kf-color-tertiary-deep`),
3208
+ * distinto del body (`--kf-color-white`) — pensado para contener
3209
+ * `kf-drawer-session`, que asume ese fondo y no lo trae él mismo (ver
3210
+ * `DrawerSession.ts`). Si el slot queda vacío, el contenedor colapsa
3211
+ * (`display: none`) y no deja una franja oscura sin contenido.
3212
+ *
3213
+ * <kf-drawer [(open)]="menuOpen" ariaLabel="Menú de navegación">
3214
+ * <div kf-drawer-header>
3215
+ * <kf-drawer-session name="Tomás Soto" email="tomas@kaufmann.com" (logoutClick)="onLogout()" />
3216
+ * </div>
3217
+ * <a href="/">Home</a>
3218
+ * <a href="/vehiculos">Vehículos</a>
3219
+ * </kf-drawer>
3220
+ */
3221
+ class Drawer {
3222
+ /** Visibilidad del panel, two-way: `[(open)]="menuOpen"`. */
3223
+ open = model(false, /* @ts-ignore */
3224
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
3225
+ /** Borde de anclaje del panel. `left` (default) es la convención dominante
3226
+ * (Material/iOS/Android): el botón que abre el drawer y el propio panel
3227
+ * quedan del mismo lado. */
3228
+ side = input('left', /* @ts-ignore */
3229
+ ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
3230
+ /** `aria-label` del `<nav>` interno — describe el propósito del panel para
3231
+ * lectores de pantalla (ej. "Menú de navegación"). */
3232
+ ariaLabel = input('Menú', /* @ts-ignore */
3233
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
3234
+ panelRef = viewChild.required('panel', /* @ts-ignore */
3235
+ ...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
3236
+ focusTrapFactory = inject(FocusTrapFactory);
3237
+ document = inject(DOCUMENT);
3238
+ focusTrap = null;
3239
+ lastFocusedElement = null;
3240
+ constructor() {
3241
+ afterRenderEffect(() => {
3242
+ if (this.open()) {
3243
+ this.lastFocusedElement = this.document.activeElement;
3244
+ const trap = this.getOrCreateFocusTrap();
3245
+ void trap.focusInitialElement();
3246
+ }
3247
+ else {
3248
+ this.focusTrap?.destroy();
3249
+ this.focusTrap = null;
3250
+ this.lastFocusedElement?.focus();
3251
+ this.lastFocusedElement = null;
3252
+ }
3253
+ });
3254
+ inject(DestroyRef).onDestroy(() => this.focusTrap?.destroy());
3255
+ }
3256
+ /** Cierra el drawer con Escape — mismo patrón que `Dropdown`/`Modal`. */
3257
+ onEscape() {
3258
+ this.open.set(false);
3259
+ }
3260
+ getOrCreateFocusTrap() {
3261
+ if (!this.focusTrap) {
3262
+ this.focusTrap = this.focusTrapFactory.create(this.panelRef().nativeElement);
3263
+ }
3264
+ return this.focusTrap;
3265
+ }
3266
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Drawer, deps: [], target: i0.ɵɵFactoryTarget.Component });
3267
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: Drawer, isStandalone: true, selector: "kf-drawer", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, viewQueries: [{ propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: `
3268
+ @if (open()) {
3269
+ <button
3270
+ type="button"
3271
+ class="kf-drawer__overlay"
3272
+ aria-label="Cerrar menú"
3273
+ (click)="open.set(false)"
3274
+ ></button>
3275
+ }
3276
+
3277
+ <nav
3278
+ #panel
3279
+ class="kf-drawer__panel"
3280
+ [class.kf-drawer__panel--open]="open()"
3281
+ [class.kf-drawer__panel--right]="side() === 'right'"
3282
+ [attr.aria-label]="ariaLabel()"
3283
+ [attr.aria-hidden]="!open()"
3284
+ >
3285
+ <div class="kf-drawer__header"><ng-content select="[kf-drawer-header]" /></div>
3286
+ <div class="kf-drawer__body"><ng-content /></div>
3287
+ <div class="kf-drawer__footer"><ng-content select="[kf-drawer-footer]" /></div>
3288
+ </nav>
3289
+ `, isInline: true, styles: [".kf-drawer__overlay{position:fixed;inset:0;border:none;padding:0;cursor:pointer;background:#171d2566;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:39}.kf-drawer__panel{position:fixed;top:0;left:0;bottom:0;z-index:40;display:flex;flex-direction:column;width:82%;max-width:320px;background:var(--kf-color-white);box-shadow:8px 0 24px -8px #1018284d;transform:translate(-100%);visibility:hidden;transition:transform .24s cubic-bezier(.32,.72,0,1),visibility 0s linear .24s}.kf-drawer__panel--right{left:auto;right:0;box-shadow:-8px 0 24px -8px #1018284d;transform:translate(100%)}.kf-drawer__panel--open{transform:translate(0);visibility:visible;transition:transform .24s cubic-bezier(.32,.72,0,1),visibility 0s linear 0s}.kf-drawer__header:empty,.kf-drawer__footer:empty{display:none}.kf-drawer__header{padding:var(--kf-space-lg);background:var(--kf-color-tertiary-deep)}.kf-drawer__body{flex:1 1 auto;display:flex;flex-direction:column;gap:var(--kf-space-xs);padding:var(--kf-space-lg);overflow-y:auto}.kf-drawer__footer{padding:var(--kf-space-lg);border-top:1px solid var(--kf-color-border)}\n"] });
3290
+ }
3291
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Drawer, decorators: [{
3292
+ type: Component,
3293
+ args: [{ selector: 'kf-drawer', template: `
3294
+ @if (open()) {
3295
+ <button
3296
+ type="button"
3297
+ class="kf-drawer__overlay"
3298
+ aria-label="Cerrar menú"
3299
+ (click)="open.set(false)"
3300
+ ></button>
3301
+ }
3302
+
3303
+ <nav
3304
+ #panel
3305
+ class="kf-drawer__panel"
3306
+ [class.kf-drawer__panel--open]="open()"
3307
+ [class.kf-drawer__panel--right]="side() === 'right'"
3308
+ [attr.aria-label]="ariaLabel()"
3309
+ [attr.aria-hidden]="!open()"
3310
+ >
3311
+ <div class="kf-drawer__header"><ng-content select="[kf-drawer-header]" /></div>
3312
+ <div class="kf-drawer__body"><ng-content /></div>
3313
+ <div class="kf-drawer__footer"><ng-content select="[kf-drawer-footer]" /></div>
3314
+ </nav>
3315
+ `, styles: [".kf-drawer__overlay{position:fixed;inset:0;border:none;padding:0;cursor:pointer;background:#171d2566;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:39}.kf-drawer__panel{position:fixed;top:0;left:0;bottom:0;z-index:40;display:flex;flex-direction:column;width:82%;max-width:320px;background:var(--kf-color-white);box-shadow:8px 0 24px -8px #1018284d;transform:translate(-100%);visibility:hidden;transition:transform .24s cubic-bezier(.32,.72,0,1),visibility 0s linear .24s}.kf-drawer__panel--right{left:auto;right:0;box-shadow:-8px 0 24px -8px #1018284d;transform:translate(100%)}.kf-drawer__panel--open{transform:translate(0);visibility:visible;transition:transform .24s cubic-bezier(.32,.72,0,1),visibility 0s linear 0s}.kf-drawer__header:empty,.kf-drawer__footer:empty{display:none}.kf-drawer__header{padding:var(--kf-space-lg);background:var(--kf-color-tertiary-deep)}.kf-drawer__body{flex:1 1 auto;display:flex;flex-direction:column;gap:var(--kf-space-xs);padding:var(--kf-space-lg);overflow-y:auto}.kf-drawer__footer{padding:var(--kf-space-lg);border-top:1px solid var(--kf-color-border)}\n"] }]
3316
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], onEscape: [{
3317
+ type: HostListener,
3318
+ args: ['document:keydown.escape']
3319
+ }] } });
3320
+
3321
+ /**
3322
+ * Botón de cerrar sesión, pensado para proyectarse dentro de
3323
+ * `[kf-drawer-footer]` de `kf-drawer`, o junto a `kf-drawer-session` dentro
3324
+ * de `[kf-sidebar-footer]` de `kf-sidebar-shell` — separado de
3325
+ * `kf-drawer-session` (que solo muestra identidad: brand, avatar, nombre,
3326
+ * email), no anidado dentro de ella.
3327
+ *
3328
+ * Reproduce el mismo botón que antes vivía embebido en `DrawerSession`:
3329
+ * ícono `log-out` + label, color `--kf-color-error` con hover en
3330
+ * `--kf-color-error-soft` y foco visible con `--kf-shadow-focus`. Se separó
3331
+ * en un componente propio porque identidad y logout son responsabilidades
3332
+ * distintas — `kf-drawer-session` no debería saber que existe un botón de
3333
+ * cerrar sesión, y el footer del drawer ya tiene su propio padding/borde
3334
+ * superior (ver `Drawer.ts`) que reemplaza el borde que antes traía el botón
3335
+ * mismo.
3336
+ *
3337
+ * No sabe nada de autenticación real — solo emite `logoutClick` cuando el
3338
+ * consumidor debe cerrar la sesión real.
3339
+ *
3340
+ * `collapsed` oculta el label y centra el ícono, exponiéndolo vía `title`
3341
+ * — mismo criterio que `SidebarNavItem.collapsed` y
3342
+ * `DrawerSession.collapsed`, para cuando este botón vive dentro de un
3343
+ * `kf-sidebar-shell` colapsado.
3344
+ *
3345
+ * <kf-drawer-logout kf-portal-header-footer (logoutClick)="onLogout()" />
3346
+ */
3347
+ class DrawerLogout {
3348
+ /** Label del botón, en español neutro por defecto. Input opcional por si
3349
+ * algún consumidor futuro necesita otro texto. */
3350
+ label = input('Cerrar sesión', /* @ts-ignore */
3351
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3352
+ /** Oculta el label y centra el ícono — ver comentario de la clase. */
3353
+ collapsed = input(false, { ...(ngDevMode ? { debugName: "collapsed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3354
+ logoutClick = output();
3355
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DrawerLogout, deps: [], target: i0.ɵɵFactoryTarget.Component });
3356
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: DrawerLogout, isStandalone: true, selector: "kf-drawer-logout", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { logoutClick: "logoutClick" }, ngImport: i0, template: `
3357
+ <button
3358
+ type="button"
3359
+ class="kf-drawer-logout__button"
3360
+ [class.kf-drawer-logout__button--collapsed]="collapsed()"
3361
+ [attr.title]="collapsed() ? label() : null"
3362
+ (click)="logoutClick.emit()"
3363
+ >
3364
+ <kf-icon name="log-out" size="sm" />
3365
+ @if (!collapsed()) {
3366
+ {{ label() }}
3367
+ }
3368
+ </button>
3369
+ `, isInline: true, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-drawer-logout__button{display:flex;align-items:center;gap:var(--kf-space-xs);width:100%;border:none;background:transparent;padding:0;font:inherit;font-size:.875rem;font-weight:600;color:var(--kf-color-error);cursor:pointer}.kf-drawer-logout__button--collapsed{justify-content:center}.kf-drawer-logout__button:hover{color:var(--kf-color-error-strong);background:var(--kf-color-error-soft)}.kf-drawer-logout__button:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }] });
3370
+ }
3371
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DrawerLogout, decorators: [{
3372
+ type: Component,
3373
+ args: [{ selector: 'kf-drawer-logout', imports: [Icon], template: `
3374
+ <button
3375
+ type="button"
3376
+ class="kf-drawer-logout__button"
3377
+ [class.kf-drawer-logout__button--collapsed]="collapsed()"
3378
+ [attr.title]="collapsed() ? label() : null"
3379
+ (click)="logoutClick.emit()"
3380
+ >
3381
+ <kf-icon name="log-out" size="sm" />
3382
+ @if (!collapsed()) {
3383
+ {{ label() }}
3384
+ }
3385
+ </button>
3386
+ `, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-drawer-logout__button{display:flex;align-items:center;gap:var(--kf-space-xs);width:100%;border:none;background:transparent;padding:0;font:inherit;font-size:.875rem;font-weight:600;color:var(--kf-color-error);cursor:pointer}.kf-drawer-logout__button--collapsed{justify-content:center}.kf-drawer-logout__button:hover{color:var(--kf-color-error-strong);background:var(--kf-color-error-soft)}.kf-drawer-logout__button:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}\n"] }]
3387
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], logoutClick: [{ type: i0.Output, args: ["logoutClick"] }] } });
3388
+
3389
+ /**
3390
+ * Bloque de identidad para `[kf-drawer-header]` de `kf-drawer` o
3391
+ * `[kf-sidebar-footer]` de `kf-sidebar-shell`: avatar + nombre + email,
3392
+ * brand opcional arriba. Solo identidad — el logout vive aparte, en
3393
+ * `kf-drawer-logout`.
3394
+ *
3395
+ * `collapsed` oculta brand/nombre/email y deja solo el avatar (para un
3396
+ * `kf-sidebar-shell` colapsado). `interactive` convierte la tarjeta en un
3397
+ * botón real que emite `cardClick`, para cuando la identidad es el
3398
+ * disparador de un menú de acciones (ej. envuelta en `kf-dropdown`).
3399
+ *
3400
+ * <kf-dropdown [(open)]="menuOpen">
3401
+ * <kf-drawer-session interactive name="Juan Pérez" email="juan@empresa.com" (cardClick)="menuOpen.set(!menuOpen())" />
3402
+ * <kf-dropdown-panel variant="dark">
3403
+ * <kf-dropdown-item icon="log-out" tone="error" (click)="onLogout()">Cerrar sesión</kf-dropdown-item>
3404
+ * </kf-dropdown-panel>
3405
+ * </kf-dropdown>
3406
+ */
3407
+ class DrawerSession {
3408
+ name = input.required(/* @ts-ignore */
3409
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
3410
+ email = input('', /* @ts-ignore */
3411
+ ...(ngDevMode ? [{ debugName: "email" }] : /* istanbul ignore next */ []));
3412
+ initials = input('', /* @ts-ignore */
3413
+ ...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
3414
+ avatarUrl = input('', /* @ts-ignore */
3415
+ ...(ngDevMode ? [{ debugName: "avatarUrl" }] : /* istanbul ignore next */ []));
3416
+ /** Label superior opcional, ej. "Portal Kaufmann LATAM". */
3417
+ brand = input('', /* @ts-ignore */
3418
+ ...(ngDevMode ? [{ debugName: "brand" }] : /* istanbul ignore next */ []));
3419
+ /** Oculta brand/nombre/email y centra el avatar — ver comentario de la clase. */
3420
+ collapsed = input(false, { ...(ngDevMode ? { debugName: "collapsed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3421
+ /** Convierte la tarjeta en un botón real con ícono de kebab — ver comentario de la clase. */
3422
+ interactive = input(false, { ...(ngDevMode ? { debugName: "interactive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3423
+ /** Se emite al clickear la tarjeta, solo cuando `interactive` es `true`. */
3424
+ cardClick = output();
3425
+ /** Si no se pasa `initials`, se derivan de `name`: primera letra de cada
3426
+ * una de las primeras 2 palabras, en mayúscula. */
3427
+ displayInitials = computed(() => {
3428
+ const explicit = this.initials();
3429
+ if (explicit) {
3430
+ return explicit;
3431
+ }
3432
+ return this.name()
3433
+ .trim()
3434
+ .split(/\s+/)
3435
+ .slice(0, 2)
3436
+ .map((word) => word.charAt(0).toUpperCase())
3437
+ .join('');
3438
+ }, /* @ts-ignore */
3439
+ ...(ngDevMode ? [{ debugName: "displayInitials" }] : /* istanbul ignore next */ []));
3440
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DrawerSession, deps: [], target: i0.ɵɵFactoryTarget.Component });
3441
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: DrawerSession, isStandalone: true, selector: "kf-drawer-session", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, email: { classPropertyName: "email", publicName: "email", isSignal: true, isRequired: false, transformFunction: null }, initials: { classPropertyName: "initials", publicName: "initials", isSignal: true, isRequired: false, transformFunction: null }, avatarUrl: { classPropertyName: "avatarUrl", publicName: "avatarUrl", isSignal: true, isRequired: false, transformFunction: null }, brand: { classPropertyName: "brand", publicName: "brand", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cardClick: "cardClick" }, ngImport: i0, template: `
3442
+ @if (brand() && !collapsed()) {
3443
+ <span class="kf-drawer-session__brand">{{ brand() }}</span>
3444
+ }
3445
+
3446
+ @if (interactive()) {
3447
+ <button
3448
+ type="button"
3449
+ class="kf-drawer-session__card kf-drawer-session__card--interactive"
3450
+ [class.kf-drawer-session__card--collapsed]="collapsed()"
3451
+ (click)="cardClick.emit()"
3452
+ >
3453
+ <kf-avatar [initials]="displayInitials()" [src]="avatarUrl()" />
3454
+ @if (!collapsed()) {
3455
+ <div class="kf-drawer-session__identity">
3456
+ <span class="kf-drawer-session__name">{{ name() }}</span>
3457
+ @if (email()) {
3458
+ <span class="kf-drawer-session__email">{{ email() }}</span>
3459
+ }
3460
+ </div>
3461
+ <kf-icon name="more-vertical" size="sm" />
3462
+ }
3463
+ </button>
3464
+ } @else {
3465
+ <div class="kf-drawer-session__card" [class.kf-drawer-session__card--collapsed]="collapsed()">
3466
+ <kf-avatar [initials]="displayInitials()" [src]="avatarUrl()" />
3467
+ @if (!collapsed()) {
3468
+ <div class="kf-drawer-session__identity">
3469
+ <span class="kf-drawer-session__name">{{ name() }}</span>
3470
+ @if (email()) {
3471
+ <span class="kf-drawer-session__email">{{ email() }}</span>
3472
+ }
3473
+ </div>
3474
+ }
3475
+ </div>
3476
+ }
3477
+ `, isInline: true, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-drawer-session__brand{display:block;margin-bottom:var(--kf-space-sm);font-family:var(--kf-font-mono);font-size:.6875rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#ffffffa6}.kf-drawer-session__card{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);width:100%;padding:var(--kf-space-sm);border:1px solid rgba(255,255,255,.14);border-radius:var(--kf-radius-md);background:#ffffff0a}.kf-drawer-session__card--interactive{font:inherit;color:inherit;cursor:pointer;text-align:left;transition:background .14s}.kf-drawer-session__card--interactive:hover{background:#ffffff17}.kf-drawer-session__card--interactive:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-drawer-session__card--interactive kf-icon{flex:none;color:#ffffff73}.kf-drawer-session__card--collapsed{justify-content:center;width:40px;height:40px;padding:0;border-color:transparent;background:transparent}.kf-drawer-session__card--collapsed:hover{background:#ffffff17}.kf-drawer-session__identity{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto}.kf-drawer-session__name{font-size:.9375rem;font-weight:600;color:var(--kf-color-white);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-drawer-session__email{font-size:.8125rem;color:#ffffffa6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "component", type: Avatar, selector: "kf-avatar", inputs: ["initials", "src", "round", "pill"] }, { kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }] });
3478
+ }
3479
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DrawerSession, decorators: [{
3480
+ type: Component,
3481
+ args: [{ selector: 'kf-drawer-session', imports: [Avatar, Icon], template: `
3482
+ @if (brand() && !collapsed()) {
3483
+ <span class="kf-drawer-session__brand">{{ brand() }}</span>
3484
+ }
3485
+
3486
+ @if (interactive()) {
3487
+ <button
3488
+ type="button"
3489
+ class="kf-drawer-session__card kf-drawer-session__card--interactive"
3490
+ [class.kf-drawer-session__card--collapsed]="collapsed()"
3491
+ (click)="cardClick.emit()"
3492
+ >
3493
+ <kf-avatar [initials]="displayInitials()" [src]="avatarUrl()" />
3494
+ @if (!collapsed()) {
3495
+ <div class="kf-drawer-session__identity">
3496
+ <span class="kf-drawer-session__name">{{ name() }}</span>
3497
+ @if (email()) {
3498
+ <span class="kf-drawer-session__email">{{ email() }}</span>
3499
+ }
3500
+ </div>
3501
+ <kf-icon name="more-vertical" size="sm" />
3502
+ }
3503
+ </button>
3504
+ } @else {
3505
+ <div class="kf-drawer-session__card" [class.kf-drawer-session__card--collapsed]="collapsed()">
3506
+ <kf-avatar [initials]="displayInitials()" [src]="avatarUrl()" />
3507
+ @if (!collapsed()) {
3508
+ <div class="kf-drawer-session__identity">
3509
+ <span class="kf-drawer-session__name">{{ name() }}</span>
3510
+ @if (email()) {
3511
+ <span class="kf-drawer-session__email">{{ email() }}</span>
3512
+ }
3513
+ </div>
3514
+ }
3515
+ </div>
3516
+ }
3517
+ `, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-drawer-session__brand{display:block;margin-bottom:var(--kf-space-sm);font-family:var(--kf-font-mono);font-size:.6875rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#ffffffa6}.kf-drawer-session__card{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);width:100%;padding:var(--kf-space-sm);border:1px solid rgba(255,255,255,.14);border-radius:var(--kf-radius-md);background:#ffffff0a}.kf-drawer-session__card--interactive{font:inherit;color:inherit;cursor:pointer;text-align:left;transition:background .14s}.kf-drawer-session__card--interactive:hover{background:#ffffff17}.kf-drawer-session__card--interactive:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-drawer-session__card--interactive kf-icon{flex:none;color:#ffffff73}.kf-drawer-session__card--collapsed{justify-content:center;width:40px;height:40px;padding:0;border-color:transparent;background:transparent}.kf-drawer-session__card--collapsed:hover{background:#ffffff17}.kf-drawer-session__identity{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto}.kf-drawer-session__name{font-size:.9375rem;font-weight:600;color:var(--kf-color-white);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-drawer-session__email{font-size:.8125rem;color:#ffffffa6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
3518
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], email: [{ type: i0.Input, args: [{ isSignal: true, alias: "email", required: false }] }], initials: [{ type: i0.Input, args: [{ isSignal: true, alias: "initials", required: false }] }], avatarUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarUrl", required: false }] }], brand: [{ type: i0.Input, args: [{ isSignal: true, alias: "brand", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], cardClick: [{ type: i0.Output, args: ["cardClick"] }] } });
3519
+
2897
3520
  /**
2898
3521
  * Ítem de acción dentro de un `kf-dropdown-panel`. Es un `<button>` nativo
2899
- * por dentro (no un `<a>`): las acciones de un menú de este tipo disparan
2900
- * comportamiento en la pantalla actual (cerrar sesión, ejecutar un
2901
- * comando), no navegan a otra URL.
3522
+ * (no un `<a>`): dispara comportamiento en la pantalla actual, no navega.
2902
3523
  *
2903
- * <kf-dropdown-item (click)="onLogout()" tone="error">Cerrar sesión</kf-dropdown-item>
3524
+ * `icon` es opcional — sin él, el ítem es solo texto.
3525
+ *
3526
+ * <kf-dropdown-item icon="log-out" (click)="onLogout()" tone="error">Cerrar sesión</kf-dropdown-item>
2904
3527
  */
2905
3528
  class DropdownItem {
2906
3529
  tone = input('neutral', /* @ts-ignore */
2907
3530
  ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
3531
+ /** Ícono opcional al inicio de la fila. */
3532
+ icon = input(/* @ts-ignore */
3533
+ ...(ngDevMode ? [undefined, { debugName: "icon" }] : /* istanbul ignore next */ []));
2908
3534
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DropdownItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
2909
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.0", type: DropdownItem, isStandalone: true, selector: "kf-dropdown-item", inputs: { tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
3535
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: DropdownItem, isStandalone: true, selector: "kf-dropdown-item", inputs: { tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2910
3536
  <button
2911
3537
  type="button"
2912
3538
  class="kf-dropdown-item"
2913
3539
  [class.kf-dropdown-item--error]="tone() === 'error'"
2914
3540
  role="menuitem"
2915
3541
  >
2916
- <ng-content />
3542
+ @if (icon()) {
3543
+ <kf-icon [name]="icon()!" size="sm" />
3544
+ }
3545
+ <span class="kf-dropdown-item__label"><ng-content /></span>
2917
3546
  </button>
2918
- `, isInline: true, styles: [":host{display:block}.kf-dropdown-item{display:block;width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;background:transparent;font:inherit;text-align:left;color:var(--kf-color-text);cursor:pointer}.kf-dropdown-item:hover{background:var(--kf-color-surface)}.kf-dropdown-item:focus-visible{outline:none;box-shadow:inset var(--kf-shadow-focus)}.kf-dropdown-item--error{color:var(--kf-color-error-strong)}.kf-dropdown-item--error:hover{background:var(--kf-color-error-soft)}\n"] });
3547
+ `, isInline: true, styles: [":host{display:block}.kf-dropdown-item{display:flex;align-items:center;gap:var(--kf-space-sm);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;background:transparent;font:inherit;text-align:left;color:var(--kf-color-text);cursor:pointer}.kf-dropdown-item kf-icon{flex:none;color:var(--kf-color-text-muted)}.kf-dropdown-item__label{flex:1 1 auto}.kf-dropdown-item:hover{background:var(--kf-color-surface)}.kf-dropdown-item:focus-visible{outline:none;box-shadow:inset var(--kf-shadow-focus)}.kf-dropdown-item--error{color:var(--kf-color-error-strong)}.kf-dropdown-item--error kf-icon{color:var(--kf-color-error-strong)}.kf-dropdown-item--error:hover{background:var(--kf-color-error-soft)}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item{color:#ffffffd9}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item kf-icon{color:#ffffff8c}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item:hover{background:#ffffff14}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item--error{color:var(--kf-color-error)}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item--error kf-icon{color:var(--kf-color-error)}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item--error:hover{background:#bc454529}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }] });
2919
3548
  }
2920
3549
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DropdownItem, decorators: [{
2921
3550
  type: Component,
2922
- args: [{ selector: 'kf-dropdown-item', template: `
3551
+ args: [{ selector: 'kf-dropdown-item', imports: [Icon], template: `
2923
3552
  <button
2924
3553
  type="button"
2925
3554
  class="kf-dropdown-item"
2926
3555
  [class.kf-dropdown-item--error]="tone() === 'error'"
2927
3556
  role="menuitem"
2928
3557
  >
2929
- <ng-content />
3558
+ @if (icon()) {
3559
+ <kf-icon [name]="icon()!" size="sm" />
3560
+ }
3561
+ <span class="kf-dropdown-item__label"><ng-content /></span>
2930
3562
  </button>
2931
- `, styles: [":host{display:block}.kf-dropdown-item{display:block;width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;background:transparent;font:inherit;text-align:left;color:var(--kf-color-text);cursor:pointer}.kf-dropdown-item:hover{background:var(--kf-color-surface)}.kf-dropdown-item:focus-visible{outline:none;box-shadow:inset var(--kf-shadow-focus)}.kf-dropdown-item--error{color:var(--kf-color-error-strong)}.kf-dropdown-item--error:hover{background:var(--kf-color-error-soft)}\n"] }]
2932
- }], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
3563
+ `, styles: [":host{display:block}.kf-dropdown-item{display:flex;align-items:center;gap:var(--kf-space-sm);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;background:transparent;font:inherit;text-align:left;color:var(--kf-color-text);cursor:pointer}.kf-dropdown-item kf-icon{flex:none;color:var(--kf-color-text-muted)}.kf-dropdown-item__label{flex:1 1 auto}.kf-dropdown-item:hover{background:var(--kf-color-surface)}.kf-dropdown-item:focus-visible{outline:none;box-shadow:inset var(--kf-shadow-focus)}.kf-dropdown-item--error{color:var(--kf-color-error-strong)}.kf-dropdown-item--error kf-icon{color:var(--kf-color-error-strong)}.kf-dropdown-item--error:hover{background:var(--kf-color-error-soft)}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item{color:#ffffffd9}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item kf-icon{color:#ffffff8c}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item:hover{background:#ffffff14}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item--error{color:var(--kf-color-error)}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item--error kf-icon{color:var(--kf-color-error)}:host-context(.kf-dropdown-panel--dark) .kf-dropdown-item--error:hover{background:#bc454529}\n"] }]
3564
+ }], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }] } });
2933
3565
  /**
2934
3566
  * Contenido del panel flotante de un `kf-dropdown`. Es solo una plantilla
2935
- * (`<ng-template>` por dentro, capturada vía `TemplateRef`) — `Dropdown` la
2936
- * conecta al overlay real cuando `open` pasa a `true`, no la renderiza en su
2937
- * propio lugar del DOM. Esto es lo que permite que el panel aparezca pegado
2938
- * al host de `Dropdown` sin importar dónde esté ese host en la página.
2939
- *
2940
- * <kf-dropdown [(open)]="menuOpen">
2941
- * <kf-dropdown-panel>
2942
- * <kf-dropdown-item (click)="onLogout()">Cerrar sesión</kf-dropdown-item>
2943
- * </kf-dropdown-panel>
2944
- * </kf-dropdown>
3567
+ * (`<ng-template>`, capturada vía `TemplateRef`) — `Dropdown` la conecta al
3568
+ * overlay real cuando `open` pasa a `true`, no la renderiza en su propio
3569
+ * lugar del DOM. Esto permite que el panel aparezca pegado al host de
3570
+ * `Dropdown` sin importar dónde esté ese host en la página.
3571
+ *
3572
+ * `variant`: `'solid'` (default, fondo blanco) o `'dark'` (fondo sólido
3573
+ * oscuro, mismo tono que `[kf-drawer-header]`) para cuando el trigger vive
3574
+ * sobre una superficie oscura de marca.
3575
+ *
3576
+ * `dark` es sólida a propósito, no semitransparente: el CDK Overlay
3577
+ * reposiciona el panel según dónde entra en el viewport y puede terminar
3578
+ * cayendo sobre fondo blanco en vez de sobre el rail oscuro — un color
3579
+ * sólido se ve intencional sin importar dónde lo posicione el CDK.
2945
3580
  */
2946
3581
  class DropdownPanel {
3582
+ variant = input('solid', /* @ts-ignore */
3583
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
2947
3584
  templateRef = viewChild.required(TemplateRef, /* @ts-ignore */
2948
3585
  ...(ngDevMode ? [{ debugName: "templateRef" }] : /* istanbul ignore next */ []));
2949
3586
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DropdownPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
2950
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.0", type: DropdownPanel, isStandalone: true, selector: "kf-dropdown-panel", viewQueries: [{ propertyName: "templateRef", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], ngImport: i0, template: `
3587
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.0", type: DropdownPanel, isStandalone: true, selector: "kf-dropdown-panel", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "templateRef", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], ngImport: i0, template: `
2951
3588
  <ng-template>
2952
- <div class="kf-dropdown-panel">
3589
+ <div class="kf-dropdown-panel" [class.kf-dropdown-panel--dark]="variant() === 'dark'">
2953
3590
  <div class="kf-dropdown-panel__inner" role="menu">
2954
3591
  <ng-content />
2955
3592
  </div>
2956
3593
  </div>
2957
3594
  </ng-template>
2958
- `, isInline: true, styles: [".kf-dropdown-panel{min-width:220px;max-width:100%;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-lg);font-family:var(--kf-font-body);animation:kf-dropdown-panel-enter .14s ease-out}.kf-dropdown-panel__inner{padding:var(--kf-space-xs) 0;overflow:hidden;border-radius:inherit}@media(prefers-reduced-motion:reduce){.kf-dropdown-panel{animation:none}}@keyframes kf-dropdown-panel-enter{0%{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"] });
3595
+ `, isInline: true, styles: [".kf-dropdown-panel{min-width:220px;max-width:min(280px,90vw);background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-lg);font-family:var(--kf-font-body);animation:kf-dropdown-panel-enter .14s ease-out}.kf-dropdown-panel--dark{background:var(--kf-color-tertiary-deep);border-color:#ffffff24}.kf-dropdown-panel__inner{padding:var(--kf-space-xs) 0;overflow:hidden;border-radius:inherit}@media(prefers-reduced-motion:reduce){.kf-dropdown-panel{animation:none}}@keyframes kf-dropdown-panel-enter{0%{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"] });
2959
3596
  }
2960
3597
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DropdownPanel, decorators: [{
2961
3598
  type: Component,
2962
3599
  args: [{ selector: 'kf-dropdown-panel', template: `
2963
3600
  <ng-template>
2964
- <div class="kf-dropdown-panel">
3601
+ <div class="kf-dropdown-panel" [class.kf-dropdown-panel--dark]="variant() === 'dark'">
2965
3602
  <div class="kf-dropdown-panel__inner" role="menu">
2966
3603
  <ng-content />
2967
3604
  </div>
2968
3605
  </div>
2969
3606
  </ng-template>
2970
- `, styles: [".kf-dropdown-panel{min-width:220px;max-width:100%;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-lg);font-family:var(--kf-font-body);animation:kf-dropdown-panel-enter .14s ease-out}.kf-dropdown-panel__inner{padding:var(--kf-space-xs) 0;overflow:hidden;border-radius:inherit}@media(prefers-reduced-motion:reduce){.kf-dropdown-panel{animation:none}}@keyframes kf-dropdown-panel-enter{0%{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"] }]
2971
- }], propDecorators: { templateRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
3607
+ `, styles: [".kf-dropdown-panel{min-width:220px;max-width:min(280px,90vw);background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);box-shadow:var(--kf-shadow-lg);font-family:var(--kf-font-body);animation:kf-dropdown-panel-enter .14s ease-out}.kf-dropdown-panel--dark{background:var(--kf-color-tertiary-deep);border-color:#ffffff24}.kf-dropdown-panel__inner{padding:var(--kf-space-xs) 0;overflow:hidden;border-radius:inherit}@media(prefers-reduced-motion:reduce){.kf-dropdown-panel{animation:none}}@keyframes kf-dropdown-panel-enter{0%{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"] }]
3608
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], templateRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
2972
3609
  /**
2973
3610
  * Menú de acciones flotante, conectado a un elemento disparador vía
2974
3611
  * `@angular/cdk/overlay` — la misma pieza que usa Angular Material para
2975
- * `mat-menu`/`mat-select`. Resuelve el patrón que antes se reimplementaba a
2976
- * mano en cada consumidor (ver el session-menu de `web-portal-latam-frontend`,
2977
- * ~80 líneas de HTML/TS/SCSS para toggle + click-afuera + Escape + animación
2978
- * de entrada, acopladas a una clase interna no pública de `PortalHeader`).
2979
- *
2980
- * Se apoya en el CDK en vez de un `position: absolute` casero (como sí hace
2981
- * `SelectMenu`, que puede permitírselo porque su trigger vive siempre dentro
2982
- * del propio componente) porque acá el disparador real puede vivir en OTRO
2983
- * componente por completo — ej. el avatar dentro de `kf-portal-header` — y
2984
- * el panel tiene que aparecer pegado a ESE elemento sin importar dónde el
2985
- * consumidor ubique al `Dropdown` en su HTML. El Overlay CDK calcula esa
2986
- * posición (con fallback si no entra en pantalla) en vez de que cada
2987
- * consumidor lo recalcule a mano.
2988
- *
2989
- * `open` es two-way, igual que `Alert`/`Modal`: el consumidor decide qué
2990
- * dispara la apertura (puede ser un `(click)` en un botón propio, o un
2991
- * evento de otro componente como `PortalHeader.sessionClick`) — `Dropdown`
2992
- * en sí no renderiza ningún trigger, su propio host es el punto de anclaje
2993
- * visual del panel.
3612
+ * `mat-menu`/`mat-select`.
2994
3613
  *
2995
- * <kf-dropdown [(open)]="menuOpen">
2996
- * <kf-dropdown-panel>
2997
- * <div class="mi-header">Sesión iniciada como {{ email() }}</div>
2998
- * <kf-dropdown-item (click)="onLogout()" tone="error">Cerrar sesión</kf-dropdown-item>
2999
- * </kf-dropdown-panel>
3000
- * </kf-dropdown>
3614
+ * Se apoya en el CDK en vez de un `position: absolute` casero (a diferencia
3615
+ * de `SelectMenu`, que puede permitírselo porque su trigger vive siempre
3616
+ * dentro del propio componente) porque acá el disparador real puede vivir
3617
+ * en OTRO componente por completo — ej. el avatar dentro de
3618
+ * `kf-portal-header` — y el panel tiene que aparecer pegado a ESE elemento
3619
+ * sin importar dónde el consumidor ubique al `Dropdown` en su HTML.
3620
+ *
3621
+ * `open` es two-way: el consumidor decide qué dispara la apertura —
3622
+ * `Dropdown` en sí no renderiza ningún trigger, su propio host es el punto
3623
+ * de anclaje visual del panel.
3624
+ *
3625
+ * <kf-dropdown [(open)]="menuOpen"><kf-dropdown-panel>...</kf-dropdown-panel></kf-dropdown>
3001
3626
  */
3002
3627
  class Dropdown {
3003
3628
  /** Visibilidad del panel, two-way: `[(open)]="menuOpen"`. */
@@ -3005,17 +3630,10 @@ class Dropdown {
3005
3630
  ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
3006
3631
  /**
3007
3632
  * Elemento al que se conecta el overlay, si es distinto del propio host
3008
- * de `Dropdown`. Sin esto, `Dropdown` asume que ÉL MISMO es visualmente
3009
- * el disparador (caso normal: el consumidor arma su propio botón como
3010
- * contenido, y `Dropdown` envuelve ese contenido). Cuando el disparador
3011
- * real vive dentro de OTRO componente que no se puede envolver sin
3012
- * ensanchar el área de click a algo más grande que el botón (ej. el
3013
- * avatar de sesión dentro de `PortalHeader`, que convive con logo y
3014
- * navegación en el mismo host), `anchor` permite apuntar el overlay a la
3015
- * referencia real de ese componente en vez de al host vacío de
3016
- * `Dropdown`.
3017
- *
3018
- * <kf-dropdown [(open)]="menuOpen" [anchor]="headerRef()">
3633
+ * de `Dropdown`. Sin esto, `Dropdown` asume que ÉL MISMO es el
3634
+ * disparador. Útil cuando el disparador real vive dentro de OTRO
3635
+ * componente que no se puede envolver sin ensanchar el área de click
3636
+ * (ej. el avatar de sesión dentro de `PortalHeader`).
3019
3637
  */
3020
3638
  anchor = input(/* @ts-ignore */
3021
3639
  ...(ngDevMode ? [undefined, { debugName: "anchor" }] : /* istanbul ignore next */ []));
@@ -3026,6 +3644,20 @@ class Dropdown {
3026
3644
  viewContainerRef = inject(ViewContainerRef);
3027
3645
  destroyRef = inject(DestroyRef);
3028
3646
  overlayRef = null;
3647
+ /**
3648
+ * Suscripciones a `outsidePointerEvents()`/`keydownEvents()` del overlay
3649
+ * ACTUALMENTE adjunto. `getOrCreateOverlayRef()` reutiliza el mismo
3650
+ * `overlayRef` entre aperturas (no lo recrea cada vez) — sin esto,
3651
+ * `attach()` se resuscribía a esos observables en cada apertura sin
3652
+ * cancelar la suscripción de la vez anterior, acumulando N suscripciones
3653
+ * activas sobre el mismo overlay persistente. Eso causaba el bug real de
3654
+ * "clicks seguidos": al abrir de nuevo, el propio pointerdown que originó
3655
+ * la apertura llegaba a más de una suscripción acumulada, disparando
3656
+ * `open.set(false)` de forma errática en el mismo tick (patrón observado:
3657
+ * abre cada 2 clicks, cierra cada 1). Se cancelan explícitamente en
3658
+ * `detach()`, antes de la siguiente `attach()`.
3659
+ */
3660
+ overlaySubscription = null;
3029
3661
  constructor() {
3030
3662
  effect(() => {
3031
3663
  if (this.open()) {
@@ -3035,16 +3667,16 @@ class Dropdown {
3035
3667
  this.detach();
3036
3668
  }
3037
3669
  });
3038
- this.destroyRef.onDestroy(() => this.overlayRef?.dispose());
3670
+ this.destroyRef.onDestroy(() => {
3671
+ this.overlaySubscription?.unsubscribe();
3672
+ this.overlayRef?.dispose();
3673
+ });
3039
3674
  }
3040
3675
  /**
3041
3676
  * Respaldo de `overlayRef.keydownEvents()`: ese observable del CDK solo
3042
- * ve teclas presionadas con el foco DENTRO del pane del overlay. Si el
3043
- * usuario abre el menú y presiona Escape con el foco en otro lado de la
3044
- * página (caso común — el foco puede seguir en el botón disparador, que
3045
- * vive fuera del overlay), `keydownEvents()` nunca dispara. Este listener
3046
- * a nivel de documento cubre ese caso; el de `keydownEvents()` sigue
3047
- * activo para cuando el foco sí está dentro del panel.
3677
+ * ve teclas con el foco DENTRO del panel. Si el foco queda en el botón
3678
+ * disparador (caso común), `keydownEvents()` nunca dispara — este
3679
+ * listener a nivel de documento cubre ese caso.
3048
3680
  */
3049
3681
  onDocumentEscape() {
3050
3682
  this.open.set(false);
@@ -3057,14 +3689,38 @@ class Dropdown {
3057
3689
  const overlayRef = this.getOrCreateOverlayRef();
3058
3690
  const portal = new TemplatePortal(panel.templateRef(), this.viewContainerRef);
3059
3691
  overlayRef.attach(portal);
3060
- overlayRef.outsidePointerEvents().subscribe(() => this.open.set(false));
3061
- overlayRef.keydownEvents().subscribe((event) => {
3692
+ this.overlaySubscription = new Subscription();
3693
+ this.overlaySubscription.add(overlayRef.outsidePointerEvents().subscribe((event) => {
3694
+ if (this.isEventOnTrigger(event)) {
3695
+ return;
3696
+ }
3697
+ this.open.set(false);
3698
+ }));
3699
+ this.overlaySubscription.add(overlayRef.keydownEvents().subscribe((event) => {
3062
3700
  if (event.key === 'Escape') {
3063
3701
  this.open.set(false);
3064
3702
  }
3065
- });
3703
+ }));
3704
+ }
3705
+ /**
3706
+ * El trigger real (el `anchor`, o el propio host si no se pasó ninguno —
3707
+ * ver comentario de la clase) vive FUERA del overlay a propósito: el
3708
+ * overlay recién se crea/adjunta cuando `open` pasa a `true`, así que el
3709
+ * CDK no tiene forma de saber que ese elemento externo "es parte" de su
3710
+ * disparador. Sin este filtro, el mismo `pointerdown` que abre el menú
3711
+ * (el trigger no es hijo del overlay) también llega a
3712
+ * `outsidePointerEvents()`, cerrando el panel en el mismo gesto que lo
3713
+ * abrió — bug real reproducido con Playwright: "abre y cierra" en
3714
+ * clicks consecutivos rápidos sobre la tarjeta de sesión del sidebar.
3715
+ */
3716
+ isEventOnTrigger(event) {
3717
+ const trigger = this.anchor()?.nativeElement ?? this.host.nativeElement;
3718
+ const target = event.target;
3719
+ return target instanceof Node && trigger.contains(target);
3066
3720
  }
3067
3721
  detach() {
3722
+ this.overlaySubscription?.unsubscribe();
3723
+ this.overlaySubscription = null;
3068
3724
  this.overlayRef?.detach();
3069
3725
  }
3070
3726
  getOrCreateOverlayRef() {
@@ -3074,34 +3730,23 @@ class Dropdown {
3074
3730
  const positionStrategy = this.overlayService
3075
3731
  .position()
3076
3732
  .flexibleConnectedTo(this.anchor() ?? this.host)
3077
- // 4 posiciones, no 2: además de abajo/arriba (alineado al borde
3078
- // derecho del anchor, el caso común), agrega el mismo par alineado al
3079
- // borde IZQUIERDO. Con solo el par "end" — como estaba antes — un
3080
- // trigger cerca del borde derecho de la pantalla no tenía ningún
3081
- // fallback horizontal: el panel se salía del viewport en vez de
3082
- // reacomodarse hacia la izquierda. El CDK prueba cada posición en
3083
- // orden y usa la primera que entra completa en el viewport.
3733
+ // 4 posiciones, no 2: además del par alineado al borde derecho del
3734
+ // anchor, agrega el mismo par alineado al borde IZQUIERDO — sin esto,
3735
+ // un trigger cerca del borde derecho de pantalla no tenía fallback
3736
+ // horizontal y el panel se salía del viewport.
3084
3737
  .withPositions([
3085
3738
  { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
3086
3739
  { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
3087
3740
  { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
3088
3741
  { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
3089
3742
  ])
3090
- // Margen de seguridad contra los bordes del viewport — sin esto, el
3091
- // CDK puede considerar una posición "válida" aunque quede pegada al
3092
- // borde con 0px de aire.
3093
3743
  .withViewportMargin(8)
3094
- // Si ninguna de las 4 posiciones entra completa (viewport muy
3095
- // angosto), la última se ajusta con push en vez de desbordarse fuera
3096
- // de pantalla sin control.
3744
+ // Si ninguna de las 4 posiciones entra completa, la última se ajusta
3745
+ // con push en vez de desbordarse fuera de pantalla.
3097
3746
  .withPush(true);
3098
3747
  this.overlayRef = this.overlayService.create({
3099
3748
  positionStrategy,
3100
3749
  scrollStrategy: this.overlayService.scrollStrategies.reposition(),
3101
- // Sin esto, el panel puede quedar más angosto que su propio
3102
- // `min-width` en pantallas chicas si el CDK decide recortar — fija un
3103
- // techo razonable en vez de dejarlo crecer sin límite ni encogerse
3104
- // por debajo de lo legible.
3105
3750
  maxWidth: '90vw',
3106
3751
  });
3107
3752
  return this.overlayRef;
@@ -3354,15 +3999,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
3354
3999
  * atributo porque el panel no necesita comportamiento propio, solo marcar
3355
4000
  * su presencia).
3356
4001
  *
3357
- * <div kf-folder-tabs-panel>...</div>
4002
+ * <div kfFolderTabsPanel>...</div>
3358
4003
  */
3359
4004
  class FolderTabsPanel {
3360
4005
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FolderTabsPanel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3361
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.0", type: FolderTabsPanel, isStandalone: true, selector: "[kf-folder-tabs-panel]", ngImport: i0 });
4006
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.0", type: FolderTabsPanel, isStandalone: true, selector: "[kfFolderTabsPanel]", ngImport: i0 });
3362
4007
  }
3363
4008
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FolderTabsPanel, decorators: [{
3364
4009
  type: Directive,
3365
- args: [{ selector: '[kf-folder-tabs-panel]' }]
4010
+ args: [{ selector: '[kfFolderTabsPanel]' }]
3366
4011
  }] });
3367
4012
  /**
3368
4013
  * Tabs con forma de "carpeta": el borde superior queda redondeado y el borde
@@ -3404,7 +4049,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
3404
4049
  * evita que cada consumidor repita esas reglas a mano.
3405
4050
  *
3406
4051
  * <kf-folder-tabs [options]="tabs" [value]="active" (valueChange)="active = $event">
3407
- * <div kf-folder-tabs-panel>contenido de la pestaña activa</div>
4052
+ * <div kfFolderTabsPanel>contenido de la pestaña activa</div>
3408
4053
  * </kf-folder-tabs>
3409
4054
  *
3410
4055
  * <!-- o, sin contenido, como antes: -->
@@ -3418,7 +4063,7 @@ class FolderTabs {
3418
4063
  valueChange = output();
3419
4064
  /**
3420
4065
  * Detecta si el consumidor proyectó contenido marcado con
3421
- * `kf-folder-tabs-panel` — sin `.required`, así que es `undefined`
4066
+ * `kfFolderTabsPanel` — sin `.required`, así que es `undefined`
3422
4067
  * cuando no hay nada proyectado (mismo patrón que usa `kf-dropdown` con
3423
4068
  * `contentChild(DropdownPanel)` para saber si hay un panel que
3424
4069
  * attachear). `hasPanel()` decide si el wrapper del panel participa del
@@ -3488,7 +4133,7 @@ class FolderTabs {
3488
4133
  </div>
3489
4134
 
3490
4135
  <div class="kf-folder-tabs__panel" [class.kf-folder-tabs__panel--empty]="!hasPanel()">
3491
- <ng-content select="[kf-folder-tabs-panel]" />
4136
+ <ng-content select="[kfFolderTabsPanel]" />
3492
4137
  </div>
3493
4138
  `, isInline: true, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-folder-tabs{display:flex;align-items:flex-end;gap:var(--kf-space-xs);perspective:900px}.kf-folder-tabs__tab{position:relative;display:inline-flex;align-items:center;gap:var(--kf-space-xs);font:inherit;font-size:.9rem;color:var(--kf-color-text-muted);background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-bottom-color:var(--kf-color-border-strong);border-radius:var(--kf-radius-sm) var(--kf-radius-sm) 0 0;padding:var(--kf-space-sm) var(--kf-space-md);cursor:pointer;transform-origin:bottom left;transform:translateY(5px) scale(.985);transition:background .14s,color .14s,border-color .14s,transform .22s cubic-bezier(.32,.72,0,1)}.kf-folder-tabs__tab:hover:not(.kf-folder-tabs__tab--active){color:var(--kf-color-text);background:var(--kf-color-white);transform:translateY(2px) scale(.99)}.kf-folder-tabs__tab:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus);z-index:1}.kf-folder-tabs__tab--active{z-index:1;color:var(--kf-color-primary);font-weight:700;background:var(--kf-color-white);border-color:var(--kf-color-border);border-bottom:2px solid var(--kf-color-primary);box-shadow:0 -6px 14px -10px #10182866;transform:translateY(1px);animation:kf-folder-tabs-pull .34s cubic-bezier(.22,1.2,.36,1) both}@keyframes kf-folder-tabs-pull{0%{transform:translateY(7px) rotateX(-10deg) scale(.97)}60%{transform:translateY(1px) rotateX(2deg) scale(1.01)}to{transform:translateY(1px) rotateX(0) scale(1)}}@media(prefers-reduced-motion:reduce){.kf-folder-tabs__tab,.kf-folder-tabs__tab--active{animation:none;transition:background .14s,color .14s,border-color .14s}}.kf-folder-tabs__panel--empty{display:contents}.kf-folder-tabs__panel:not(.kf-folder-tabs__panel--empty){background:var(--kf-color-white);border-right:1px solid var(--kf-color-border);border-bottom:1px solid var(--kf-color-border);border-left:1px solid var(--kf-color-border);border-top:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);border-top-left-radius:0;overflow:hidden}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }] });
3494
4139
  }
@@ -3518,11 +4163,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
3518
4163
  </div>
3519
4164
 
3520
4165
  <div class="kf-folder-tabs__panel" [class.kf-folder-tabs__panel--empty]="!hasPanel()">
3521
- <ng-content select="[kf-folder-tabs-panel]" />
4166
+ <ng-content select="[kfFolderTabsPanel]" />
3522
4167
  </div>
3523
4168
  `, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-folder-tabs{display:flex;align-items:flex-end;gap:var(--kf-space-xs);perspective:900px}.kf-folder-tabs__tab{position:relative;display:inline-flex;align-items:center;gap:var(--kf-space-xs);font:inherit;font-size:.9rem;color:var(--kf-color-text-muted);background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-bottom-color:var(--kf-color-border-strong);border-radius:var(--kf-radius-sm) var(--kf-radius-sm) 0 0;padding:var(--kf-space-sm) var(--kf-space-md);cursor:pointer;transform-origin:bottom left;transform:translateY(5px) scale(.985);transition:background .14s,color .14s,border-color .14s,transform .22s cubic-bezier(.32,.72,0,1)}.kf-folder-tabs__tab:hover:not(.kf-folder-tabs__tab--active){color:var(--kf-color-text);background:var(--kf-color-white);transform:translateY(2px) scale(.99)}.kf-folder-tabs__tab:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus);z-index:1}.kf-folder-tabs__tab--active{z-index:1;color:var(--kf-color-primary);font-weight:700;background:var(--kf-color-white);border-color:var(--kf-color-border);border-bottom:2px solid var(--kf-color-primary);box-shadow:0 -6px 14px -10px #10182866;transform:translateY(1px);animation:kf-folder-tabs-pull .34s cubic-bezier(.22,1.2,.36,1) both}@keyframes kf-folder-tabs-pull{0%{transform:translateY(7px) rotateX(-10deg) scale(.97)}60%{transform:translateY(1px) rotateX(2deg) scale(1.01)}to{transform:translateY(1px) rotateX(0) scale(1)}}@media(prefers-reduced-motion:reduce){.kf-folder-tabs__tab,.kf-folder-tabs__tab--active{animation:none;transition:background .14s,color .14s,border-color .14s}}.kf-folder-tabs__panel--empty{display:contents}.kf-folder-tabs__panel:not(.kf-folder-tabs__panel--empty){background:var(--kf-color-white);border-right:1px solid var(--kf-color-border);border-bottom:1px solid var(--kf-color-border);border-left:1px solid var(--kf-color-border);border-top:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md);border-top-left-radius:0;overflow:hidden}\n"] }]
3524
4169
  }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], projectedPanel: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FolderTabsPanel), { isSignal: true }] }] } });
3525
4170
 
4171
+ /**
4172
+ * Pie de página general del portal (página 38): logo + copyright a la
4173
+ * izquierda, links legales/soporte empujados a la derecha. Logo y copyright
4174
+ * son inputs (cada portal del ecosistema — LATAM, OTD, Star — trae su
4175
+ * propia marca y texto legal); `links` es la lista de accesos, típicamente
4176
+ * placeholders hasta que existan destinos reales.
4177
+ *
4178
+ * <kf-footer
4179
+ * logoSrc="images/logo-kaufmann-dark.png"
4180
+ * logoAlt="Grupo Kaufmann"
4181
+ * copyright="© 2026 Kaufmann LATAM · Todos los derechos reservados"
4182
+ * [links]="[
4183
+ * { label: 'Políticas de privacidad' },
4184
+ * { label: 'Contacto' },
4185
+ * { label: 'Sugerencias y reclamos' },
4186
+ * ]"
4187
+ * />
4188
+ */
4189
+ class Footer {
4190
+ logoSrc = input(/* @ts-ignore */
4191
+ ...(ngDevMode ? [undefined, { debugName: "logoSrc" }] : /* istanbul ignore next */ []));
4192
+ logoAlt = input('', /* @ts-ignore */
4193
+ ...(ngDevMode ? [{ debugName: "logoAlt" }] : /* istanbul ignore next */ []));
4194
+ copyright = input.required(/* @ts-ignore */
4195
+ ...(ngDevMode ? [{ debugName: "copyright" }] : /* istanbul ignore next */ []));
4196
+ links = input([], /* @ts-ignore */
4197
+ ...(ngDevMode ? [{ debugName: "links" }] : /* istanbul ignore next */ []));
4198
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Footer, deps: [], target: i0.ɵɵFactoryTarget.Component });
4199
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: Footer, isStandalone: true, selector: "kf-footer", inputs: { logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, logoAlt: { classPropertyName: "logoAlt", publicName: "logoAlt", isSignal: true, isRequired: false, transformFunction: null }, copyright: { classPropertyName: "copyright", publicName: "copyright", isSignal: true, isRequired: true, transformFunction: null }, links: { classPropertyName: "links", publicName: "links", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
4200
+ <div class="kf-footer__wrap">
4201
+ @if (logoSrc()) {
4202
+ <img class="kf-footer__logo" [src]="logoSrc()" [alt]="logoAlt()" />
4203
+ }
4204
+ <span class="kf-footer__copyright">{{ copyright() }}</span>
4205
+ @if (links().length) {
4206
+ <nav class="kf-footer__links">
4207
+ @for (link of links(); track link.label) {
4208
+ <a [href]="link.href || '#'">{{ link.label }}</a>
4209
+ }
4210
+ </nav>
4211
+ }
4212
+ </div>
4213
+ `, isInline: true, styles: [":host{display:block;margin-top:auto;background:var(--kf-color-tertiary-deep)}.kf-footer__wrap{max-width:var(--kf-footer-max-width, none);margin:0 auto;display:flex;align-items:center;flex-wrap:wrap;gap:var(--kf-space-lg);min-height:4.5rem;padding:var(--kf-space-md) var(--kf-space-lg)}.kf-footer__logo{height:1.375rem;opacity:.9}.kf-footer__copyright{font-size:.8125rem;color:#ffffff8c;font-family:var(--kf-font-body)}.kf-footer__links{margin-left:auto;display:flex;flex-wrap:wrap;gap:var(--kf-space-lg)}.kf-footer__links a{font-size:.8125rem;color:#ffffffa8;text-decoration:none;font-family:var(--kf-font-body)}.kf-footer__links a:hover{color:#fff}@media(max-width:640px){.kf-footer__wrap{flex-direction:column;align-items:flex-start}.kf-footer__links{margin-left:0}}\n"] });
4214
+ }
4215
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Footer, decorators: [{
4216
+ type: Component,
4217
+ args: [{ selector: 'kf-footer', template: `
4218
+ <div class="kf-footer__wrap">
4219
+ @if (logoSrc()) {
4220
+ <img class="kf-footer__logo" [src]="logoSrc()" [alt]="logoAlt()" />
4221
+ }
4222
+ <span class="kf-footer__copyright">{{ copyright() }}</span>
4223
+ @if (links().length) {
4224
+ <nav class="kf-footer__links">
4225
+ @for (link of links(); track link.label) {
4226
+ <a [href]="link.href || '#'">{{ link.label }}</a>
4227
+ }
4228
+ </nav>
4229
+ }
4230
+ </div>
4231
+ `, styles: [":host{display:block;margin-top:auto;background:var(--kf-color-tertiary-deep)}.kf-footer__wrap{max-width:var(--kf-footer-max-width, none);margin:0 auto;display:flex;align-items:center;flex-wrap:wrap;gap:var(--kf-space-lg);min-height:4.5rem;padding:var(--kf-space-md) var(--kf-space-lg)}.kf-footer__logo{height:1.375rem;opacity:.9}.kf-footer__copyright{font-size:.8125rem;color:#ffffff8c;font-family:var(--kf-font-body)}.kf-footer__links{margin-left:auto;display:flex;flex-wrap:wrap;gap:var(--kf-space-lg)}.kf-footer__links a{font-size:.8125rem;color:#ffffffa8;text-decoration:none;font-family:var(--kf-font-body)}.kf-footer__links a:hover{color:#fff}@media(max-width:640px){.kf-footer__wrap{flex-direction:column;align-items:flex-start}.kf-footer__links{margin-left:0}}\n"] }]
4232
+ }], propDecorators: { logoSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoSrc", required: false }] }], logoAlt: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoAlt", required: false }] }], copyright: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyright", required: true }] }], links: [{ type: i0.Input, args: [{ isSignal: true, alias: "links", required: false }] }] } });
4233
+
3526
4234
  /**
3527
4235
  * Input de formulario apilado: label arriba (en mayúsculas vía CSS), campo con
3528
4236
  * fondo gris que pasa a blanco con anillo de foco azul al enfocar.
@@ -4073,18 +4781,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4073
4781
 
4074
4782
  let nextId = 0;
4075
4783
  /**
4076
- * Dropdown de selección múltiple con checkboxes internos, siguiendo el mismo
4077
- * patrón de panel flotante que `SelectMenu` (panel con `position: absolute`
4078
- * sobre el host, sin Angular CDK Overlay, que no es dependencia del proyecto).
4079
- * Se abre y cierra por click en el disparador, y se cierra al hacer click
4080
- * afuera o al presionar Escape.
4081
- *
4082
- * A diferencia de `SelectMenu`, el disparador no muestra un único valor sino
4083
- * los nombres seleccionados junto a un badge circular con el conteo (página
4084
- * 12: "Sucursal Norte, Lima Centro" + badge "2"), y cada opción del panel se
4085
- * marca/desmarca con un checkbox sin cerrar el panel. Incluye una opción
4086
- * "Todas" al inicio de la lista, con estado `indeterminate` cuando hay
4087
- * selección parcial — el mismo patrón de "seleccionar todos" de `Checkbox`.
4784
+ * Dropdown de selección múltiple con checkboxes internos. El panel flotante
4785
+ * se conecta al trigger vía `@angular/cdk/overlay` — el mismo mecanismo que
4786
+ * `Select` (ver `../select/Select.ts`) y `Dropdown` (`../dropdown/Dropdown.ts`).
4787
+ *
4788
+ * Hasta la `0.1.9` este panel vivía con `position: absolute` manual sobre el
4789
+ * propio host, sin depender del CDK Overlay. Se migró para unificar el
4790
+ * mecanismo de posicionamiento/cierre con el resto de los dropdowns flotantes
4791
+ * de la librería (4 posiciones de fallback, `withViewportMargin`, `withPush`,
4792
+ * cierre por `outsidePointerEvents()`/`keydownEvents()` en vez de un
4793
+ * `@HostListener('document:click')` manual).
4794
+ *
4795
+ * A diferencia de `Select`, el disparador no muestra un único valor sino los
4796
+ * nombres seleccionados junto a un badge circular con el conteo, y cada
4797
+ * opción del panel se marca/desmarca con un checkbox sin cerrar el panel.
4798
+ * Incluye una opción "Todas" al inicio de la lista, con estado
4799
+ * `indeterminate` cuando hay selección parcial — el mismo patrón de
4800
+ * "seleccionar todos" de `Checkbox`. "Todas" nunca se filtra por el buscador:
4801
+ * queda siempre visible y sigue actuando sobre el universo COMPLETO de
4802
+ * `options()`, no sobre el subconjunto filtrado (ver JSDoc de `toggleAll()`).
4088
4803
  *
4089
4804
  * <kf-multi-select
4090
4805
  * label="Sucursales"
@@ -4107,18 +4822,43 @@ class MultiSelect {
4107
4822
  placeholder = input('Seleccionar', /* @ts-ignore */
4108
4823
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
4109
4824
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4825
+ /**
4826
+ * Tri-state: si el consumidor no lo especifica (`undefined`), el buscador
4827
+ * se activa solo cuando `options().length` supera `searchThreshold()`. Si
4828
+ * el consumidor define `true`/`false` explícitamente, ese valor manda por
4829
+ * sobre el umbral automático. Mismo criterio que `Select`.
4830
+ */
4831
+ searchable = input(undefined, /* @ts-ignore */
4832
+ ...(ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []));
4833
+ searchThreshold = input(7, /* @ts-ignore */
4834
+ ...(ngDevMode ? [{ debugName: "searchThreshold" }] : /* istanbul ignore next */ []));
4110
4835
  valuesChange = output();
4111
4836
  instanceId = `kf-multi-select-${nextId++}`;
4112
4837
  triggerId = `${this.instanceId}-trigger`;
4113
4838
  listboxId = `${this.instanceId}-listbox`;
4114
4839
  isOpen = signal(false, /* @ts-ignore */
4115
4840
  ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
4841
+ searchQuery = signal('', /* @ts-ignore */
4842
+ ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
4843
+ activeIndex = signal(0, /* @ts-ignore */
4844
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
4116
4845
  selectedCount = computed(() => this.values().length, /* @ts-ignore */
4117
4846
  ...(ngDevMode ? [{ debugName: "selectedCount" }] : /* istanbul ignore next */ []));
4118
4847
  allSelected = computed(() => this.options().length > 0 && this.values().length === this.options().length, /* @ts-ignore */
4119
4848
  ...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
4120
4849
  isPartiallySelected = computed(() => this.values().length > 0 && !this.allSelected(), /* @ts-ignore */
4121
4850
  ...(ngDevMode ? [{ debugName: "isPartiallySelected" }] : /* istanbul ignore next */ []));
4851
+ showSearch = computed(() => this.searchable() ?? this.options().length > this.searchThreshold(), /* @ts-ignore */
4852
+ ...(ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []));
4853
+ /** Opciones visibles tras aplicar el filtro de búsqueda ("Todas" nunca se filtra). */
4854
+ filteredOptions = computed(() => {
4855
+ const query = normalizeForSearch(this.searchQuery().trim());
4856
+ if (!query) {
4857
+ return this.options();
4858
+ }
4859
+ return this.options().filter((option) => normalizeForSearch(option.label).includes(query));
4860
+ }, /* @ts-ignore */
4861
+ ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
4122
4862
  /**
4123
4863
  * Texto del disparador: placeholder si no hay selección, o los nombres
4124
4864
  * seleccionados unidos por coma (el conteo va aparte, en el badge). Con
@@ -4136,7 +4876,37 @@ class MultiSelect {
4136
4876
  return selectedLabels.join(', ');
4137
4877
  }, /* @ts-ignore */
4138
4878
  ...(ngDevMode ? [{ debugName: "summaryText" }] : /* istanbul ignore next */ []));
4139
- host = inject((ElementRef));
4879
+ triggerRef = viewChild.required('trigger', /* @ts-ignore */
4880
+ ...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
4881
+ panelTemplate = viewChild.required('panelTemplate', /* @ts-ignore */
4882
+ ...(ngDevMode ? [{ debugName: "panelTemplate" }] : /* istanbul ignore next */ []));
4883
+ searchInputRef = viewChild('searchInput', /* @ts-ignore */
4884
+ ...(ngDevMode ? [{ debugName: "searchInputRef" }] : /* istanbul ignore next */ []));
4885
+ overlayService = inject(Overlay);
4886
+ viewContainerRef = inject(ViewContainerRef);
4887
+ destroyRef = inject(DestroyRef);
4888
+ overlayRef = null;
4889
+ constructor() {
4890
+ effect(() => {
4891
+ if (this.isOpen()) {
4892
+ this.attach();
4893
+ }
4894
+ else {
4895
+ this.detach();
4896
+ this.searchQuery.set('');
4897
+ this.activeIndex.set(0);
4898
+ }
4899
+ });
4900
+ // Autofocus del buscador al abrir — mismo patrón que Select. afterNextRender()
4901
+ // no puede invocarse dentro de un effect() (NG0203); afterRenderEffect() sí
4902
+ // es reactivo a signals y corre tras la actualización del DOM del panel.
4903
+ afterRenderEffect(() => {
4904
+ if (this.isOpen() && this.showSearch()) {
4905
+ this.searchInputRef()?.nativeElement.focus();
4906
+ }
4907
+ });
4908
+ this.destroyRef.onDestroy(() => this.overlayRef?.dispose());
4909
+ }
4140
4910
  isSelected(value) {
4141
4911
  return this.values().includes(value);
4142
4912
  }
@@ -4153,20 +4923,100 @@ class MultiSelect {
4153
4923
  : [...current, value];
4154
4924
  this.valuesChange.emit(next);
4155
4925
  }
4156
- /** Alterna entre "todas seleccionadas" y "ninguna seleccionada". */
4926
+ /**
4927
+ * Alterna entre "todas seleccionadas" y "ninguna seleccionada". Actúa
4928
+ * siempre sobre el universo COMPLETO de `options()`, no sobre
4929
+ * `filteredOptions()` — con un buscador activo, "Todas" no se filtra ni
4930
+ * cambia de significado: seguir marcando/desmarcando únicamente lo visible
4931
+ * sería sorprendente para quien ya tenía otras opciones (no visibles en el
4932
+ * filtro actual) seleccionadas de antes.
4933
+ */
4157
4934
  toggleAll() {
4158
4935
  this.valuesChange.emit(this.allSelected() ? [] : this.options().map((option) => option.value));
4159
4936
  }
4160
- onDocumentClick(event) {
4161
- if (this.isOpen() && !this.host.nativeElement.contains(event.target)) {
4162
- this.isOpen.set(false);
4937
+ onSearchInput(event) {
4938
+ this.searchQuery.set(event.target.value);
4939
+ this.activeIndex.set(0);
4940
+ }
4941
+ /** Mueve `activeIndex` sobre las opciones YA FILTRADAS visibles. Clamp simple, sin wraparound. */
4942
+ moveActive(delta) {
4943
+ const count = this.filteredOptions().length;
4944
+ if (count === 0) {
4945
+ return;
4946
+ }
4947
+ this.activeIndex.update((current) => Math.min(Math.max(current + delta, 0), count - 1));
4948
+ }
4949
+ toggleActive() {
4950
+ const option = this.filteredOptions()[this.activeIndex()];
4951
+ if (option) {
4952
+ this.toggleOption(option.value);
4163
4953
  }
4164
4954
  }
4165
4955
  onEscape() {
4166
4956
  this.isOpen.set(false);
4167
4957
  }
4958
+ attach() {
4959
+ if (this.overlayRef?.hasAttached()) {
4960
+ return;
4961
+ }
4962
+ const overlayRef = this.getOrCreateOverlayRef();
4963
+ // Se remide el ancho del trigger en cada apertura, no solo la primera —
4964
+ // ver la misma nota en Select: cachear el ancho en la creación del
4965
+ // overlay lo deja desincronizado si el trigger cambia de tamaño entre
4966
+ // una apertura y la siguiente.
4967
+ overlayRef.updateSize({ width: this.triggerRef().nativeElement.getBoundingClientRect().width });
4968
+ const portal = new TemplatePortal(this.panelTemplate(), this.viewContainerRef);
4969
+ overlayRef.attach(portal);
4970
+ // outsidePointerEvents() dispara en la fase de CAPTURA de `click` sobre
4971
+ // `body` (ver OverlayOutsideClickDispatcher en @angular/cdk), ANTES de
4972
+ // que el evento llegue al trigger y corra (click)="toggle()". Sin este
4973
+ // filtro, un click en el trigger para CERRAR el panel ya abierto hacía:
4974
+ // 1) este subscribe lo cierra primero (el trigger no es parte del
4975
+ // overlay, cuenta como "afuera"), 2) toggle() corre después y lo vuelve a
4976
+ // abrir porque ve isOpen ya en false — un solo click quedaba en no-op y
4977
+ // solo cerraba de verdad un click fuera del trigger. Mismo fix que Select
4978
+ // y Dropdown (ver notas ahí).
4979
+ overlayRef.outsidePointerEvents().subscribe((event) => {
4980
+ const target = event.target;
4981
+ if (target && this.triggerRef().nativeElement.contains(target)) {
4982
+ return;
4983
+ }
4984
+ this.isOpen.set(false);
4985
+ });
4986
+ overlayRef.keydownEvents().subscribe((event) => {
4987
+ if (event.key === 'Escape') {
4988
+ this.isOpen.set(false);
4989
+ }
4990
+ });
4991
+ }
4992
+ detach() {
4993
+ this.overlayRef?.detach();
4994
+ }
4995
+ getOrCreateOverlayRef() {
4996
+ if (this.overlayRef) {
4997
+ return this.overlayRef;
4998
+ }
4999
+ const positionStrategy = this.overlayService
5000
+ .position()
5001
+ .flexibleConnectedTo(this.triggerRef())
5002
+ .withPositions([
5003
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
5004
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
5005
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
5006
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
5007
+ ])
5008
+ .withViewportMargin(8)
5009
+ .withPush(true);
5010
+ this.overlayRef = this.overlayService.create({
5011
+ positionStrategy,
5012
+ scrollStrategy: this.overlayService.scrollStrategies.reposition(),
5013
+ width: this.triggerRef().nativeElement.getBoundingClientRect().width,
5014
+ maxWidth: '90vw',
5015
+ });
5016
+ return this.overlayRef;
5017
+ }
4168
5018
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: MultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
4169
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: MultiSelect, isStandalone: true, selector: "kf-multi-select", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, values: { classPropertyName: "values", publicName: "values", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valuesChange: "valuesChange" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: `
5019
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: MultiSelect, isStandalone: true, selector: "kf-multi-select", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, values: { classPropertyName: "values", publicName: "values", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchThreshold: { classPropertyName: "searchThreshold", publicName: "searchThreshold", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valuesChange: "valuesChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelTemplate", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }, { propertyName: "searchInputRef", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
4170
5020
  @if (label()) {
4171
5021
  <kf-field-label [for]="triggerId">{{ label() }}</kf-field-label>
4172
5022
  }
@@ -4196,55 +5046,78 @@ class MultiSelect {
4196
5046
  aria-hidden="true"
4197
5047
  ></span>
4198
5048
  </button>
4199
- @if (isOpen()) {
4200
- <ul
4201
- class="kf-multi-select__panel"
4202
- [id]="listboxId"
4203
- role="listbox"
4204
- aria-multiselectable="true"
4205
- [attr.aria-labelledby]="triggerId"
4206
- >
4207
- @if (options().length > 0) {
4208
- <li
4209
- class="kf-multi-select__option"
4210
- role="option"
4211
- tabindex="0"
4212
- [attr.aria-selected]="allSelected()"
4213
- (click)="toggleAll()"
4214
- (keydown.enter)="toggleAll()"
4215
- (keydown.space)="$event.preventDefault(); toggleAll()"
4216
- >
4217
- <span
4218
- class="kf-multi-select__checkbox"
4219
- [class.kf-multi-select__checkbox--checked]="allSelected()"
4220
- [class.kf-multi-select__checkbox--indeterminate]="isPartiallySelected()"
4221
- ></span>
4222
- <span class="kf-multi-select__option-label">Todas</span>
4223
- </li>
4224
- }
4225
- @for (option of options(); track option.value) {
4226
- <li
4227
- class="kf-multi-select__option"
4228
- [class.kf-multi-select__option--selected]="isSelected(option.value)"
4229
- role="option"
4230
- tabindex="0"
4231
- [attr.aria-selected]="isSelected(option.value)"
4232
- (click)="toggleOption(option.value)"
4233
- (keydown.enter)="toggleOption(option.value)"
4234
- (keydown.space)="$event.preventDefault(); toggleOption(option.value)"
4235
- >
4236
- <span
4237
- class="kf-multi-select__checkbox"
4238
- [class.kf-multi-select__checkbox--checked]="isSelected(option.value)"
4239
- ></span>
4240
- <span class="kf-multi-select__option-label">{{ option.label }}</span>
4241
- </li>
4242
- } @empty {
4243
- <li class="kf-multi-select__empty">Sin opciones disponibles</li>
5049
+ <ng-template #panelTemplate>
5050
+ <div class="kf-multi-select__panel">
5051
+ @if (showSearch()) {
5052
+ <input
5053
+ #searchInput
5054
+ type="text"
5055
+ class="kf-multi-select__search"
5056
+ placeholder="Buscar…"
5057
+ [value]="searchQuery()"
5058
+ (input)="onSearchInput($event)"
5059
+ (keydown.arrowdown)="$event.preventDefault(); moveActive(1)"
5060
+ (keydown.arrowup)="$event.preventDefault(); moveActive(-1)"
5061
+ (keydown.enter)="$event.preventDefault(); toggleActive()"
5062
+ />
4244
5063
  }
4245
- </ul>
4246
- }
4247
- `, isInline: true, styles: [":host{display:inline-flex;flex-direction:column;gap:var(--kf-space-xs);position:relative;font:inherit;width:240px}.kf-multi-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);width:100%;font:inherit;font-family:var(--kf-font-body);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);cursor:pointer;transition:border-color .14s,box-shadow .14s}.kf-multi-select__trigger:hover:not(:disabled){border-color:var(--kf-color-border-strong)}.kf-multi-select__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-multi-select__trigger:disabled{opacity:.5;cursor:not-allowed}.kf-multi-select__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;text-align:left}.kf-multi-select__value--placeholder{color:var(--kf-color-text-muted)}.kf-multi-select__count{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:1.25rem;height:1.25rem;padding:0 5px;border-radius:var(--kf-radius-pill);background:var(--kf-color-primary);color:var(--kf-color-white);font-size:.72rem;font-weight:700}.kf-multi-select__caret{flex-shrink:0;width:8px;height:8px;border-right:2px solid var(--kf-color-text-muted);border-bottom:2px solid var(--kf-color-text-muted);transform:rotate(45deg);transition:transform .14s;margin-top:-4px}.kf-multi-select__caret--open{transform:rotate(-135deg);margin-top:4px}.kf-multi-select__panel{position:absolute;top:100%;left:0;right:0;z-index:10;margin:var(--kf-space-xs) 0 0;padding:var(--kf-space-xs) 0;list-style:none;max-height:240px;overflow-y:auto;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-multi-select__option{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-multi-select__option:hover{background:var(--kf-color-primary-soft)}.kf-multi-select__option--selected{color:var(--kf-color-primary);font-weight:600}.kf-multi-select__checkbox{display:inline-flex;flex-shrink:0;width:var(--kf-space-md);height:var(--kf-space-md);border:1px solid var(--kf-color-border-strong);border-radius:var(--kf-radius-xs);background:var(--kf-color-white);transition:background .14s,border-color .14s;position:relative}.kf-multi-select__checkbox--checked{background:var(--kf-color-primary);border-color:var(--kf-color-primary)}.kf-multi-select__checkbox--checked:after{content:\"\";position:absolute;left:4px;top:1px;width:4px;height:8px;border:solid var(--kf-color-white);border-width:0 2px 2px 0;transform:rotate(45deg)}.kf-multi-select__checkbox--indeterminate{background:var(--kf-color-primary);border-color:var(--kf-color-primary)}.kf-multi-select__checkbox--indeterminate:after{content:\"\";position:absolute;left:3px;top:6px;width:8px;height:2px;background:var(--kf-color-white);border:none;transform:none}.kf-multi-select__option-label{overflow:hidden;text-overflow:ellipsis}.kf-multi-select__empty{padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text-muted)}\n"], dependencies: [{ kind: "component", type: FieldLabel, selector: "kf-field-label", inputs: ["for", "required"] }] });
5064
+ <ul
5065
+ class="kf-multi-select__list"
5066
+ [id]="listboxId"
5067
+ role="listbox"
5068
+ aria-multiselectable="true"
5069
+ [attr.aria-labelledby]="triggerId"
5070
+ >
5071
+ @if (options().length > 0) {
5072
+ <li
5073
+ class="kf-multi-select__option"
5074
+ role="option"
5075
+ tabindex="0"
5076
+ [attr.aria-selected]="allSelected()"
5077
+ (click)="toggleAll()"
5078
+ (keydown.enter)="toggleAll()"
5079
+ (keydown.space)="$event.preventDefault(); toggleAll()"
5080
+ >
5081
+ <span
5082
+ class="kf-multi-select__checkbox"
5083
+ [class.kf-multi-select__checkbox--checked]="allSelected()"
5084
+ [class.kf-multi-select__checkbox--indeterminate]="isPartiallySelected()"
5085
+ ></span>
5086
+ <span class="kf-multi-select__option-label">Todas</span>
5087
+ </li>
5088
+ }
5089
+ @for (option of filteredOptions(); track option.value; let i = $index) {
5090
+ <li
5091
+ class="kf-multi-select__option"
5092
+ [class.kf-multi-select__option--selected]="isSelected(option.value)"
5093
+ [class.kf-multi-select__option--active]="i === activeIndex()"
5094
+ role="option"
5095
+ tabindex="0"
5096
+ [attr.aria-selected]="isSelected(option.value)"
5097
+ (click)="toggleOption(option.value)"
5098
+ (mouseenter)="activeIndex.set(i)"
5099
+ (keydown.enter)="toggleOption(option.value)"
5100
+ (keydown.space)="$event.preventDefault(); toggleOption(option.value)"
5101
+ (keydown.arrowdown)="$event.preventDefault(); moveActive(1)"
5102
+ (keydown.arrowup)="$event.preventDefault(); moveActive(-1)"
5103
+ >
5104
+ <span
5105
+ class="kf-multi-select__checkbox"
5106
+ [class.kf-multi-select__checkbox--checked]="isSelected(option.value)"
5107
+ ></span>
5108
+ <span class="kf-multi-select__option-label">{{ option.label }}</span>
5109
+ </li>
5110
+ } @empty {
5111
+ @if (options().length > 0) {
5112
+ <li class="kf-multi-select__empty">Sin resultados</li>
5113
+ } @else {
5114
+ <li class="kf-multi-select__empty">Sin opciones disponibles</li>
5115
+ }
5116
+ }
5117
+ </ul>
5118
+ </div>
5119
+ </ng-template>
5120
+ `, isInline: true, styles: [":host{display:inline-flex;flex-direction:column;gap:var(--kf-space-xs);font:inherit;width:240px}.kf-multi-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);width:100%;font:inherit;font-family:var(--kf-font-body);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);cursor:pointer;transition:border-color .14s,box-shadow .14s}.kf-multi-select__trigger:hover:not(:disabled){border-color:var(--kf-color-border-strong)}.kf-multi-select__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-multi-select__trigger:disabled{opacity:.5;cursor:not-allowed}.kf-multi-select__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;text-align:left}.kf-multi-select__value--placeholder{color:var(--kf-color-text-muted)}.kf-multi-select__count{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:1.25rem;height:1.25rem;padding:0 5px;border-radius:var(--kf-radius-pill);background:var(--kf-color-primary);color:var(--kf-color-white);font-size:.72rem;font-weight:700}.kf-multi-select__caret{flex-shrink:0;width:8px;height:8px;border-right:2px solid var(--kf-color-text-muted);border-bottom:2px solid var(--kf-color-text-muted);transform:rotate(45deg);transition:transform .14s;margin-top:-4px}.kf-multi-select__caret--open{transform:rotate(-135deg);margin-top:4px}.kf-multi-select__panel{box-sizing:border-box;width:100%;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-md);overflow:hidden}.kf-multi-select__search{box-sizing:border-box;width:100%;font:inherit;font-family:var(--kf-font-body);color:var(--kf-color-text);padding:var(--kf-space-sm) var(--kf-space-md);border:none;border-bottom:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm) var(--kf-radius-sm) 0 0;outline:none}.kf-multi-select__search:focus-visible{box-shadow:inset 0 0 0 2px var(--kf-color-primary)}.kf-multi-select__list{box-sizing:border-box;width:100%;margin:0;padding:var(--kf-space-xs) 0;list-style:none;max-height:240px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--kf-color-gray-400) transparent}.kf-multi-select__list::-webkit-scrollbar{width:6px}.kf-multi-select__list::-webkit-scrollbar-track{background:transparent}.kf-multi-select__list::-webkit-scrollbar-thumb{background-color:var(--kf-color-gray-400);border-radius:var(--kf-radius-pill)}.kf-multi-select__list::-webkit-scrollbar-thumb:hover{background-color:var(--kf-color-gray-500)}.kf-multi-select__option{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-multi-select__option:hover{background:var(--kf-color-primary-soft)}.kf-multi-select__option--selected{color:var(--kf-color-primary);font-weight:600}.kf-multi-select__option--active{background:var(--kf-color-primary-soft)}.kf-multi-select__checkbox{display:inline-flex;flex-shrink:0;width:var(--kf-space-md);height:var(--kf-space-md);border:1px solid var(--kf-color-border-strong);border-radius:var(--kf-radius-xs);background:var(--kf-color-white);transition:background .14s,border-color .14s;position:relative}.kf-multi-select__checkbox--checked{background:var(--kf-color-primary);border-color:var(--kf-color-primary)}.kf-multi-select__checkbox--checked:after{content:\"\";position:absolute;left:4px;top:1px;width:4px;height:8px;border:solid var(--kf-color-white);border-width:0 2px 2px 0;transform:rotate(45deg)}.kf-multi-select__checkbox--indeterminate{background:var(--kf-color-primary);border-color:var(--kf-color-primary)}.kf-multi-select__checkbox--indeterminate:after{content:\"\";position:absolute;left:3px;top:6px;width:8px;height:2px;background:var(--kf-color-white);border:none;transform:none}.kf-multi-select__option-label{overflow:hidden;text-overflow:ellipsis}.kf-multi-select__empty{text-align:center;padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text-muted)}\n"], dependencies: [{ kind: "component", type: FieldLabel, selector: "kf-field-label", inputs: ["for", "required"] }] });
4248
5121
  }
4249
5122
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: MultiSelect, decorators: [{
4250
5123
  type: Component,
@@ -4278,63 +5151,191 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4278
5151
  aria-hidden="true"
4279
5152
  ></span>
4280
5153
  </button>
4281
- @if (isOpen()) {
4282
- <ul
4283
- class="kf-multi-select__panel"
4284
- [id]="listboxId"
4285
- role="listbox"
4286
- aria-multiselectable="true"
4287
- [attr.aria-labelledby]="triggerId"
4288
- >
4289
- @if (options().length > 0) {
4290
- <li
4291
- class="kf-multi-select__option"
4292
- role="option"
4293
- tabindex="0"
4294
- [attr.aria-selected]="allSelected()"
4295
- (click)="toggleAll()"
4296
- (keydown.enter)="toggleAll()"
4297
- (keydown.space)="$event.preventDefault(); toggleAll()"
4298
- >
4299
- <span
4300
- class="kf-multi-select__checkbox"
4301
- [class.kf-multi-select__checkbox--checked]="allSelected()"
4302
- [class.kf-multi-select__checkbox--indeterminate]="isPartiallySelected()"
4303
- ></span>
4304
- <span class="kf-multi-select__option-label">Todas</span>
4305
- </li>
4306
- }
4307
- @for (option of options(); track option.value) {
4308
- <li
4309
- class="kf-multi-select__option"
4310
- [class.kf-multi-select__option--selected]="isSelected(option.value)"
4311
- role="option"
4312
- tabindex="0"
4313
- [attr.aria-selected]="isSelected(option.value)"
4314
- (click)="toggleOption(option.value)"
4315
- (keydown.enter)="toggleOption(option.value)"
4316
- (keydown.space)="$event.preventDefault(); toggleOption(option.value)"
4317
- >
4318
- <span
4319
- class="kf-multi-select__checkbox"
4320
- [class.kf-multi-select__checkbox--checked]="isSelected(option.value)"
4321
- ></span>
4322
- <span class="kf-multi-select__option-label">{{ option.label }}</span>
4323
- </li>
4324
- } @empty {
4325
- <li class="kf-multi-select__empty">Sin opciones disponibles</li>
5154
+ <ng-template #panelTemplate>
5155
+ <div class="kf-multi-select__panel">
5156
+ @if (showSearch()) {
5157
+ <input
5158
+ #searchInput
5159
+ type="text"
5160
+ class="kf-multi-select__search"
5161
+ placeholder="Buscar…"
5162
+ [value]="searchQuery()"
5163
+ (input)="onSearchInput($event)"
5164
+ (keydown.arrowdown)="$event.preventDefault(); moveActive(1)"
5165
+ (keydown.arrowup)="$event.preventDefault(); moveActive(-1)"
5166
+ (keydown.enter)="$event.preventDefault(); toggleActive()"
5167
+ />
4326
5168
  }
4327
- </ul>
4328
- }
4329
- `, styles: [":host{display:inline-flex;flex-direction:column;gap:var(--kf-space-xs);position:relative;font:inherit;width:240px}.kf-multi-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);width:100%;font:inherit;font-family:var(--kf-font-body);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);cursor:pointer;transition:border-color .14s,box-shadow .14s}.kf-multi-select__trigger:hover:not(:disabled){border-color:var(--kf-color-border-strong)}.kf-multi-select__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-multi-select__trigger:disabled{opacity:.5;cursor:not-allowed}.kf-multi-select__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;text-align:left}.kf-multi-select__value--placeholder{color:var(--kf-color-text-muted)}.kf-multi-select__count{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:1.25rem;height:1.25rem;padding:0 5px;border-radius:var(--kf-radius-pill);background:var(--kf-color-primary);color:var(--kf-color-white);font-size:.72rem;font-weight:700}.kf-multi-select__caret{flex-shrink:0;width:8px;height:8px;border-right:2px solid var(--kf-color-text-muted);border-bottom:2px solid var(--kf-color-text-muted);transform:rotate(45deg);transition:transform .14s;margin-top:-4px}.kf-multi-select__caret--open{transform:rotate(-135deg);margin-top:4px}.kf-multi-select__panel{position:absolute;top:100%;left:0;right:0;z-index:10;margin:var(--kf-space-xs) 0 0;padding:var(--kf-space-xs) 0;list-style:none;max-height:240px;overflow-y:auto;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-multi-select__option{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-multi-select__option:hover{background:var(--kf-color-primary-soft)}.kf-multi-select__option--selected{color:var(--kf-color-primary);font-weight:600}.kf-multi-select__checkbox{display:inline-flex;flex-shrink:0;width:var(--kf-space-md);height:var(--kf-space-md);border:1px solid var(--kf-color-border-strong);border-radius:var(--kf-radius-xs);background:var(--kf-color-white);transition:background .14s,border-color .14s;position:relative}.kf-multi-select__checkbox--checked{background:var(--kf-color-primary);border-color:var(--kf-color-primary)}.kf-multi-select__checkbox--checked:after{content:\"\";position:absolute;left:4px;top:1px;width:4px;height:8px;border:solid var(--kf-color-white);border-width:0 2px 2px 0;transform:rotate(45deg)}.kf-multi-select__checkbox--indeterminate{background:var(--kf-color-primary);border-color:var(--kf-color-primary)}.kf-multi-select__checkbox--indeterminate:after{content:\"\";position:absolute;left:3px;top:6px;width:8px;height:2px;background:var(--kf-color-white);border:none;transform:none}.kf-multi-select__option-label{overflow:hidden;text-overflow:ellipsis}.kf-multi-select__empty{padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text-muted)}\n"] }]
4330
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], values: [{ type: i0.Input, args: [{ isSignal: true, alias: "values", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], valuesChange: [{ type: i0.Output, args: ["valuesChange"] }], onDocumentClick: [{
4331
- type: HostListener,
4332
- args: ['document:click', ['$event']]
4333
- }], onEscape: [{
5169
+ <ul
5170
+ class="kf-multi-select__list"
5171
+ [id]="listboxId"
5172
+ role="listbox"
5173
+ aria-multiselectable="true"
5174
+ [attr.aria-labelledby]="triggerId"
5175
+ >
5176
+ @if (options().length > 0) {
5177
+ <li
5178
+ class="kf-multi-select__option"
5179
+ role="option"
5180
+ tabindex="0"
5181
+ [attr.aria-selected]="allSelected()"
5182
+ (click)="toggleAll()"
5183
+ (keydown.enter)="toggleAll()"
5184
+ (keydown.space)="$event.preventDefault(); toggleAll()"
5185
+ >
5186
+ <span
5187
+ class="kf-multi-select__checkbox"
5188
+ [class.kf-multi-select__checkbox--checked]="allSelected()"
5189
+ [class.kf-multi-select__checkbox--indeterminate]="isPartiallySelected()"
5190
+ ></span>
5191
+ <span class="kf-multi-select__option-label">Todas</span>
5192
+ </li>
5193
+ }
5194
+ @for (option of filteredOptions(); track option.value; let i = $index) {
5195
+ <li
5196
+ class="kf-multi-select__option"
5197
+ [class.kf-multi-select__option--selected]="isSelected(option.value)"
5198
+ [class.kf-multi-select__option--active]="i === activeIndex()"
5199
+ role="option"
5200
+ tabindex="0"
5201
+ [attr.aria-selected]="isSelected(option.value)"
5202
+ (click)="toggleOption(option.value)"
5203
+ (mouseenter)="activeIndex.set(i)"
5204
+ (keydown.enter)="toggleOption(option.value)"
5205
+ (keydown.space)="$event.preventDefault(); toggleOption(option.value)"
5206
+ (keydown.arrowdown)="$event.preventDefault(); moveActive(1)"
5207
+ (keydown.arrowup)="$event.preventDefault(); moveActive(-1)"
5208
+ >
5209
+ <span
5210
+ class="kf-multi-select__checkbox"
5211
+ [class.kf-multi-select__checkbox--checked]="isSelected(option.value)"
5212
+ ></span>
5213
+ <span class="kf-multi-select__option-label">{{ option.label }}</span>
5214
+ </li>
5215
+ } @empty {
5216
+ @if (options().length > 0) {
5217
+ <li class="kf-multi-select__empty">Sin resultados</li>
5218
+ } @else {
5219
+ <li class="kf-multi-select__empty">Sin opciones disponibles</li>
5220
+ }
5221
+ }
5222
+ </ul>
5223
+ </div>
5224
+ </ng-template>
5225
+ `, styles: [":host{display:inline-flex;flex-direction:column;gap:var(--kf-space-xs);font:inherit;width:240px}.kf-multi-select__trigger{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);width:100%;font:inherit;font-family:var(--kf-font-body);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);cursor:pointer;transition:border-color .14s,box-shadow .14s}.kf-multi-select__trigger:hover:not(:disabled){border-color:var(--kf-color-border-strong)}.kf-multi-select__trigger:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}.kf-multi-select__trigger:disabled{opacity:.5;cursor:not-allowed}.kf-multi-select__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;text-align:left}.kf-multi-select__value--placeholder{color:var(--kf-color-text-muted)}.kf-multi-select__count{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:1.25rem;height:1.25rem;padding:0 5px;border-radius:var(--kf-radius-pill);background:var(--kf-color-primary);color:var(--kf-color-white);font-size:.72rem;font-weight:700}.kf-multi-select__caret{flex-shrink:0;width:8px;height:8px;border-right:2px solid var(--kf-color-text-muted);border-bottom:2px solid var(--kf-color-text-muted);transform:rotate(45deg);transition:transform .14s;margin-top:-4px}.kf-multi-select__caret--open{transform:rotate(-135deg);margin-top:4px}.kf-multi-select__panel{box-sizing:border-box;width:100%;background:var(--kf-color-white);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-md);overflow:hidden}.kf-multi-select__search{box-sizing:border-box;width:100%;font:inherit;font-family:var(--kf-font-body);color:var(--kf-color-text);padding:var(--kf-space-sm) var(--kf-space-md);border:none;border-bottom:1px solid var(--kf-color-border);border-radius:var(--kf-radius-sm) var(--kf-radius-sm) 0 0;outline:none}.kf-multi-select__search:focus-visible{box-shadow:inset 0 0 0 2px var(--kf-color-primary)}.kf-multi-select__list{box-sizing:border-box;width:100%;margin:0;padding:var(--kf-space-xs) 0;list-style:none;max-height:240px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--kf-color-gray-400) transparent}.kf-multi-select__list::-webkit-scrollbar{width:6px}.kf-multi-select__list::-webkit-scrollbar-track{background:transparent}.kf-multi-select__list::-webkit-scrollbar-thumb{background-color:var(--kf-color-gray-400);border-radius:var(--kf-radius-pill)}.kf-multi-select__list::-webkit-scrollbar-thumb:hover{background-color:var(--kf-color-gray-500)}.kf-multi-select__option{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kf-multi-select__option:hover{background:var(--kf-color-primary-soft)}.kf-multi-select__option--selected{color:var(--kf-color-primary);font-weight:600}.kf-multi-select__option--active{background:var(--kf-color-primary-soft)}.kf-multi-select__checkbox{display:inline-flex;flex-shrink:0;width:var(--kf-space-md);height:var(--kf-space-md);border:1px solid var(--kf-color-border-strong);border-radius:var(--kf-radius-xs);background:var(--kf-color-white);transition:background .14s,border-color .14s;position:relative}.kf-multi-select__checkbox--checked{background:var(--kf-color-primary);border-color:var(--kf-color-primary)}.kf-multi-select__checkbox--checked:after{content:\"\";position:absolute;left:4px;top:1px;width:4px;height:8px;border:solid var(--kf-color-white);border-width:0 2px 2px 0;transform:rotate(45deg)}.kf-multi-select__checkbox--indeterminate{background:var(--kf-color-primary);border-color:var(--kf-color-primary)}.kf-multi-select__checkbox--indeterminate:after{content:\"\";position:absolute;left:3px;top:6px;width:8px;height:2px;background:var(--kf-color-white);border:none;transform:none}.kf-multi-select__option-label{overflow:hidden;text-overflow:ellipsis}.kf-multi-select__empty{text-align:center;padding:var(--kf-space-sm) var(--kf-space-md);color:var(--kf-color-text-muted)}\n"] }]
5226
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], values: [{ type: i0.Input, args: [{ isSignal: true, alias: "values", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchThreshold", required: false }] }], valuesChange: [{ type: i0.Output, args: ["valuesChange"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelTemplate: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], searchInputRef: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }], onEscape: [{
4334
5227
  type: HostListener,
4335
5228
  args: ['document:keydown.escape']
4336
5229
  }] } });
4337
5230
 
5231
+ /**
5232
+ * Tipografía unificada — separa la etiqueta semántica (`as`, qué es el
5233
+ * texto para el DOM/accesibilidad) del estilo visual (`variant`, cómo se
5234
+ * ve). Antes de este componente, cada pantalla que consume la librería
5235
+ * definía su propio `h1`/`p` con `font-size` suelto para el mismo rol
5236
+ * (título de página, subtítulo, texto secundario) — auditoría sobre
5237
+ * `web-portal-latam-frontend` encontró 9 tamaños distintos entre login y
5238
+ * home sin ningún paso de escala en común. `variant` resuelve eso contra
5239
+ * la escala de `tokens/base.css` (`--kf-font-size-*`).
5240
+ *
5241
+ * `tone` sigue la misma convención cerrada que `Alert`/`Badge`/`Chip`: sin
5242
+ * escape hatch de color libre. Si un caso necesita un color que `tone` no
5243
+ * cubre, es una discusión de design system, no una prop nueva acá.
5244
+ *
5245
+ * `title-xl`/`title-xxl` son un tier aparte de `title-lg|md|sm`: no vienen
5246
+ * de la auditoría de contenido de página (esa cerró en 2.125rem), son para
5247
+ * títulos protagonistas de pantalla completa (ej. bienvenida del home vía
5248
+ * `kf-page-header[size=xl|xxl]`) — ver tokens/base.css.
5249
+ *
5250
+ * <kf-text as="h1" variant="title-lg">Bienvenido de nuevo</kf-text>
5251
+ * <kf-text as="p" variant="body-muted">Ingresá con tu cuenta corporativa.</kf-text>
5252
+ * <kf-text as="span" variant="caption" tone="error">Campo obligatorio</kf-text>
5253
+ */
5254
+ class Text {
5255
+ /** Etiqueta semántica real que se renderiza — nunca es solo un `div`. */
5256
+ as = input('p', /* @ts-ignore */
5257
+ ...(ngDevMode ? [{ debugName: "as" }] : /* istanbul ignore next */ []));
5258
+ variant = input('body', /* @ts-ignore */
5259
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
5260
+ tone = input('neutral', /* @ts-ignore */
5261
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
5262
+ /**
5263
+ * Copia de `as` para uso dentro del template: `as` es palabra reservada
5264
+ * en las expresiones de Angular (`@if (x; as y)`), así que `@switch (as())`
5265
+ * no parsea — NG5002. La prop pública sigue llamándose `as` (es solo un
5266
+ * atributo HTML ahí, `<kf-text as="h1">`, no una expresión).
5267
+ */
5268
+ tag = computed(() => this.as(), /* @ts-ignore */
5269
+ ...(ngDevMode ? [{ debugName: "tag" }] : /* istanbul ignore next */ []));
5270
+ hostClass() {
5271
+ const classes = [`kf-text--${this.variant()}`];
5272
+ if (this.tone() !== 'neutral') {
5273
+ classes.push(`kf-text--${this.tone()}`);
5274
+ }
5275
+ return classes.join(' ');
5276
+ }
5277
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Text, deps: [], target: i0.ɵɵFactoryTarget.Component });
5278
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: Text, isStandalone: true, selector: "kf-text", inputs: { as: { classPropertyName: "as", publicName: "as", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
5279
+ <!--
5280
+ Una sola proyección compartida (ng-template + ngTemplateOutlet) en vez
5281
+ de un <ng-content> por cada rama del @switch: Angular solo conecta la
5282
+ proyección real del consumidor a la PRIMERA instancia de <ng-content>
5283
+ que encuentra en el árbol de compilación — con un <ng-content> por
5284
+ rama, todas menos una quedan vacías en runtime (bug detectado por
5285
+ Text.spec.ts, no algo documentado a simple vista en los docs de Angular).
5286
+ -->
5287
+ <ng-template #projected><ng-content /></ng-template>
5288
+ @switch (tag()) {
5289
+ @case ('h1') {
5290
+ <h1 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h1>
5291
+ }
5292
+ @case ('h2') {
5293
+ <h2 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h2>
5294
+ }
5295
+ @case ('h3') {
5296
+ <h3 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h3>
5297
+ }
5298
+ @case ('span') {
5299
+ <span [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></span>
5300
+ }
5301
+ @default {
5302
+ <p [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></p>
5303
+ }
5304
+ }
5305
+ `, isInline: true, styles: [":host{display:contents}h1,h2,h3,p,span{margin:0;font-family:var(--kf-font-body);color:var(--kf-color-text)}.kf-text--title-xxl{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-xxl);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-xl{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-xl);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-lg{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-lg);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-md{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-md);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-sm{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-sm);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--body{font-size:var(--kf-font-size-body);font-weight:var(--kf-font-weight-body)}.kf-text--body-muted{font-size:var(--kf-font-size-body);font-weight:var(--kf-font-weight-body);color:var(--kf-color-text-muted)}.kf-text--caption{font-size:var(--kf-font-size-caption);font-weight:var(--kf-font-weight-body);color:var(--kf-color-text-muted)}.kf-text--ok{color:var(--kf-color-success-strong)}.kf-text--warn{color:var(--kf-color-warning-strong)}.kf-text--error{color:var(--kf-color-error-strong)}.kf-text--info{color:var(--kf-color-info-strong)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
5306
+ }
5307
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Text, decorators: [{
5308
+ type: Component,
5309
+ args: [{ selector: 'kf-text', imports: [NgTemplateOutlet], template: `
5310
+ <!--
5311
+ Una sola proyección compartida (ng-template + ngTemplateOutlet) en vez
5312
+ de un <ng-content> por cada rama del @switch: Angular solo conecta la
5313
+ proyección real del consumidor a la PRIMERA instancia de <ng-content>
5314
+ que encuentra en el árbol de compilación — con un <ng-content> por
5315
+ rama, todas menos una quedan vacías en runtime (bug detectado por
5316
+ Text.spec.ts, no algo documentado a simple vista en los docs de Angular).
5317
+ -->
5318
+ <ng-template #projected><ng-content /></ng-template>
5319
+ @switch (tag()) {
5320
+ @case ('h1') {
5321
+ <h1 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h1>
5322
+ }
5323
+ @case ('h2') {
5324
+ <h2 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h2>
5325
+ }
5326
+ @case ('h3') {
5327
+ <h3 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h3>
5328
+ }
5329
+ @case ('span') {
5330
+ <span [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></span>
5331
+ }
5332
+ @default {
5333
+ <p [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></p>
5334
+ }
5335
+ }
5336
+ `, styles: [":host{display:contents}h1,h2,h3,p,span{margin:0;font-family:var(--kf-font-body);color:var(--kf-color-text)}.kf-text--title-xxl{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-xxl);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-xl{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-xl);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-lg{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-lg);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-md{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-md);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-sm{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-sm);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--body{font-size:var(--kf-font-size-body);font-weight:var(--kf-font-weight-body)}.kf-text--body-muted{font-size:var(--kf-font-size-body);font-weight:var(--kf-font-weight-body);color:var(--kf-color-text-muted)}.kf-text--caption{font-size:var(--kf-font-size-caption);font-weight:var(--kf-font-weight-body);color:var(--kf-color-text-muted)}.kf-text--ok{color:var(--kf-color-success-strong)}.kf-text--warn{color:var(--kf-color-warning-strong)}.kf-text--error{color:var(--kf-color-error-strong)}.kf-text--info{color:var(--kf-color-info-strong)}\n"] }]
5337
+ }], propDecorators: { as: [{ type: i0.Input, args: [{ isSignal: true, alias: "as", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
5338
+
4338
5339
  /**
4339
5340
  * Bloque de título al tope de una página: chip de ícono opcional, título (h1),
4340
5341
  * subtítulo opcional y un área de acciones opcional proyectada vía
@@ -4346,6 +5347,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4346
5347
  * </kf-page-header>
4347
5348
  *
4348
5349
  * <kf-page-header icon="settings" title="Configuración" />
5350
+ *
5351
+ * <kf-page-header size="xl" title="Bienvenido de nuevo" />
4349
5352
  */
4350
5353
  class PageHeader {
4351
5354
  icon = input(undefined, /* @ts-ignore */
@@ -4354,8 +5357,12 @@ class PageHeader {
4354
5357
  ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
4355
5358
  subtitle = input(undefined, /* @ts-ignore */
4356
5359
  ...(ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []));
5360
+ size = input('md', /* @ts-ignore */
5361
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
5362
+ titleVariant = computed(() => `title-${this.size()}`, /* @ts-ignore */
5363
+ ...(ngDevMode ? [{ debugName: "titleVariant" }] : /* istanbul ignore next */ []));
4357
5364
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PageHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
4358
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PageHeader, isStandalone: true, selector: "kf-page-header", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
5365
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PageHeader, isStandalone: true, selector: "kf-page-header", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
4359
5366
  @if (icon(); as iconName) {
4360
5367
  <span class="kf-page-header__icon">
4361
5368
  <kf-icon [name]="iconName" size="lg" />
@@ -4363,7 +5370,9 @@ class PageHeader {
4363
5370
  }
4364
5371
 
4365
5372
  <div class="kf-page-header__heading">
4366
- <h1 class="kf-page-header__title">{{ title() }}</h1>
5373
+ <kf-text as="h1" class="kf-page-header__title" [variant]="titleVariant()">
5374
+ {{ title() }}
5375
+ </kf-text>
4367
5376
  @if (subtitle()) {
4368
5377
  <p class="kf-page-header__subtitle">{{ subtitle() }}</p>
4369
5378
  }
@@ -4372,11 +5381,11 @@ class PageHeader {
4372
5381
  <div class="kf-page-header__actions">
4373
5382
  <ng-content select="[actions]" />
4374
5383
  </div>
4375
- `, isInline: true, styles: [":host{display:flex;align-items:center;gap:var(--kf-space-md);font-family:var(--kf-font-body)}.kf-page-header__icon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:48px;height:48px;border-radius:var(--kf-radius-md);background:var(--kf-color-primary-soft);color:var(--kf-color-primary)}.kf-page-header__heading{display:flex;flex-direction:column;gap:var(--kf-space-xs);flex:1 1 auto;min-width:0}.kf-page-header__title{margin:0;font-family:var(--kf-font-title);font-size:1.5rem;font-weight:700;line-height:1.2;color:var(--kf-color-ink)}.kf-page-header__subtitle{margin:0;font-size:.9375rem;color:var(--kf-color-text-muted)}.kf-page-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"] }] });
5384
+ `, isInline: true, styles: [":host{display:flex;align-items:center;gap:var(--kf-space-md);font-family:var(--kf-font-body)}.kf-page-header__icon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:48px;height:48px;border-radius:var(--kf-radius-md);background:var(--kf-color-primary-soft);color:var(--kf-color-primary)}.kf-page-header__heading{display:flex;flex-direction:column;gap:var(--kf-space-xs);flex:1 1 auto;min-width:0}.kf-page-header__title{line-height:1.2}.kf-page-header__subtitle{margin:0;font-size:.9375rem;color:var(--kf-color-text-muted)}.kf-page-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"] }, { kind: "component", type: Text, selector: "kf-text", inputs: ["as", "variant", "tone"] }] });
4376
5385
  }
4377
5386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PageHeader, decorators: [{
4378
5387
  type: Component,
4379
- args: [{ selector: 'kf-page-header', imports: [Icon], template: `
5388
+ args: [{ selector: 'kf-page-header', imports: [Icon, Text], template: `
4380
5389
  @if (icon(); as iconName) {
4381
5390
  <span class="kf-page-header__icon">
4382
5391
  <kf-icon [name]="iconName" size="lg" />
@@ -4384,7 +5393,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4384
5393
  }
4385
5394
 
4386
5395
  <div class="kf-page-header__heading">
4387
- <h1 class="kf-page-header__title">{{ title() }}</h1>
5396
+ <kf-text as="h1" class="kf-page-header__title" [variant]="titleVariant()">
5397
+ {{ title() }}
5398
+ </kf-text>
4388
5399
  @if (subtitle()) {
4389
5400
  <p class="kf-page-header__subtitle">{{ subtitle() }}</p>
4390
5401
  }
@@ -4393,8 +5404,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4393
5404
  <div class="kf-page-header__actions">
4394
5405
  <ng-content select="[actions]" />
4395
5406
  </div>
4396
- `, styles: [":host{display:flex;align-items:center;gap:var(--kf-space-md);font-family:var(--kf-font-body)}.kf-page-header__icon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:48px;height:48px;border-radius:var(--kf-radius-md);background:var(--kf-color-primary-soft);color:var(--kf-color-primary)}.kf-page-header__heading{display:flex;flex-direction:column;gap:var(--kf-space-xs);flex:1 1 auto;min-width:0}.kf-page-header__title{margin:0;font-family:var(--kf-font-title);font-size:1.5rem;font-weight:700;line-height:1.2;color:var(--kf-color-ink)}.kf-page-header__subtitle{margin:0;font-size:.9375rem;color:var(--kf-color-text-muted)}.kf-page-header__actions{flex:none;display:flex;align-items:center;gap:var(--kf-space-sm)}\n"] }]
4397
- }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }] } });
5407
+ `, styles: [":host{display:flex;align-items:center;gap:var(--kf-space-md);font-family:var(--kf-font-body)}.kf-page-header__icon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:48px;height:48px;border-radius:var(--kf-radius-md);background:var(--kf-color-primary-soft);color:var(--kf-color-primary)}.kf-page-header__heading{display:flex;flex-direction:column;gap:var(--kf-space-xs);flex:1 1 auto;min-width:0}.kf-page-header__title{line-height:1.2}.kf-page-header__subtitle{margin:0;font-size:.9375rem;color:var(--kf-color-text-muted)}.kf-page-header__actions{flex:none;display:flex;align-items:center;gap:var(--kf-space-sm)}\n"] }]
5408
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
4398
5409
 
4399
5410
  const SIBLINGS = 1;
4400
5411
  /**
@@ -4565,6 +5576,13 @@ function buildPaginationItems(current, total) {
4565
5576
  * <kf-panel title="Mis documentos por pagar" actionLabel="Ir a pagos" actionHref="/pagos">
4566
5577
  * <kf-action-tile-row icon="download" title="Pagos" description="Revisa tu estado de cuenta y realiza pagos." />
4567
5578
  * </kf-panel>
5579
+ *
5580
+ * El host y el `kf-card` interno son flex column con `flex:1` de punta a
5581
+ * punta — si un grid/flex externo estira `kf-panel` a la altura de un
5582
+ * hermano más alto, el contenido proyectado hereda esa altura en vez de
5583
+ * dejar un hueco vacío bajo el `kf-card`. Sin esa presión externa (altura
5584
+ * "auto" del contenedor), `flex:1` no tiene espacio extra que repartir y el
5585
+ * panel se comporta igual que antes.
4568
5586
  */
4569
5587
  class Panel {
4570
5588
  title = input.required(/* @ts-ignore */
@@ -4590,7 +5608,7 @@ class Panel {
4590
5608
  <ng-content />
4591
5609
  </div>
4592
5610
  </kf-card>
4593
- `, isInline: true, styles: [":host{display:block}.kf-panel__head{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);margin-bottom:var(--kf-space-sm)}.kf-panel__title{font-family:var(--kf-font-title);font-size:1.05rem;font-weight:700;color:var(--kf-color-ink)}.kf-panel__action{display:inline-flex;align-items:center;gap:2px;flex:none;font-size:.8125rem;font-weight:600;color:var(--kf-color-primary);text-decoration:none}.kf-panel__action:hover{text-decoration:underline}.kf-panel__body{display:flex;flex-direction:column;gap:var(--kf-space-sm);margin-top:var(--kf-space-sm)}\n"], dependencies: [{ kind: "component", type: Card, selector: "kf-card", inputs: ["flush"] }, { kind: "component", type: Divider, selector: "kf-divider", inputs: ["orientation"] }, { kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }] });
5611
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column}kf-card{flex:1}.kf-panel__head{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);margin-bottom:var(--kf-space-sm)}.kf-panel__title{font-family:var(--kf-font-title);font-size:1.05rem;font-weight:700;color:var(--kf-color-ink)}.kf-panel__action{display:inline-flex;align-items:center;gap:2px;flex:none;font-size:.8125rem;font-weight:600;color:var(--kf-color-primary);text-decoration:none}.kf-panel__action:hover{text-decoration:underline}.kf-panel__body{display:flex;flex:1;flex-direction:column;gap:var(--kf-space-sm);margin-top:var(--kf-space-sm)}\n"], dependencies: [{ kind: "component", type: Card, selector: "kf-card", inputs: ["flush"] }, { kind: "component", type: Divider, selector: "kf-divider", inputs: ["orientation"] }, { kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }] });
4594
5612
  }
4595
5613
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Panel, decorators: [{
4596
5614
  type: Component,
@@ -4610,7 +5628,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4610
5628
  <ng-content />
4611
5629
  </div>
4612
5630
  </kf-card>
4613
- `, styles: [":host{display:block}.kf-panel__head{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);margin-bottom:var(--kf-space-sm)}.kf-panel__title{font-family:var(--kf-font-title);font-size:1.05rem;font-weight:700;color:var(--kf-color-ink)}.kf-panel__action{display:inline-flex;align-items:center;gap:2px;flex:none;font-size:.8125rem;font-weight:600;color:var(--kf-color-primary);text-decoration:none}.kf-panel__action:hover{text-decoration:underline}.kf-panel__body{display:flex;flex-direction:column;gap:var(--kf-space-sm);margin-top:var(--kf-space-sm)}\n"] }]
5631
+ `, styles: [":host{display:flex;flex-direction:column}kf-card{flex:1}.kf-panel__head{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-sm);margin-bottom:var(--kf-space-sm)}.kf-panel__title{font-family:var(--kf-font-title);font-size:1.05rem;font-weight:700;color:var(--kf-color-ink)}.kf-panel__action{display:inline-flex;align-items:center;gap:2px;flex:none;font-size:.8125rem;font-weight:600;color:var(--kf-color-primary);text-decoration:none}.kf-panel__action:hover{text-decoration:underline}.kf-panel__body{display:flex;flex:1;flex-direction:column;gap:var(--kf-space-sm);margin-top:var(--kf-space-sm)}\n"] }]
4614
5632
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], actionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionLabel", required: false }] }], actionHref: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionHref", required: false }] }] } });
4615
5633
 
4616
5634
  /**
@@ -4629,21 +5647,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4629
5647
  *
4630
5648
  * Bajo los 768px, el `<nav>` deja de mostrarse en línea junto al logo y
4631
5649
  * colapsa detrás de un botón de menú (ícono `menu`/`x`) que despliega un
4632
- * drawer lateral (desde el borde derecho, `position: fixed` + overlay de
4633
- * fondo) con los mismos `navItems` en columna. Ese drawer expone un slot
4634
- * (`<ng-content select="[kf-portal-header-extra]">`) al final, pensado
4635
- * para que el consumidor proyecte contenido propio que en escritorio vive
4636
- * en otro lugar (ej. el menú de sesión) — `PortalHeader` no sabe qué es
4637
- * ese contenido ni cómo se abre, solo le da un lugar dentro del drawer ya
4638
- * desplegado. El overlay cierra el drawer al tocarlo, y `Escape` también.
5650
+ * `kf-drawer` (panel lateral genérico, ver `Drawer.ts`) anclado al borde
5651
+ * IZQUIERDO — mismo lado que el botón que lo abre, convención dominante
5652
+ * Material/iOS/Android — con los mismos `navItems` en columna dentro del
5653
+ * body (fondo claro) de ese drawer.
5654
+ *
5655
+ * `PortalHeader` expone dos slots propios pensados para que el consumidor
5656
+ * proyecte contenido que en escritorio vive en otro lugar:
5657
+ * - `<ng-content select="[kf-portal-header-extra]">`: identidad de sesión
5658
+ * (ej. `kf-drawer-session`). Cae en `[kf-drawer-header]` de `kf-drawer` —
5659
+ * el único contenedor con fondo oscuro (`--kf-color-tertiary-deep`, ver
5660
+ * `Drawer.ts`) que `kf-drawer-session` espera como fondo (no trae uno
5661
+ * propio, ver `DrawerSession.ts`).
5662
+ * - `<ng-content select="[kf-portal-header-footer]">`: acciones separadas del
5663
+ * resto (ej. `kf-drawer-logout`). Cae en `[kf-drawer-footer]` de
5664
+ * `kf-drawer`, mismo criterio de "no reimplementar nada del drawer": el
5665
+ * footer ya trae su propio padding/separador (ver `Drawer.ts`).
5666
+ *
5667
+ * `PortalHeader` no sabe qué es ninguno de los dos contenidos ni cómo se
5668
+ * abren, solo les da el lugar correcto dentro del drawer ya desplegado.
5669
+ * `kf-drawer` resuelve overlay con blur, cierre con click-afuera/`Escape`,
5670
+ * atrapado de foco real y devolución de foco al cerrar — `PortalHeader` no
5671
+ * reimplementa nada de eso, solo controla su `open` desde el botón de
5672
+ * hamburguesa.
4639
5673
  *
4640
5674
  * <kf-portal-header
4641
5675
  * [isAdmin]="true"
4642
5676
  * [adminActions]="[{ label: 'Consultar deuda clientes', href: '/deuda' }, { label: 'Cambiar DNI', href: '/dni' }]"
4643
- * [navItems]="[{ label: 'Home', href: '/', active: true }, { label: 'Vehículos', href: '/vehiculos' }]"
5677
+ * [navItems]="[{ label: 'Home', href: '/', active: true, icon: 'grid' }, { label: 'Vehículos', href: '/vehiculos', icon: 'truck' }]"
4644
5678
  * userInitials="TS"
4645
5679
  * >
4646
- * <div kf-portal-header-extra>contenido propio, solo visible en el panel mobile</div>
5680
+ * <kf-drawer-session
5681
+ * kf-portal-header-extra
5682
+ * brand="Portal Kaufmann LATAM"
5683
+ * name="Tomás Soto"
5684
+ * email="tomas.soto@kaufmann.com"
5685
+ * />
5686
+ * <kf-drawer-logout kf-portal-header-footer (logoutClick)="onLogout()" />
4647
5687
  * </kf-portal-header>
4648
5688
  */
4649
5689
  class PortalHeader {
@@ -4657,13 +5697,11 @@ class PortalHeader {
4657
5697
  userInitials = input.required(/* @ts-ignore */
4658
5698
  ...(ngDevMode ? [{ debugName: "userInitials" }] : /* istanbul ignore next */ []));
4659
5699
  sessionClick = output();
4660
- /** Visibilidad del drawer de navegación mobile (bajo los 768px). */
5700
+ /** Visibilidad del drawer de navegación mobile (bajo los 768px). Se pasa a
5701
+ * `kf-drawer` como `[(open)]` — `PortalHeader` ya no implementa overlay,
5702
+ * `Escape` ni atrapado de foco por su cuenta, eso vive en `Drawer`. */
4661
5703
  mobileMenuOpen = signal(false, /* @ts-ignore */
4662
5704
  ...(ngDevMode ? [{ debugName: "mobileMenuOpen" }] : /* istanbul ignore next */ []));
4663
- /** Cierra el drawer con Escape — mismo patrón que cualquier drawer/modal. */
4664
- onEscape() {
4665
- this.mobileMenuOpen.set(false);
4666
- }
4667
5705
  /**
4668
5706
  * `ElementRef` real del botón de sesión (el avatar en la esquina
4669
5707
  * derecha), no de todo el header. Pensado para anclar un `kf-dropdown`
@@ -4676,7 +5714,7 @@ class PortalHeader {
4676
5714
  */
4677
5715
  sessionButtonRef = viewChild.required('sessionButton', { ...(ngDevMode ? { debugName: "sessionButtonRef" } : /* istanbul ignore next */ {}), read: ElementRef });
4678
5716
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PortalHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
4679
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PortalHeader, isStandalone: true, selector: "kf-portal-header", inputs: { brand: { classPropertyName: "brand", publicName: "brand", isSignal: true, isRequired: false, transformFunction: null }, isAdmin: { classPropertyName: "isAdmin", publicName: "isAdmin", isSignal: true, isRequired: false, transformFunction: null }, adminActions: { classPropertyName: "adminActions", publicName: "adminActions", isSignal: true, isRequired: false, transformFunction: null }, navItems: { classPropertyName: "navItems", publicName: "navItems", isSignal: true, isRequired: false, transformFunction: null }, userInitials: { classPropertyName: "userInitials", publicName: "userInitials", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { sessionClick: "sessionClick" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, viewQueries: [{ propertyName: "sessionButtonRef", first: true, predicate: ["sessionButton"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: `
5717
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PortalHeader, isStandalone: true, selector: "kf-portal-header", inputs: { brand: { classPropertyName: "brand", publicName: "brand", isSignal: true, isRequired: false, transformFunction: null }, isAdmin: { classPropertyName: "isAdmin", publicName: "isAdmin", isSignal: true, isRequired: false, transformFunction: null }, adminActions: { classPropertyName: "adminActions", publicName: "adminActions", isSignal: true, isRequired: false, transformFunction: null }, navItems: { classPropertyName: "navItems", publicName: "navItems", isSignal: true, isRequired: false, transformFunction: null }, userInitials: { classPropertyName: "userInitials", publicName: "userInitials", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { sessionClick: "sessionClick" }, viewQueries: [{ propertyName: "sessionButtonRef", first: true, predicate: ["sessionButton"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: `
4680
5718
  @if (isAdmin()) {
4681
5719
  <div class="kf-portal-header__admin-bar">
4682
5720
  <kf-admin-badge inline />
@@ -4722,16 +5760,10 @@ class PortalHeader {
4722
5760
  </button>
4723
5761
  </div>
4724
5762
 
4725
- @if (mobileMenuOpen()) {
4726
- <div class="kf-portal-header__overlay" (click)="mobileMenuOpen.set(false)"></div>
4727
- }
4728
-
4729
- <nav
4730
- class="kf-portal-header__drawer"
4731
- [class.kf-portal-header__drawer--open]="mobileMenuOpen()"
4732
- role="menu"
4733
- [attr.aria-hidden]="!mobileMenuOpen()"
4734
- >
5763
+ <kf-drawer [(open)]="mobileMenuOpen" ariaLabel="Menú de navegación">
5764
+ <div kf-drawer-header>
5765
+ <ng-content select="[kf-portal-header-extra]" />
5766
+ </div>
4735
5767
  @for (item of navItems(); track item.label) {
4736
5768
  <a
4737
5769
  class="kf-portal-header__mobile-nav-item"
@@ -4739,16 +5771,22 @@ class PortalHeader {
4739
5771
  [href]="item.href"
4740
5772
  (click)="mobileMenuOpen.set(false)"
4741
5773
  >
4742
- {{ item.label }}
5774
+ @if (item.icon) {
5775
+ <kf-icon [name]="item.icon" size="sm" />
5776
+ }
5777
+ <span class="kf-portal-header__mobile-nav-item-label">{{ item.label }}</span>
5778
+ <kf-icon name="chevron-right" size="sm" />
4743
5779
  </a>
4744
5780
  }
4745
- <ng-content select="[kf-portal-header-extra]" />
4746
- </nav>
4747
- `, isInline: true, styles: [":host{display:block;position:sticky;top:0;z-index:30;font-family:var(--kf-font-body)}.kf-portal-header__admin-bar{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-md);padding:var(--kf-space-xs) var(--kf-space-lg);background:var(--kf-color-tertiary-deep)}.kf-portal-header__admin-actions{display:flex;gap:var(--kf-space-md)}.kf-portal-header__admin-action{font-size:.8125rem;font-weight:600;color:#ffffffd9;text-decoration:none}.kf-portal-header__admin-action:hover{color:var(--kf-color-white)}.kf-portal-header__brand-bar{display:flex;align-items:center;gap:var(--kf-space-lg);padding:var(--kf-space-sm) var(--kf-space-lg);background:var(--kf-color-tertiary-deep);border-top:1px solid rgba(255,255,255,.1)}.kf-portal-header__logo{font-family:var(--kf-font-title);font-size:1.25rem;font-weight:700;letter-spacing:.02em;color:var(--kf-color-white);text-transform:uppercase;flex:1 1 auto;min-width:0}.kf-portal-header__nav{display:flex;align-items:center;gap:var(--kf-space-xs);flex:1 1 auto}.kf-portal-header__nav-item{padding:var(--kf-space-xs) var(--kf-space-md);border-radius:var(--kf-radius-sm);color:#ffffffbf;font-size:.875rem;font-weight:600;text-decoration:none;white-space:nowrap;transition:background .14s,color .14s}.kf-portal-header__nav-item:hover:not(.kf-portal-header__nav-item--active){color:var(--kf-color-white)}.kf-portal-header__nav-item--active{background:var(--kf-color-white);color:var(--kf-color-ink)}.kf-portal-header__session{flex:none;border:none;background:transparent;padding:0;cursor:pointer}.kf-portal-header__menu-toggle{display:none;flex:none;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--kf-radius-sm);background:transparent;color:var(--kf-color-white);cursor:pointer}.kf-portal-header__menu-toggle:hover{background:#ffffff1a}.kf-portal-header__overlay{position:fixed;inset:0;background:#10182866;z-index:39}.kf-portal-header__drawer{position:fixed;top:0;right:0;bottom:0;z-index:40;display:none;flex-direction:column;width:min(80vw,320px);padding:var(--kf-space-lg);background:var(--kf-color-tertiary-deep);box-shadow:-8px 0 24px -8px #1018284d;transform:translate(100%);visibility:hidden;transition:transform .24s cubic-bezier(.32,.72,0,1),visibility 0s linear .24s}.kf-portal-header__drawer--open{transform:translate(0);visibility:visible;transition:transform .24s cubic-bezier(.32,.72,0,1),visibility 0s linear 0s}.kf-portal-header__mobile-nav-item{padding:var(--kf-space-sm) var(--kf-space-xs);color:#ffffffd9;font-size:.9375rem;font-weight:600;text-decoration:none;border-radius:var(--kf-radius-sm)}.kf-portal-header__mobile-nav-item:hover{background:#ffffff14}.kf-portal-header__mobile-nav-item--active{color:var(--kf-color-white);background:#ffffff24}@media(max-width:768px){.kf-portal-header__nav,.kf-portal-header__session{display:none}.kf-portal-header__menu-toggle{display:inline-flex}.kf-portal-header__drawer{display:flex}}\n"], dependencies: [{ kind: "component", type: AdminBadge, selector: "kf-admin-badge", inputs: ["inline"] }, { kind: "component", type: Avatar, selector: "kf-avatar", inputs: ["initials", "src", "round", "pill"] }, { kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }] });
5781
+ <div kf-drawer-footer>
5782
+ <ng-content select="[kf-portal-header-footer]" />
5783
+ </div>
5784
+ </kf-drawer>
5785
+ `, isInline: true, styles: [":host{display:block;position:sticky;top:0;z-index:30;font-family:var(--kf-font-body)}.kf-portal-header__admin-bar{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-md);padding:var(--kf-space-xs) var(--kf-space-lg);background:var(--kf-color-tertiary-deep)}.kf-portal-header__admin-actions{display:flex;gap:var(--kf-space-md)}.kf-portal-header__admin-action{font-size:.8125rem;font-weight:600;color:#ffffffd9;text-decoration:none}.kf-portal-header__admin-action:hover{color:var(--kf-color-white)}.kf-portal-header__brand-bar{display:flex;align-items:center;gap:var(--kf-space-lg);padding:var(--kf-space-sm) var(--kf-space-lg);background:var(--kf-color-tertiary-deep);border-top:1px solid rgba(255,255,255,.1)}.kf-portal-header__logo{font-family:var(--kf-font-title);font-size:1.25rem;font-weight:700;letter-spacing:.02em;color:var(--kf-color-white);text-transform:uppercase;flex:1 1 auto;min-width:0}.kf-portal-header__nav{display:flex;align-items:center;gap:var(--kf-space-xs);flex:1 1 auto}.kf-portal-header__nav-item{padding:var(--kf-space-xs) var(--kf-space-md);border-radius:var(--kf-radius-sm);color:#ffffffbf;font-size:.875rem;font-weight:600;text-decoration:none;white-space:nowrap;transition:background .14s,color .14s}.kf-portal-header__nav-item:hover:not(.kf-portal-header__nav-item--active){color:var(--kf-color-white)}.kf-portal-header__nav-item--active{background:var(--kf-color-white);color:var(--kf-color-ink)}.kf-portal-header__session{flex:none;border:none;background:transparent;padding:0;cursor:pointer}.kf-portal-header__menu-toggle{display:none;flex:none;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--kf-radius-sm);background:transparent;color:var(--kf-color-white);cursor:pointer}.kf-portal-header__menu-toggle:hover{background:#ffffff1a}.kf-portal-header__mobile-nav-item{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-xs);color:var(--kf-color-text);font-size:.9375rem;font-weight:600;text-decoration:none;border-radius:var(--kf-radius-sm)}.kf-portal-header__mobile-nav-item:hover{background:var(--kf-color-surface)}.kf-portal-header__mobile-nav-item--active{color:var(--kf-color-primary-active);background:var(--kf-color-primary-soft)}.kf-portal-header__mobile-nav-item-label{flex:1 1 auto}.kf-portal-header__mobile-nav-item kf-icon:last-child{color:#171d2559}@media(max-width:768px){.kf-portal-header__nav,.kf-portal-header__session{display:none}.kf-portal-header__menu-toggle{display:inline-flex}}\n"], dependencies: [{ kind: "component", type: AdminBadge, selector: "kf-admin-badge", inputs: ["inline"] }, { kind: "component", type: Avatar, selector: "kf-avatar", inputs: ["initials", "src", "round", "pill"] }, { kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }, { kind: "component", type: Drawer, selector: "kf-drawer", inputs: ["open", "side", "ariaLabel"], outputs: ["openChange"] }] });
4748
5786
  }
4749
5787
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PortalHeader, decorators: [{
4750
5788
  type: Component,
4751
- args: [{ selector: 'kf-portal-header', imports: [AdminBadge, Avatar, Icon], template: `
5789
+ args: [{ selector: 'kf-portal-header', imports: [AdminBadge, Avatar, Icon, Drawer], template: `
4752
5790
  @if (isAdmin()) {
4753
5791
  <div class="kf-portal-header__admin-bar">
4754
5792
  <kf-admin-badge inline />
@@ -4794,16 +5832,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4794
5832
  </button>
4795
5833
  </div>
4796
5834
 
4797
- @if (mobileMenuOpen()) {
4798
- <div class="kf-portal-header__overlay" (click)="mobileMenuOpen.set(false)"></div>
4799
- }
4800
-
4801
- <nav
4802
- class="kf-portal-header__drawer"
4803
- [class.kf-portal-header__drawer--open]="mobileMenuOpen()"
4804
- role="menu"
4805
- [attr.aria-hidden]="!mobileMenuOpen()"
4806
- >
5835
+ <kf-drawer [(open)]="mobileMenuOpen" ariaLabel="Menú de navegación">
5836
+ <div kf-drawer-header>
5837
+ <ng-content select="[kf-portal-header-extra]" />
5838
+ </div>
4807
5839
  @for (item of navItems(); track item.label) {
4808
5840
  <a
4809
5841
  class="kf-portal-header__mobile-nav-item"
@@ -4811,16 +5843,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4811
5843
  [href]="item.href"
4812
5844
  (click)="mobileMenuOpen.set(false)"
4813
5845
  >
4814
- {{ item.label }}
5846
+ @if (item.icon) {
5847
+ <kf-icon [name]="item.icon" size="sm" />
5848
+ }
5849
+ <span class="kf-portal-header__mobile-nav-item-label">{{ item.label }}</span>
5850
+ <kf-icon name="chevron-right" size="sm" />
4815
5851
  </a>
4816
5852
  }
4817
- <ng-content select="[kf-portal-header-extra]" />
4818
- </nav>
4819
- `, styles: [":host{display:block;position:sticky;top:0;z-index:30;font-family:var(--kf-font-body)}.kf-portal-header__admin-bar{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-md);padding:var(--kf-space-xs) var(--kf-space-lg);background:var(--kf-color-tertiary-deep)}.kf-portal-header__admin-actions{display:flex;gap:var(--kf-space-md)}.kf-portal-header__admin-action{font-size:.8125rem;font-weight:600;color:#ffffffd9;text-decoration:none}.kf-portal-header__admin-action:hover{color:var(--kf-color-white)}.kf-portal-header__brand-bar{display:flex;align-items:center;gap:var(--kf-space-lg);padding:var(--kf-space-sm) var(--kf-space-lg);background:var(--kf-color-tertiary-deep);border-top:1px solid rgba(255,255,255,.1)}.kf-portal-header__logo{font-family:var(--kf-font-title);font-size:1.25rem;font-weight:700;letter-spacing:.02em;color:var(--kf-color-white);text-transform:uppercase;flex:1 1 auto;min-width:0}.kf-portal-header__nav{display:flex;align-items:center;gap:var(--kf-space-xs);flex:1 1 auto}.kf-portal-header__nav-item{padding:var(--kf-space-xs) var(--kf-space-md);border-radius:var(--kf-radius-sm);color:#ffffffbf;font-size:.875rem;font-weight:600;text-decoration:none;white-space:nowrap;transition:background .14s,color .14s}.kf-portal-header__nav-item:hover:not(.kf-portal-header__nav-item--active){color:var(--kf-color-white)}.kf-portal-header__nav-item--active{background:var(--kf-color-white);color:var(--kf-color-ink)}.kf-portal-header__session{flex:none;border:none;background:transparent;padding:0;cursor:pointer}.kf-portal-header__menu-toggle{display:none;flex:none;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--kf-radius-sm);background:transparent;color:var(--kf-color-white);cursor:pointer}.kf-portal-header__menu-toggle:hover{background:#ffffff1a}.kf-portal-header__overlay{position:fixed;inset:0;background:#10182866;z-index:39}.kf-portal-header__drawer{position:fixed;top:0;right:0;bottom:0;z-index:40;display:none;flex-direction:column;width:min(80vw,320px);padding:var(--kf-space-lg);background:var(--kf-color-tertiary-deep);box-shadow:-8px 0 24px -8px #1018284d;transform:translate(100%);visibility:hidden;transition:transform .24s cubic-bezier(.32,.72,0,1),visibility 0s linear .24s}.kf-portal-header__drawer--open{transform:translate(0);visibility:visible;transition:transform .24s cubic-bezier(.32,.72,0,1),visibility 0s linear 0s}.kf-portal-header__mobile-nav-item{padding:var(--kf-space-sm) var(--kf-space-xs);color:#ffffffd9;font-size:.9375rem;font-weight:600;text-decoration:none;border-radius:var(--kf-radius-sm)}.kf-portal-header__mobile-nav-item:hover{background:#ffffff14}.kf-portal-header__mobile-nav-item--active{color:var(--kf-color-white);background:#ffffff24}@media(max-width:768px){.kf-portal-header__nav,.kf-portal-header__session{display:none}.kf-portal-header__menu-toggle{display:inline-flex}.kf-portal-header__drawer{display:flex}}\n"] }]
4820
- }], propDecorators: { brand: [{ type: i0.Input, args: [{ isSignal: true, alias: "brand", required: false }] }], isAdmin: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAdmin", required: false }] }], adminActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "adminActions", required: false }] }], navItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "navItems", required: false }] }], userInitials: [{ type: i0.Input, args: [{ isSignal: true, alias: "userInitials", required: true }] }], sessionClick: [{ type: i0.Output, args: ["sessionClick"] }], onEscape: [{
4821
- type: HostListener,
4822
- args: ['document:keydown.escape']
4823
- }], sessionButtonRef: [{ type: i0.ViewChild, args: ['sessionButton', { ...{ read: ElementRef }, isSignal: true }] }] } });
5853
+ <div kf-drawer-footer>
5854
+ <ng-content select="[kf-portal-header-footer]" />
5855
+ </div>
5856
+ </kf-drawer>
5857
+ `, styles: [":host{display:block;position:sticky;top:0;z-index:30;font-family:var(--kf-font-body)}.kf-portal-header__admin-bar{display:flex;align-items:center;justify-content:space-between;gap:var(--kf-space-md);padding:var(--kf-space-xs) var(--kf-space-lg);background:var(--kf-color-tertiary-deep)}.kf-portal-header__admin-actions{display:flex;gap:var(--kf-space-md)}.kf-portal-header__admin-action{font-size:.8125rem;font-weight:600;color:#ffffffd9;text-decoration:none}.kf-portal-header__admin-action:hover{color:var(--kf-color-white)}.kf-portal-header__brand-bar{display:flex;align-items:center;gap:var(--kf-space-lg);padding:var(--kf-space-sm) var(--kf-space-lg);background:var(--kf-color-tertiary-deep);border-top:1px solid rgba(255,255,255,.1)}.kf-portal-header__logo{font-family:var(--kf-font-title);font-size:1.25rem;font-weight:700;letter-spacing:.02em;color:var(--kf-color-white);text-transform:uppercase;flex:1 1 auto;min-width:0}.kf-portal-header__nav{display:flex;align-items:center;gap:var(--kf-space-xs);flex:1 1 auto}.kf-portal-header__nav-item{padding:var(--kf-space-xs) var(--kf-space-md);border-radius:var(--kf-radius-sm);color:#ffffffbf;font-size:.875rem;font-weight:600;text-decoration:none;white-space:nowrap;transition:background .14s,color .14s}.kf-portal-header__nav-item:hover:not(.kf-portal-header__nav-item--active){color:var(--kf-color-white)}.kf-portal-header__nav-item--active{background:var(--kf-color-white);color:var(--kf-color-ink)}.kf-portal-header__session{flex:none;border:none;background:transparent;padding:0;cursor:pointer}.kf-portal-header__menu-toggle{display:none;flex:none;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--kf-radius-sm);background:transparent;color:var(--kf-color-white);cursor:pointer}.kf-portal-header__menu-toggle:hover{background:#ffffff1a}.kf-portal-header__mobile-nav-item{display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-xs);color:var(--kf-color-text);font-size:.9375rem;font-weight:600;text-decoration:none;border-radius:var(--kf-radius-sm)}.kf-portal-header__mobile-nav-item:hover{background:var(--kf-color-surface)}.kf-portal-header__mobile-nav-item--active{color:var(--kf-color-primary-active);background:var(--kf-color-primary-soft)}.kf-portal-header__mobile-nav-item-label{flex:1 1 auto}.kf-portal-header__mobile-nav-item kf-icon:last-child{color:#171d2559}@media(max-width:768px){.kf-portal-header__nav,.kf-portal-header__session{display:none}.kf-portal-header__menu-toggle{display:inline-flex}}\n"] }]
5858
+ }], propDecorators: { brand: [{ type: i0.Input, args: [{ isSignal: true, alias: "brand", required: false }] }], isAdmin: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAdmin", required: false }] }], adminActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "adminActions", required: false }] }], navItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "navItems", required: false }] }], userInitials: [{ type: i0.Input, args: [{ isSignal: true, alias: "userInitials", required: true }] }], sessionClick: [{ type: i0.Output, args: ["sessionClick"] }], sessionButtonRef: [{ type: i0.ViewChild, args: ['sessionButton', { ...{ read: ElementRef }, isSignal: true }] }] } });
4824
5859
 
4825
5860
  /**
4826
5861
  * Estado vacío de reservas en taller con llamada a agendar (página 36):
@@ -4833,7 +5868,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4833
5868
  * invitación + las acciones como un bloque compuesto propio del dominio
4834
5869
  * "taller", no un placeholder de una sola pieza.
4835
5870
  *
4836
- * <kf-reserve-block emptyLabel="No tienes reservas de taller"
5871
+ * `grow` estira el panel de invitación para llenar el espacio vertical
5872
+ * disponible — solo cuando el consumidor comparte fila con una tarjeta más
5873
+ * alta (ej. junto a "Total de vehículos"); en un lugar sin esa presión
5874
+ * (home, debajo del carousel) se deja en su alto natural.
5875
+ *
5876
+ * <kf-reserve-block emptyLabel="No tienes reservas de taller" grow
4837
5877
  * title="¿Necesitas mantención?" description="Tenemos la mejor red de talleres."
4838
5878
  * actionLabel="Agenda tu reserva aquí" actionHref="/taller/agendar">
4839
5879
  * <kf-button actions icon="calendar">Reservar hora</kf-button>
@@ -4851,13 +5891,15 @@ class ReserveBlock {
4851
5891
  ...(ngDevMode ? [undefined, { debugName: "actionLabel" }] : /* istanbul ignore next */ []));
4852
5892
  actionHref = input(/* @ts-ignore */
4853
5893
  ...(ngDevMode ? [undefined, { debugName: "actionHref" }] : /* istanbul ignore next */ []));
5894
+ /** La invitación se estira para llenar la tarjeta (ej. junto a "Total de vehículos"). */
5895
+ grow = input(false, { ...(ngDevMode ? { debugName: "grow" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4854
5896
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ReserveBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
4855
- 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 } }, ngImport: i0, template: `
5897
+ 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: `
4856
5898
  <div class="kf-reserve-block__empty">
4857
5899
  <kf-icon name="calendar" size="sm" />
4858
5900
  <span>{{ emptyLabel() }}</span>
4859
5901
  </div>
4860
- <div class="kf-reserve-block__invite">
5902
+ <div class="kf-reserve-block__invite" [class.kf-reserve-block__invite--grow]="grow()">
4861
5903
  <span class="kf-reserve-block__invite-icon">
4862
5904
  <kf-icon name="tool" size="md" />
4863
5905
  </span>
@@ -4877,7 +5919,7 @@ class ReserveBlock {
4877
5919
  <div class="kf-reserve-block__actions">
4878
5920
  <ng-content select="[actions]" />
4879
5921
  </div>
4880
- `, isInline: true, 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:flex-start;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-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{display:inline-flex;align-items:center;gap:2px;margin-top:var(--kf-space-xs);font-size:.8125rem;font-weight:600;color:var(--kf-color-primary);text-decoration:none}.kf-reserve-block__invite-action:hover{text-decoration:underline}.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"] }] });
5922
+ `, isInline: true, 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{display:inline-flex;align-items:center;gap:2px;margin-top:var(--kf-space-xs);font-size:.8125rem;font-weight:600;color:var(--kf-color-primary);text-decoration:none}.kf-reserve-block__invite-action:hover{text-decoration:underline}.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"] }] });
4881
5923
  }
4882
5924
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ReserveBlock, decorators: [{
4883
5925
  type: Component,
@@ -4886,7 +5928,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4886
5928
  <kf-icon name="calendar" size="sm" />
4887
5929
  <span>{{ emptyLabel() }}</span>
4888
5930
  </div>
4889
- <div class="kf-reserve-block__invite">
5931
+ <div class="kf-reserve-block__invite" [class.kf-reserve-block__invite--grow]="grow()">
4890
5932
  <span class="kf-reserve-block__invite-icon">
4891
5933
  <kf-icon name="tool" size="md" />
4892
5934
  </span>
@@ -4906,8 +5948,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4906
5948
  <div class="kf-reserve-block__actions">
4907
5949
  <ng-content select="[actions]" />
4908
5950
  </div>
4909
- `, 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:flex-start;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-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{display:inline-flex;align-items:center;gap:2px;margin-top:var(--kf-space-xs);font-size:.8125rem;font-weight:600;color:var(--kf-color-primary);text-decoration:none}.kf-reserve-block__invite-action:hover{text-decoration:underline}.kf-reserve-block__actions{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}\n"] }]
4910
- }], 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 }] }] } });
5951
+ `, 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{display:inline-flex;align-items:center;gap:2px;margin-top:var(--kf-space-xs);font-size:.8125rem;font-weight:600;color:var(--kf-color-primary);text-decoration:none}.kf-reserve-block__invite-action:hover{text-decoration:underline}.kf-reserve-block__actions{display:flex;flex-wrap:wrap;gap:var(--kf-space-sm)}\n"] }]
5952
+ }], 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 }] }] } });
4911
5953
 
4912
5954
  /**
4913
5955
  * Título de sub-sección con chip de ícono, pensado para anidarse dentro de
@@ -5072,6 +6114,471 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
5072
6114
  `, styles: [":host{display:inline-block}.kf-segmented__track{position:relative;display:grid;grid-template-columns:repeat(var(--kf-segmented-count),1fr);gap:2px;padding:2px;background:var(--kf-color-surface);border:1px solid var(--kf-color-border);border-radius:var(--kf-radius-md)}.kf-segmented__thumb{position:absolute;top:2px;left:2px;width:calc((100% - 4px) / var(--kf-segmented-count));height:calc(100% - 4px);background:var(--kf-color-white);border-radius:var(--kf-radius-sm);box-shadow:var(--kf-shadow-xs);transform:translate(calc(var(--kf-segmented-index, 0) * 100%));transition:transform .2s ease}.kf-segmented__option{position:relative;z-index:1;font:inherit;padding:var(--kf-space-xs) var(--kf-space-md);border:none;background:transparent;color:var(--kf-color-text-muted);border-radius:var(--kf-radius-sm);cursor:pointer;transition:color .14s}.kf-segmented__option:hover:not(.kf-segmented__option--active){color:var(--kf-color-text)}.kf-segmented__option--active{color:var(--kf-color-ink)}.kf-segmented__option:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus)}\n"] }]
5073
6115
  }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
5074
6116
 
6117
+ /**
6118
+ * Contenido visual del tooltip — componente interno, no exportado en
6119
+ * `public-api.ts`. `TooltipDirective` es la única forma pública de usarlo.
6120
+ */
6121
+ class TooltipContent {
6122
+ text = '';
6123
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TooltipContent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6124
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: TooltipContent, isStandalone: true, selector: "kf-tooltip-content", ngImport: i0, template: `{{ text }}`, isInline: true, styles: [":host{display:block;max-width:220px;padding:4px var(--kf-space-sm);border-radius:var(--kf-radius-sm);background:var(--kf-color-tertiary-deep);color:var(--kf-color-white);font-family:var(--kf-font-body);font-size:.75rem;font-weight:600;line-height:1.4;white-space:nowrap;box-shadow:var(--kf-shadow-lg);animation:kf-tooltip-enter .1s ease-out;pointer-events:none}@media(prefers-reduced-motion:reduce){:host{animation:none}}@keyframes kf-tooltip-enter{0%{opacity:0}to{opacity:1}}\n"] });
6125
+ }
6126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TooltipContent, decorators: [{
6127
+ type: Component,
6128
+ args: [{ selector: 'kf-tooltip-content', template: `{{ text }}`, styles: [":host{display:block;max-width:220px;padding:4px var(--kf-space-sm);border-radius:var(--kf-radius-sm);background:var(--kf-color-tertiary-deep);color:var(--kf-color-white);font-family:var(--kf-font-body);font-size:.75rem;font-weight:600;line-height:1.4;white-space:nowrap;box-shadow:var(--kf-shadow-lg);animation:kf-tooltip-enter .1s ease-out;pointer-events:none}@media(prefers-reduced-motion:reduce){:host{animation:none}}@keyframes kf-tooltip-enter{0%{opacity:0}to{opacity:1}}\n"] }]
6129
+ }] });
6130
+ /**
6131
+ * Tooltip de hover/focus — reemplaza el atributo `title` nativo del
6132
+ * navegador (lento en aparecer, sin estilo, inconsistente entre
6133
+ * navegadores/SO) por un panel propio, mismo mecanismo de
6134
+ * `@angular/cdk/overlay` que ya usa `Dropdown`.
6135
+ *
6136
+ * Pensado para el caso "ícono sin label visible necesita comunicar qué
6137
+ * es" (ej. `kf-sidebar-nav-item` colapsado). No es two-way: a diferencia
6138
+ * de `Dropdown`, esto se abre/cierra solo con hover/focus, nunca por click.
6139
+ *
6140
+ * <a [kfTooltip]="label()" kfTooltipPosition="right">…</a>
6141
+ */
6142
+ class TooltipDirective {
6143
+ text = '';
6144
+ kfTooltipPosition = 'right';
6145
+ host = inject((ElementRef));
6146
+ overlayService = inject(Overlay);
6147
+ viewContainerRef = inject(ViewContainerRef);
6148
+ injector = inject(Injector);
6149
+ overlayRef = null;
6150
+ show() {
6151
+ if (!this.text || this.overlayRef?.hasAttached()) {
6152
+ return;
6153
+ }
6154
+ const positionStrategy = this.overlayService
6155
+ .position()
6156
+ .flexibleConnectedTo(this.host)
6157
+ .withPositions(this.positionsFor(this.kfTooltipPosition))
6158
+ .withViewportMargin(8)
6159
+ .withPush(true);
6160
+ const overlayRef = this.overlayService.create({
6161
+ positionStrategy,
6162
+ scrollStrategy: this.overlayService.scrollStrategies.reposition(),
6163
+ });
6164
+ const portal = new ComponentPortal(TooltipContent, this.viewContainerRef, this.injector);
6165
+ const componentRef = overlayRef.attach(portal);
6166
+ componentRef.instance.text = this.text;
6167
+ this.overlayRef = overlayRef;
6168
+ }
6169
+ hide() {
6170
+ this.overlayRef?.dispose();
6171
+ this.overlayRef = null;
6172
+ }
6173
+ positionsFor(placement) {
6174
+ const gap = 12;
6175
+ switch (placement) {
6176
+ case 'right':
6177
+ return [
6178
+ {
6179
+ originX: 'end',
6180
+ originY: 'center',
6181
+ overlayX: 'start',
6182
+ overlayY: 'center',
6183
+ offsetX: gap,
6184
+ },
6185
+ ];
6186
+ case 'left':
6187
+ return [
6188
+ {
6189
+ originX: 'start',
6190
+ originY: 'center',
6191
+ overlayX: 'end',
6192
+ overlayY: 'center',
6193
+ offsetX: -gap,
6194
+ },
6195
+ ];
6196
+ case 'top':
6197
+ return [
6198
+ {
6199
+ originX: 'center',
6200
+ originY: 'top',
6201
+ overlayX: 'center',
6202
+ overlayY: 'bottom',
6203
+ offsetY: -gap,
6204
+ },
6205
+ ];
6206
+ case 'bottom':
6207
+ default:
6208
+ return [
6209
+ {
6210
+ originX: 'center',
6211
+ originY: 'bottom',
6212
+ overlayX: 'center',
6213
+ overlayY: 'top',
6214
+ offsetY: gap,
6215
+ },
6216
+ ];
6217
+ }
6218
+ }
6219
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
6220
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.0", type: TooltipDirective, isStandalone: true, selector: "[kfTooltip]", inputs: { text: ["kfTooltip", "text"], kfTooltipPosition: "kfTooltipPosition" }, host: { listeners: { "mouseenter": "show()", "focus": "show()", "mouseleave": "hide()", "blur": "hide()", "document:keydown.escape": "hide()" } }, ngImport: i0 });
6221
+ }
6222
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TooltipDirective, decorators: [{
6223
+ type: Directive,
6224
+ args: [{
6225
+ selector: '[kfTooltip]',
6226
+ }]
6227
+ }], propDecorators: { text: [{
6228
+ type: Input$1,
6229
+ args: ['kfTooltip']
6230
+ }], kfTooltipPosition: [{
6231
+ type: Input$1
6232
+ }], show: [{
6233
+ type: HostListener,
6234
+ args: ['mouseenter']
6235
+ }, {
6236
+ type: HostListener,
6237
+ args: ['focus']
6238
+ }], hide: [{
6239
+ type: HostListener,
6240
+ args: ['mouseleave']
6241
+ }, {
6242
+ type: HostListener,
6243
+ args: ['blur']
6244
+ }, {
6245
+ type: HostListener,
6246
+ args: ['document:keydown.escape']
6247
+ }] } });
6248
+
6249
+ /**
6250
+ * Ítem de navegación de `kf-sidebar-shell` — ícono + label en fila, estado
6251
+ * activo, sin agrupación. Es el equivalente vertical/persistente del `<a>`
6252
+ * que arma `kf-portal-header` para su `<nav>` horizontal.
6253
+ *
6254
+ * `collapsed` se recibe como input explícito (no vía `:host-context`): el
6255
+ * componente no necesita saber cómo funciona el estado de colapso del
6256
+ * shell, solo qué hacer cuando `collapsed` es `true`.
6257
+ *
6258
+ * No sabe nada de routing — emite `href` como cualquier link y deja que el
6259
+ * consumidor intercepte el click para integrarlo con el Router.
6260
+ */
6261
+ class SidebarNavItem {
6262
+ icon = input.required(/* @ts-ignore */
6263
+ ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
6264
+ label = input.required(/* @ts-ignore */
6265
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
6266
+ href = input.required(/* @ts-ignore */
6267
+ ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
6268
+ active = input(false, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
6269
+ /** Oculta el label y centra el ícono — lo controla `kf-sidebar-shell` según su propio estado de colapso. */
6270
+ collapsed = input(false, { ...(ngDevMode ? { debugName: "collapsed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
6271
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: SidebarNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
6272
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: SidebarNavItem, isStandalone: true, selector: "kf-sidebar-nav-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
6273
+ <a
6274
+ class="kf-sidebar-nav-item"
6275
+ [class.kf-sidebar-nav-item--active]="active()"
6276
+ [class.kf-sidebar-nav-item--collapsed]="collapsed()"
6277
+ [href]="href()"
6278
+ [attr.aria-current]="active() ? 'page' : null"
6279
+ [kfTooltip]="collapsed() ? label() : ''"
6280
+ >
6281
+ <kf-icon [name]="icon()" size="md" />
6282
+ @if (!collapsed()) {
6283
+ <span class="kf-sidebar-nav-item__label">{{ label() }}</span>
6284
+ }
6285
+ </a>
6286
+ `, isInline: true, styles: [".kf-sidebar-nav-item{box-sizing:border-box;position:relative;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border-radius:var(--kf-radius-sm);color:#ffffffb3;font-family:var(--kf-font-body);font-size:.875rem;font-weight:600;text-decoration:none;white-space:nowrap;transition:background .14s,color .14s}.kf-sidebar-nav-item:hover:not(.kf-sidebar-nav-item--active){background:#ffffff0f;color:var(--kf-color-white)}.kf-sidebar-nav-item--collapsed:hover:not(.kf-sidebar-nav-item--active){background:#ffffff1f}.kf-sidebar-nav-item--active{background:#ffffff1f;color:var(--kf-color-white)}.kf-sidebar-nav-item--active:before{content:\"\";position:absolute;left:calc(-1 * var(--kf-space-xs));top:20%;bottom:20%;width:3px;border-radius:var(--kf-radius-pill);background:var(--kf-color-primary-hover)}.kf-sidebar-nav-item--collapsed{justify-content:center;width:40px;height:40px;padding-inline:0}.kf-sidebar-nav-item--collapsed.kf-sidebar-nav-item--active:before{left:calc(-1 * var(--kf-space-sm))}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }, { kind: "directive", type: TooltipDirective, selector: "[kfTooltip]", inputs: ["kfTooltip", "kfTooltipPosition"] }] });
6287
+ }
6288
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: SidebarNavItem, decorators: [{
6289
+ type: Component,
6290
+ args: [{ selector: 'kf-sidebar-nav-item', imports: [Icon, TooltipDirective], template: `
6291
+ <a
6292
+ class="kf-sidebar-nav-item"
6293
+ [class.kf-sidebar-nav-item--active]="active()"
6294
+ [class.kf-sidebar-nav-item--collapsed]="collapsed()"
6295
+ [href]="href()"
6296
+ [attr.aria-current]="active() ? 'page' : null"
6297
+ [kfTooltip]="collapsed() ? label() : ''"
6298
+ >
6299
+ <kf-icon [name]="icon()" size="md" />
6300
+ @if (!collapsed()) {
6301
+ <span class="kf-sidebar-nav-item__label">{{ label() }}</span>
6302
+ }
6303
+ </a>
6304
+ `, styles: [".kf-sidebar-nav-item{box-sizing:border-box;position:relative;display:flex;align-items:center;gap:var(--kf-space-sm);padding:var(--kf-space-sm) var(--kf-space-md);border-radius:var(--kf-radius-sm);color:#ffffffb3;font-family:var(--kf-font-body);font-size:.875rem;font-weight:600;text-decoration:none;white-space:nowrap;transition:background .14s,color .14s}.kf-sidebar-nav-item:hover:not(.kf-sidebar-nav-item--active){background:#ffffff0f;color:var(--kf-color-white)}.kf-sidebar-nav-item--collapsed:hover:not(.kf-sidebar-nav-item--active){background:#ffffff1f}.kf-sidebar-nav-item--active{background:#ffffff1f;color:var(--kf-color-white)}.kf-sidebar-nav-item--active:before{content:\"\";position:absolute;left:calc(-1 * var(--kf-space-xs));top:20%;bottom:20%;width:3px;border-radius:var(--kf-radius-pill);background:var(--kf-color-primary-hover)}.kf-sidebar-nav-item--collapsed{justify-content:center;width:40px;height:40px;padding-inline:0}.kf-sidebar-nav-item--collapsed.kf-sidebar-nav-item--active:before{left:calc(-1 * var(--kf-space-sm))}\n"] }]
6305
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }] } });
6306
+
6307
+ /**
6308
+ * Signal reactivo con el estado actual de un media query — para cuando un
6309
+ * componente necesita SABER en qué lado de un breakpoint está (no solo
6310
+ * mostrar/ocultar con CSS). Un `@media` en `styles` no alcanza cuando la
6311
+ * decisión no es puramente visual: por ejemplo, un componente hijo que
6312
+ * recibe un input booleano (`[collapsed]`) no puede "leer" un breakpoint
6313
+ * CSS del padre — necesita que alguien en TypeScript se lo diga.
6314
+ *
6315
+ * Encontrado al construir `SidebarShell`: el estado `collapsed` (colapso
6316
+ * del rail en desktop) se pasaba tal cual a cada `kf-sidebar-nav-item`, sin
6317
+ * importar el viewport. En mobile, donde el rail deslizado siempre muestra
6318
+ * etiquetas completas (ver estilos de `SidebarShell`), los ítems seguían
6319
+ * ocultando su label si el usuario había dejado el rail colapsado en
6320
+ * desktop antes de achicar la ventana — un CSS que dice "en mobile se ve
6321
+ * ancho" no cambia el valor del input que ya llegó al hijo.
6322
+ *
6323
+ * Debe llamarse dentro de un contexto de inyección (el `constructor` de un
6324
+ * componente, o un campo de clase inicializado ahí — mismo requisito que
6325
+ * cualquier función que use `inject()`).
6326
+ *
6327
+ * protected readonly isMobile = matchMediaSignal('(max-width: 900px)');
6328
+ * // ...
6329
+ * [collapsed]="collapsed() && !isMobile()"
6330
+ */
6331
+ function matchMediaSignal(query) {
6332
+ const mediaQueryList = matchMedia(query);
6333
+ const state = signal(mediaQueryList.matches, /* @ts-ignore */
6334
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
6335
+ const onChange = (event) => state.set(event.matches);
6336
+ mediaQueryList.addEventListener('change', onChange);
6337
+ inject(DestroyRef).onDestroy(() => mediaQueryList.removeEventListener('change', onChange));
6338
+ return state.asReadonly();
6339
+ }
6340
+
6341
+ /** Mismo breakpoint que el `@media` de este componente — ver estilos. */
6342
+ const MOBILE_BREAKPOINT_QUERY = '(max-width: 900px)';
6343
+ /**
6344
+ * Shell de layout con navegación lateral persistente — el complemento
6345
+ * vertical de `kf-portal-header` (navbar horizontal). Pensado para
6346
+ * portales tipo "aplicación interna" (Portal OTD, Servicio Star).
6347
+ *
6348
+ * `navItems` se recibe como array de datos (no como contenido proyectado):
6349
+ * el componente no sabe nada de routing, emite `navItemClick` con el ítem
6350
+ * clickeado y deja que el consumidor decida cómo navegar.
6351
+ *
6352
+ * Slots: `[kf-sidebar-brand]` (logo del rail expandido, visible en desktop
6353
+ * y en el panel mobile — es el MISMO rail), `[kf-sidebar-brand-collapsed]`
6354
+ * (reemplazo opcional cuando el rail está colapsado en desktop),
6355
+ * `[kf-sidebar-brand-mobile]` (logo de la topbar angosta de mobile, fondo
6356
+ * claro) y `[kf-sidebar-footer]` (tarjeta de usuario / acciones al pie).
6357
+ *
6358
+ * Botón de colapso: vive como último ítem de la propia lista de
6359
+ * navegación, no flotando en el borde del rail — mismo criterio de
6360
+ * tap-target grande y predecible que el resto de los ítems.
6361
+ *
6362
+ * Responsive: bajo los 900px el sidebar NO se reemplaza por un panel
6363
+ * distinto — es el MISMO rail el que pasa a `position: fixed` fuera de la
6364
+ * pantalla (`translateX(-100%)`) y se desliza a la vista (`translateX(0)`)
6365
+ * sobre un backdrop cuando `mobileOpen` es `true`. Una topbar angosta con
6366
+ * botón de hamburguesa reemplaza al rail en el flujo del documento
6367
+ * mientras el panel está cerrado.
6368
+ *
6369
+ * A propósito NO reutiliza `kf-drawer` (el panel lateral genérico de la
6370
+ * librería): `kf-drawer` tiene su propio fondo claro fijo y duplicaría el
6371
+ * árbol de navegación en un segundo lugar del DOM. Bug de contraste ya
6372
+ * corregido en el historial: los `kf-sidebar-nav-item`, con texto claro
6373
+ * pensado para el gradiente oscuro, quedaban invisibles sobre el fondo
6374
+ * blanco de `kf-drawer`. Acá hay un solo rail, un solo árbol de nav, que
6375
+ * cambia de posición según el viewport.
6376
+ */
6377
+ class SidebarShell {
6378
+ /** Ítems del menú del rail. */
6379
+ navItems = input([], /* @ts-ignore */
6380
+ ...(ngDevMode ? [{ debugName: "navItems" }] : /* istanbul ignore next */ []));
6381
+ /** Estado de colapso del rail en desktop, two-way. Sin efecto bajo los 900px (ver estilos). */
6382
+ collapsed = model(false, /* @ts-ignore */
6383
+ ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
6384
+ /** Visibilidad del rail deslizado como panel en mobile, two-way. */
6385
+ mobileOpen = model(false, /* @ts-ignore */
6386
+ ...(ngDevMode ? [{ debugName: "mobileOpen" }] : /* istanbul ignore next */ []));
6387
+ /** Se emite al clickear un ítem — el consumidor decide cómo navegar (ej. `router.navigateByUrl`). */
6388
+ navItemClick = output();
6389
+ /** `true` bajo los 900px — sin esto, un rail dejado colapsado en desktop
6390
+ * seguía ocultando el label de cada ítem al abrir el panel en mobile. */
6391
+ isMobileViewport = matchMediaSignal(MOBILE_BREAKPOINT_QUERY);
6392
+ /** Estado de colapso que efectivamente se le pasa a cada `kf-sidebar-nav-item`
6393
+ * y al propio botón de colapso. */
6394
+ itemsCollapsed = computed(() => this.collapsed() && !this.isMobileViewport(), /* @ts-ignore */
6395
+ ...(ngDevMode ? [{ debugName: "itemsCollapsed" }] : /* istanbul ignore next */ []));
6396
+ /** Cierra el panel mobile con Escape — mismo criterio que `Dropdown`/`Modal`/`Drawer`. */
6397
+ onEscape() {
6398
+ this.mobileOpen.set(false);
6399
+ }
6400
+ onNavItemClick(event, item) {
6401
+ event.preventDefault();
6402
+ this.mobileOpen.set(false);
6403
+ this.navItemClick.emit(item);
6404
+ }
6405
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: SidebarShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
6406
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: SidebarShell, isStandalone: true, selector: "kf-sidebar-shell", inputs: { navItems: { classPropertyName: "navItems", publicName: "navItems", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, mobileOpen: { classPropertyName: "mobileOpen", publicName: "mobileOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", mobileOpen: "mobileOpenChange", navItemClick: "navItemClick" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: `
6407
+ @if (mobileOpen()) {
6408
+ <button
6409
+ type="button"
6410
+ class="kf-sidebar-shell__backdrop"
6411
+ aria-label="Cerrar menú"
6412
+ (click)="mobileOpen.set(false)"
6413
+ ></button>
6414
+ }
6415
+
6416
+ <aside
6417
+ class="kf-sidebar-shell__rail"
6418
+ [class.kf-sidebar-shell__rail--collapsed]="collapsed()"
6419
+ [class.kf-sidebar-shell__rail--mobile-open]="mobileOpen()"
6420
+ >
6421
+ <div class="kf-sidebar-shell__brand">
6422
+ <div class="kf-sidebar-shell__brand-full">
6423
+ <ng-content select="[kf-sidebar-brand]" />
6424
+ </div>
6425
+ <div class="kf-sidebar-shell__brand-collapsed">
6426
+ <ng-content select="[kf-sidebar-brand-collapsed]" />
6427
+ </div>
6428
+
6429
+ <button
6430
+ type="button"
6431
+ class="kf-sidebar-shell__mobile-close"
6432
+ aria-label="Cerrar menú"
6433
+ (click)="mobileOpen.set(false)"
6434
+ >
6435
+ <kf-icon name="x" size="md" />
6436
+ </button>
6437
+ </div>
6438
+
6439
+ <nav class="kf-sidebar-shell__nav">
6440
+ @for (item of navItems(); track item.href) {
6441
+ <kf-sidebar-nav-item
6442
+ [icon]="item.icon"
6443
+ [label]="item.label"
6444
+ [href]="item.href"
6445
+ [active]="!!item.active"
6446
+ [collapsed]="itemsCollapsed()"
6447
+ (click)="onNavItemClick($event, item)"
6448
+ />
6449
+ }
6450
+ </nav>
6451
+
6452
+ <div class="kf-sidebar-shell__bottom">
6453
+ <button
6454
+ type="button"
6455
+ class="kf-sidebar-shell__collapse-toggle"
6456
+ [class.kf-sidebar-shell__collapse-toggle--collapsed]="itemsCollapsed()"
6457
+ [attr.aria-expanded]="!collapsed()"
6458
+ [attr.aria-label]="collapsed() ? 'Expandir menú' : 'Colapsar menú'"
6459
+ [kfTooltip]="itemsCollapsed() ? (collapsed() ? 'Expandir menú' : 'Colapsar menú') : ''"
6460
+ (click)="collapsed.set(!collapsed())"
6461
+ >
6462
+ <kf-icon [name]="collapsed() ? 'chevron-right' : 'chevron-left'" size="sm" />
6463
+ @if (!itemsCollapsed()) {
6464
+ <span class="kf-sidebar-shell__collapse-toggle-label">
6465
+ {{ collapsed() ? 'Expandir menú' : 'Colapsar menú' }}
6466
+ </span>
6467
+ }
6468
+ </button>
6469
+
6470
+ <div class="kf-sidebar-shell__footer">
6471
+ <ng-content select="[kf-sidebar-footer]" />
6472
+ </div>
6473
+ </div>
6474
+ </aside>
6475
+
6476
+ <div class="kf-sidebar-shell__topbar">
6477
+ <button
6478
+ type="button"
6479
+ class="kf-sidebar-shell__menu-toggle"
6480
+ aria-label="Abrir menú de navegación"
6481
+ (click)="mobileOpen.set(true)"
6482
+ >
6483
+ <kf-icon name="menu" size="md" />
6484
+ </button>
6485
+ <div class="kf-sidebar-shell__topbar-brand">
6486
+ <ng-content select="[kf-sidebar-brand-mobile]" />
6487
+ </div>
6488
+ </div>
6489
+ `, isInline: true, styles: [":host{display:contents}.kf-sidebar-shell__backdrop{display:none}.kf-sidebar-shell__rail{box-sizing:border-box;display:flex;flex-direction:column;position:relative;width:268px;flex:none;padding:var(--kf-space-lg) var(--kf-space-md);background:linear-gradient(190deg,var(--kf-color-sidebar-gradient-from) 0%,var(--kf-color-sidebar-gradient-to) 60%);transition:width .18s ease,padding-inline .18s ease}.kf-sidebar-shell__rail--collapsed{width:56px;padding-inline:var(--kf-space-sm)}.kf-sidebar-shell__brand{position:relative;display:flex;align-items:center;justify-content:center;margin-bottom:var(--kf-space-xl)}.kf-sidebar-shell__brand-full{display:flex;justify-content:center;width:100%}.kf-sidebar-shell__brand-full ::ng-deep img{display:block;height:1.5rem}.kf-sidebar-shell__brand-collapsed{display:none}.kf-sidebar-shell__brand-collapsed ::ng-deep img{display:block;height:1.75rem}.kf-sidebar-shell__rail--collapsed .kf-sidebar-shell__brand-full{display:none}.kf-sidebar-shell__rail--collapsed .kf-sidebar-shell__brand-collapsed{display:block}.kf-sidebar-shell__mobile-close{display:none;margin-left:auto;align-items:center;justify-content:center;width:2rem;height:2rem;border:none;border-radius:var(--kf-radius-sm);background:#ffffff14;color:#fffc;cursor:pointer}.kf-sidebar-shell__mobile-close:hover{background:#ffffff29;color:var(--kf-color-white)}.kf-sidebar-shell__nav{flex:1 1 auto;display:flex;flex-direction:column;gap:var(--kf-space-xs);overflow-y:auto}.kf-sidebar-shell__bottom{display:flex;flex-direction:column;margin-top:var(--kf-space-sm);padding-top:var(--kf-space-sm);border-top:1px solid rgba(255,255,255,.12)}.kf-sidebar-shell__footer{margin-top:var(--kf-space-sm)}.kf-sidebar-shell__footer:empty{display:none}.kf-sidebar-shell__collapse-toggle{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;border-radius:var(--kf-radius-sm);background:#ffffff0f;color:#ffffffb3;font-family:var(--kf-font-body);font-size:.8125rem;font-weight:600;cursor:pointer;transition:background .14s,color .14s}.kf-sidebar-shell__collapse-toggle:hover{background:#ffffff1f;color:var(--kf-color-white)}.kf-sidebar-shell__collapse-toggle--collapsed{justify-content:center;width:40px;height:40px;padding-inline:0}.kf-sidebar-shell__collapse-toggle-label{flex:1 1 auto;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kf-sidebar-shell__topbar{display:none}@media(max-width:900px){.kf-sidebar-shell__rail{position:fixed;inset:0 auto 0 0;z-index:100;width:284px;max-width:86vw;transform:translate(-100%);transition:transform .26s cubic-bezier(.4,0,.2,1);box-shadow:var(--kf-shadow-lg)}.kf-sidebar-shell__rail--mobile-open{transform:translate(0)}.kf-sidebar-shell__rail--collapsed{width:284px;max-width:86vw}.kf-sidebar-shell__rail--collapsed .kf-sidebar-shell__brand-full{display:block}.kf-sidebar-shell__rail--collapsed .kf-sidebar-shell__brand-collapsed,.kf-sidebar-shell__collapse-toggle{display:none}.kf-sidebar-shell__mobile-close{display:flex}.kf-sidebar-shell__backdrop{display:block;position:fixed;inset:0;z-index:90;border:none;padding:0;cursor:pointer;background:#0a0e1480}.kf-sidebar-shell__topbar{display:flex;align-items:center;gap:var(--kf-space-md);padding:var(--kf-space-sm) var(--kf-space-lg);background:var(--kf-color-white);border-bottom:1px solid var(--kf-color-border)}.kf-sidebar-shell__menu-toggle{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--kf-radius-sm);background:transparent;color:var(--kf-color-ink);cursor:pointer;flex:none}.kf-sidebar-shell__menu-toggle:hover{background:var(--kf-color-surface)}.kf-sidebar-shell__topbar-brand ::ng-deep img{display:block;height:1.25rem}}\n"], dependencies: [{ kind: "component", type: Icon, selector: "kf-icon", inputs: ["name", "size"] }, { kind: "component", type: SidebarNavItem, selector: "kf-sidebar-nav-item", inputs: ["icon", "label", "href", "active", "collapsed"] }, { kind: "directive", type: TooltipDirective, selector: "[kfTooltip]", inputs: ["kfTooltip", "kfTooltipPosition"] }] });
6490
+ }
6491
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: SidebarShell, decorators: [{
6492
+ type: Component,
6493
+ args: [{ selector: 'kf-sidebar-shell', imports: [Icon, SidebarNavItem, TooltipDirective], template: `
6494
+ @if (mobileOpen()) {
6495
+ <button
6496
+ type="button"
6497
+ class="kf-sidebar-shell__backdrop"
6498
+ aria-label="Cerrar menú"
6499
+ (click)="mobileOpen.set(false)"
6500
+ ></button>
6501
+ }
6502
+
6503
+ <aside
6504
+ class="kf-sidebar-shell__rail"
6505
+ [class.kf-sidebar-shell__rail--collapsed]="collapsed()"
6506
+ [class.kf-sidebar-shell__rail--mobile-open]="mobileOpen()"
6507
+ >
6508
+ <div class="kf-sidebar-shell__brand">
6509
+ <div class="kf-sidebar-shell__brand-full">
6510
+ <ng-content select="[kf-sidebar-brand]" />
6511
+ </div>
6512
+ <div class="kf-sidebar-shell__brand-collapsed">
6513
+ <ng-content select="[kf-sidebar-brand-collapsed]" />
6514
+ </div>
6515
+
6516
+ <button
6517
+ type="button"
6518
+ class="kf-sidebar-shell__mobile-close"
6519
+ aria-label="Cerrar menú"
6520
+ (click)="mobileOpen.set(false)"
6521
+ >
6522
+ <kf-icon name="x" size="md" />
6523
+ </button>
6524
+ </div>
6525
+
6526
+ <nav class="kf-sidebar-shell__nav">
6527
+ @for (item of navItems(); track item.href) {
6528
+ <kf-sidebar-nav-item
6529
+ [icon]="item.icon"
6530
+ [label]="item.label"
6531
+ [href]="item.href"
6532
+ [active]="!!item.active"
6533
+ [collapsed]="itemsCollapsed()"
6534
+ (click)="onNavItemClick($event, item)"
6535
+ />
6536
+ }
6537
+ </nav>
6538
+
6539
+ <div class="kf-sidebar-shell__bottom">
6540
+ <button
6541
+ type="button"
6542
+ class="kf-sidebar-shell__collapse-toggle"
6543
+ [class.kf-sidebar-shell__collapse-toggle--collapsed]="itemsCollapsed()"
6544
+ [attr.aria-expanded]="!collapsed()"
6545
+ [attr.aria-label]="collapsed() ? 'Expandir menú' : 'Colapsar menú'"
6546
+ [kfTooltip]="itemsCollapsed() ? (collapsed() ? 'Expandir menú' : 'Colapsar menú') : ''"
6547
+ (click)="collapsed.set(!collapsed())"
6548
+ >
6549
+ <kf-icon [name]="collapsed() ? 'chevron-right' : 'chevron-left'" size="sm" />
6550
+ @if (!itemsCollapsed()) {
6551
+ <span class="kf-sidebar-shell__collapse-toggle-label">
6552
+ {{ collapsed() ? 'Expandir menú' : 'Colapsar menú' }}
6553
+ </span>
6554
+ }
6555
+ </button>
6556
+
6557
+ <div class="kf-sidebar-shell__footer">
6558
+ <ng-content select="[kf-sidebar-footer]" />
6559
+ </div>
6560
+ </div>
6561
+ </aside>
6562
+
6563
+ <div class="kf-sidebar-shell__topbar">
6564
+ <button
6565
+ type="button"
6566
+ class="kf-sidebar-shell__menu-toggle"
6567
+ aria-label="Abrir menú de navegación"
6568
+ (click)="mobileOpen.set(true)"
6569
+ >
6570
+ <kf-icon name="menu" size="md" />
6571
+ </button>
6572
+ <div class="kf-sidebar-shell__topbar-brand">
6573
+ <ng-content select="[kf-sidebar-brand-mobile]" />
6574
+ </div>
6575
+ </div>
6576
+ `, styles: [":host{display:contents}.kf-sidebar-shell__backdrop{display:none}.kf-sidebar-shell__rail{box-sizing:border-box;display:flex;flex-direction:column;position:relative;width:268px;flex:none;padding:var(--kf-space-lg) var(--kf-space-md);background:linear-gradient(190deg,var(--kf-color-sidebar-gradient-from) 0%,var(--kf-color-sidebar-gradient-to) 60%);transition:width .18s ease,padding-inline .18s ease}.kf-sidebar-shell__rail--collapsed{width:56px;padding-inline:var(--kf-space-sm)}.kf-sidebar-shell__brand{position:relative;display:flex;align-items:center;justify-content:center;margin-bottom:var(--kf-space-xl)}.kf-sidebar-shell__brand-full{display:flex;justify-content:center;width:100%}.kf-sidebar-shell__brand-full ::ng-deep img{display:block;height:1.5rem}.kf-sidebar-shell__brand-collapsed{display:none}.kf-sidebar-shell__brand-collapsed ::ng-deep img{display:block;height:1.75rem}.kf-sidebar-shell__rail--collapsed .kf-sidebar-shell__brand-full{display:none}.kf-sidebar-shell__rail--collapsed .kf-sidebar-shell__brand-collapsed{display:block}.kf-sidebar-shell__mobile-close{display:none;margin-left:auto;align-items:center;justify-content:center;width:2rem;height:2rem;border:none;border-radius:var(--kf-radius-sm);background:#ffffff14;color:#fffc;cursor:pointer}.kf-sidebar-shell__mobile-close:hover{background:#ffffff29;color:var(--kf-color-white)}.kf-sidebar-shell__nav{flex:1 1 auto;display:flex;flex-direction:column;gap:var(--kf-space-xs);overflow-y:auto}.kf-sidebar-shell__bottom{display:flex;flex-direction:column;margin-top:var(--kf-space-sm);padding-top:var(--kf-space-sm);border-top:1px solid rgba(255,255,255,.12)}.kf-sidebar-shell__footer{margin-top:var(--kf-space-sm)}.kf-sidebar-shell__footer:empty{display:none}.kf-sidebar-shell__collapse-toggle{box-sizing:border-box;display:flex;align-items:center;gap:var(--kf-space-sm);width:100%;padding:var(--kf-space-sm) var(--kf-space-md);border:none;border-radius:var(--kf-radius-sm);background:#ffffff0f;color:#ffffffb3;font-family:var(--kf-font-body);font-size:.8125rem;font-weight:600;cursor:pointer;transition:background .14s,color .14s}.kf-sidebar-shell__collapse-toggle:hover{background:#ffffff1f;color:var(--kf-color-white)}.kf-sidebar-shell__collapse-toggle--collapsed{justify-content:center;width:40px;height:40px;padding-inline:0}.kf-sidebar-shell__collapse-toggle-label{flex:1 1 auto;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kf-sidebar-shell__topbar{display:none}@media(max-width:900px){.kf-sidebar-shell__rail{position:fixed;inset:0 auto 0 0;z-index:100;width:284px;max-width:86vw;transform:translate(-100%);transition:transform .26s cubic-bezier(.4,0,.2,1);box-shadow:var(--kf-shadow-lg)}.kf-sidebar-shell__rail--mobile-open{transform:translate(0)}.kf-sidebar-shell__rail--collapsed{width:284px;max-width:86vw}.kf-sidebar-shell__rail--collapsed .kf-sidebar-shell__brand-full{display:block}.kf-sidebar-shell__rail--collapsed .kf-sidebar-shell__brand-collapsed,.kf-sidebar-shell__collapse-toggle{display:none}.kf-sidebar-shell__mobile-close{display:flex}.kf-sidebar-shell__backdrop{display:block;position:fixed;inset:0;z-index:90;border:none;padding:0;cursor:pointer;background:#0a0e1480}.kf-sidebar-shell__topbar{display:flex;align-items:center;gap:var(--kf-space-md);padding:var(--kf-space-sm) var(--kf-space-lg);background:var(--kf-color-white);border-bottom:1px solid var(--kf-color-border)}.kf-sidebar-shell__menu-toggle{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:var(--kf-radius-sm);background:transparent;color:var(--kf-color-ink);cursor:pointer;flex:none}.kf-sidebar-shell__menu-toggle:hover{background:var(--kf-color-surface)}.kf-sidebar-shell__topbar-brand ::ng-deep img{display:block;height:1.25rem}}\n"] }]
6577
+ }], propDecorators: { navItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "navItems", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], mobileOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileOpen", required: false }] }, { type: i0.Output, args: ["mobileOpenChange"] }], navItemClick: [{ type: i0.Output, args: ["navItemClick"] }], onEscape: [{
6578
+ type: HostListener,
6579
+ args: ['document:keydown.escape']
6580
+ }] } });
6581
+
5075
6582
  /**
5076
6583
  * Indicador numérico compacto (página 34): label + valor grande, con un
5077
6584
  * ícono a la derecha del label — más chico y sin badge de esquina que
@@ -5340,109 +6847,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
5340
6847
  `, styles: [":host{display:block;font-family:var(--kf-font-body)}.kf-tabs{position:relative;display:flex;align-items:stretch;gap:var(--kf-space-md);border-bottom:1px solid var(--kf-color-border)}.kf-tabs__tab{display:inline-flex;align-items:center;gap:var(--kf-space-xs);font:inherit;font-size:.9rem;color:var(--kf-color-text-muted);background:transparent;border:none;padding:var(--kf-space-sm) 0;cursor:pointer;transition:color .14s}.kf-tabs__tab:hover:not(.kf-tabs__tab--active){color:var(--kf-color-text)}.kf-tabs__tab:focus-visible{outline:none;box-shadow:var(--kf-shadow-focus);border-radius:var(--kf-radius-xs)}.kf-tabs__tab--active{color:var(--kf-color-primary);font-size:1rem;font-weight:700}.kf-tabs__indicator{position:absolute;bottom:-1px;left:0;height:2px;background:var(--kf-color-primary);border-radius:2px 2px 0 0;pointer-events:none;transition:transform .28s cubic-bezier(.32,.72,0,1),width .28s cubic-bezier(.32,.72,0,1),opacity .14s}@media(prefers-reduced-motion:reduce){.kf-tabs__indicator{transition:opacity .14s}}\n"] }]
5341
6848
  }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], track: [{ type: i0.ViewChild, args: ['track', { isSignal: true }] }] } });
5342
6849
 
5343
- /**
5344
- * Tipografía unificada — separa la etiqueta semántica (`as`, qué es el
5345
- * texto para el DOM/accesibilidad) del estilo visual (`variant`, cómo se
5346
- * ve). Antes de este componente, cada pantalla que consume la librería
5347
- * definía su propio `h1`/`p` con `font-size` suelto para el mismo rol
5348
- * (título de página, subtítulo, texto secundario) — auditoría sobre
5349
- * `web-portal-latam-frontend` encontró 9 tamaños distintos entre login y
5350
- * home sin ningún paso de escala en común. `variant` resuelve eso contra
5351
- * la escala de `tokens/base.css` (`--kf-font-size-*`).
5352
- *
5353
- * `tone` sigue la misma convención cerrada que `Alert`/`Badge`/`Chip`: sin
5354
- * escape hatch de color libre. Si un caso necesita un color que `tone` no
5355
- * cubre, es una discusión de design system, no una prop nueva acá.
5356
- *
5357
- * <kf-text as="h1" variant="title-lg">Bienvenido de nuevo</kf-text>
5358
- * <kf-text as="p" variant="body-muted">Ingresá con tu cuenta corporativa.</kf-text>
5359
- * <kf-text as="span" variant="caption" tone="error">Campo obligatorio</kf-text>
5360
- */
5361
- class Text {
5362
- /** Etiqueta semántica real que se renderiza — nunca es solo un `div`. */
5363
- as = input('p', /* @ts-ignore */
5364
- ...(ngDevMode ? [{ debugName: "as" }] : /* istanbul ignore next */ []));
5365
- variant = input('body', /* @ts-ignore */
5366
- ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
5367
- tone = input('neutral', /* @ts-ignore */
5368
- ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
5369
- /**
5370
- * Copia de `as` para uso dentro del template: `as` es palabra reservada
5371
- * en las expresiones de Angular (`@if (x; as y)`), así que `@switch (as())`
5372
- * no parsea — NG5002. La prop pública sigue llamándose `as` (es solo un
5373
- * atributo HTML ahí, `<kf-text as="h1">`, no una expresión).
5374
- */
5375
- tag = computed(() => this.as(), /* @ts-ignore */
5376
- ...(ngDevMode ? [{ debugName: "tag" }] : /* istanbul ignore next */ []));
5377
- hostClass() {
5378
- const classes = [`kf-text--${this.variant()}`];
5379
- if (this.tone() !== 'neutral') {
5380
- classes.push(`kf-text--${this.tone()}`);
5381
- }
5382
- return classes.join(' ');
5383
- }
5384
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Text, deps: [], target: i0.ɵɵFactoryTarget.Component });
5385
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: Text, isStandalone: true, selector: "kf-text", inputs: { as: { classPropertyName: "as", publicName: "as", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
5386
- <!--
5387
- Una sola proyección compartida (ng-template + ngTemplateOutlet) en vez
5388
- de un <ng-content> por cada rama del @switch: Angular solo conecta la
5389
- proyección real del consumidor a la PRIMERA instancia de <ng-content>
5390
- que encuentra en el árbol de compilación — con un <ng-content> por
5391
- rama, todas menos una quedan vacías en runtime (bug detectado por
5392
- Text.spec.ts, no algo documentado a simple vista en los docs de Angular).
5393
- -->
5394
- <ng-template #projected><ng-content /></ng-template>
5395
- @switch (tag()) {
5396
- @case ('h1') {
5397
- <h1 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h1>
5398
- }
5399
- @case ('h2') {
5400
- <h2 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h2>
5401
- }
5402
- @case ('h3') {
5403
- <h3 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h3>
5404
- }
5405
- @case ('span') {
5406
- <span [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></span>
5407
- }
5408
- @default {
5409
- <p [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></p>
5410
- }
5411
- }
5412
- `, isInline: true, styles: [":host{display:contents}h1,h2,h3,p,span{margin:0;font-family:var(--kf-font-body);color:var(--kf-color-text)}.kf-text--title-lg{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-lg);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-md{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-md);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-sm{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-sm);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--body{font-size:var(--kf-font-size-body);font-weight:var(--kf-font-weight-body)}.kf-text--body-muted{font-size:var(--kf-font-size-body);font-weight:var(--kf-font-weight-body);color:var(--kf-color-text-muted)}.kf-text--caption{font-size:var(--kf-font-size-caption);font-weight:var(--kf-font-weight-body);color:var(--kf-color-text-muted)}.kf-text--ok{color:var(--kf-color-success-strong)}.kf-text--warn{color:var(--kf-color-warning-strong)}.kf-text--error{color:var(--kf-color-error-strong)}.kf-text--info{color:var(--kf-color-info-strong)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
5413
- }
5414
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Text, decorators: [{
5415
- type: Component,
5416
- args: [{ selector: 'kf-text', imports: [NgTemplateOutlet], template: `
5417
- <!--
5418
- Una sola proyección compartida (ng-template + ngTemplateOutlet) en vez
5419
- de un <ng-content> por cada rama del @switch: Angular solo conecta la
5420
- proyección real del consumidor a la PRIMERA instancia de <ng-content>
5421
- que encuentra en el árbol de compilación — con un <ng-content> por
5422
- rama, todas menos una quedan vacías en runtime (bug detectado por
5423
- Text.spec.ts, no algo documentado a simple vista en los docs de Angular).
5424
- -->
5425
- <ng-template #projected><ng-content /></ng-template>
5426
- @switch (tag()) {
5427
- @case ('h1') {
5428
- <h1 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h1>
5429
- }
5430
- @case ('h2') {
5431
- <h2 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h2>
5432
- }
5433
- @case ('h3') {
5434
- <h3 [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></h3>
5435
- }
5436
- @case ('span') {
5437
- <span [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></span>
5438
- }
5439
- @default {
5440
- <p [class]="hostClass()"><ng-container [ngTemplateOutlet]="projected" /></p>
5441
- }
5442
- }
5443
- `, styles: [":host{display:contents}h1,h2,h3,p,span{margin:0;font-family:var(--kf-font-body);color:var(--kf-color-text)}.kf-text--title-lg{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-lg);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-md{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-md);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--title-sm{font-family:var(--kf-font-title);font-size:var(--kf-font-size-title-sm);font-weight:var(--kf-font-weight-title);color:var(--kf-color-ink)}.kf-text--body{font-size:var(--kf-font-size-body);font-weight:var(--kf-font-weight-body)}.kf-text--body-muted{font-size:var(--kf-font-size-body);font-weight:var(--kf-font-weight-body);color:var(--kf-color-text-muted)}.kf-text--caption{font-size:var(--kf-font-size-caption);font-weight:var(--kf-font-weight-body);color:var(--kf-color-text-muted)}.kf-text--ok{color:var(--kf-color-success-strong)}.kf-text--warn{color:var(--kf-color-warning-strong)}.kf-text--error{color:var(--kf-color-error-strong)}.kf-text--info{color:var(--kf-color-info-strong)}\n"] }]
5444
- }], propDecorators: { as: [{ type: i0.Input, args: [{ isSignal: true, alias: "as", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
5445
-
5446
6850
  /** Fixed icon per tone (page 20 of the design system) — not configurable per call. */
5447
6851
  const TOAST_ICON = {
5448
6852
  success: 'check-circle',
@@ -5792,5 +7196,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
5792
7196
  * Generated bundle index. Do not edit.
5793
7197
  */
5794
7198
 
5795
- export { ActionTile, AdminBadge, Alert, Avatar, Badge, Breadcrumb, Button, Card, Carousel, CarouselSlide, Checkbox, Chip, CompanySelector, ContentSlot, ContentState, DataTable, Divider, Dropdown, DropdownItem, DropdownPanel, EmptyState, FeatureCard, Field, FieldLabel, FilterChips, FolderTabs, FolderTabsPanel, Icon, Input, Link, MetricCard, Modal, MultiSelect, PageHeader, Pagination, Panel, PortalHeader, ReserveBlock, SectionHeader, Segmented, Select, Skeleton, Spinner, StatTile, StatusPill, Tabs, Text, ToastHost, ToastService, VehicleHeader, ViewToggle };
7199
+ export { ActionTile, AdminBadge, Alert, AuthLayout, Avatar, Badge, Breadcrumb, Button, Card, Carousel, CarouselSlide, Checkbox, Chip, CompanySelector, ContentSlot, ContentState, DataTable, Divider, Drawer, DrawerLogout, DrawerSession, Dropdown, DropdownItem, DropdownPanel, EmptyState, FeatureCard, Field, FieldLabel, FilterChips, FolderTabs, FolderTabsPanel, Footer, Icon, Input, Link, MetricCard, Modal, MultiSelect, PageHeader, Pagination, Panel, PortalHeader, ReserveBlock, SectionHeader, Segmented, Select, SidebarNavItem, SidebarShell, Skeleton, Spinner, StatTile, StatusPill, Tabs, Text, ToastHost, ToastService, TooltipDirective, VehicleHeader, ViewToggle };
5796
7200
  //# sourceMappingURL=kaufmann-lab-ui.mjs.map