@kaufmann-lab/ui 0.1.17 → 0.1.19

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.
@@ -0,0 +1,269 @@
1
+ import { signal, inject, DestroyRef } from '@angular/core';
2
+
3
+ /**
4
+ * Separador de miles genérico — sin signo, sin asumir que el valor es
5
+ * dinero. Cubre casos como kilometraje/horómetro (`formatNumber(45231,
6
+ * { locale: 'en-US' })` → `'45,231'`), no solo moneda.
7
+ *
8
+ * formatNumber(16800); // '16.800'
9
+ * formatNumber(16800.5, { decimals: 2 }); // '16.800,50'
10
+ * formatNumber(45231, { locale: 'en-US' }); // '45,231'
11
+ */
12
+ function formatNumber(value, options = {}) {
13
+ const { decimals = 0, locale = 'es' } = options;
14
+ return value.toLocaleString(locale, {
15
+ minimumFractionDigits: decimals,
16
+ maximumFractionDigits: decimals,
17
+ });
18
+ }
19
+ /**
20
+ * Formatea un monto de dinero — compone sobre `formatNumber` agregando el
21
+ * signo. Mismos defaults que la convención de OTD (punto de miles, sin
22
+ * decimales, prefijo `$`), overrideables para otra moneda/país/formato:
23
+ *
24
+ * formatCurrency(16800); // '$16.800'
25
+ * formatCurrency(16800.5, { decimals: 2 }); // '$16.800,50'
26
+ * formatCurrency(16800, { sign: '' }); // '16.800'
27
+ * formatCurrency(16800, { sign: 'US$', locale: 'en-US' }); // 'US$16,800'
28
+ */
29
+ function formatCurrency(amount, options = {}) {
30
+ const { sign = '$', ...numberOptions } = options;
31
+ return `${sign}${formatNumber(amount, numberOptions)}`;
32
+ }
33
+ /**
34
+ * Inverso de `formatNumber` — recupera el número crudo de un valor ya
35
+ * formateado con el mismo `locale`. Solo soporta `locale` cuyo separador de
36
+ * miles y decimal sean `'.'`/`','` (`es`) o `','`/`'.'` (`en-US`) — son los
37
+ * 2 casos reales del ecosistema hoy; si aparece un tercer locale con otros
38
+ * separadores, extender acá antes de usarlo.
39
+ */
40
+ function parseNumber(formatted, locale = 'es') {
41
+ const [thousands, decimal] = locale === 'en-US' ? [',', '.'] : ['.', ','];
42
+ const withoutThousands = formatted.split(thousands).join('');
43
+ const withDecimalPoint = withoutThousands.replace(decimal, '.');
44
+ return Number(withDecimalPoint.replace(/[^0-9.-]/g, ''));
45
+ }
46
+ /**
47
+ * Inverso de `formatCurrency` — recupera el número crudo de un monto ya
48
+ * formateado, descartando cualquier signo/prefijo de moneda.
49
+ *
50
+ * parseCurrency('$16.800'); // 16800
51
+ */
52
+ function parseCurrency(formatted, locale = 'es') {
53
+ return parseNumber(formatted, locale);
54
+ }
55
+ const CLOCK_FORMAT = new Intl.DateTimeFormat('es-CL', {
56
+ hour: '2-digit',
57
+ minute: '2-digit',
58
+ hourCycle: 'h23',
59
+ });
60
+ /** "12:40" — hora en formato de 24 horas. */
61
+ function formatClockTime(date) {
62
+ return CLOCK_FORMAT.format(date);
63
+ }
64
+ /**
65
+ * Quita espacios (incluido cualquier espacio en blanco intermedio, no solo
66
+ * al principio/final) y puntos de un identificador de documento — un
67
+ * espacio invisible (ej. pegado por accidente desde otra fuente) es un
68
+ * motivo de rechazo silencioso real que un usuario no sabe explicarse.
69
+ * Genérico: sirve tanto para RUT (`formatChileanRut` ya lo usa por
70
+ * dentro) como para cualquier otro identificador con el mismo problema.
71
+ *
72
+ * sanitizeDocumentId(' 51 00004-8 '); // '5100004-8'
73
+ * sanitizeDocumentId('12.345.678-9'); // '12345678-9'
74
+ */
75
+ function sanitizeDocumentId(raw) {
76
+ return raw.replace(/[\s.]/g, '');
77
+ }
78
+ /**
79
+ * Formatea un RUT chileno insertando el guion automáticamente a medida
80
+ * que se escribe — igual que un input de tarjeta de crédito, el usuario
81
+ * solo tipea dígitos (y opcionalmente `k`/`K` como dígito verificador) y
82
+ * el guion aparece solo antes del último carácter. Solo da forma al
83
+ * string — no valida si el dígito verificador es matemáticamente
84
+ * correcto (ver `isValidChileanRut` para eso) ni si la cuenta existe
85
+ * (eso lo confirma GAM del lado servidor al autenticar).
86
+ *
87
+ * formatChileanRut('123456789'); // '12345678-9'
88
+ * formatChileanRut('5100004-8'); // '5100004-8' (ya formateado, idempotente)
89
+ * formatChileanRut('5100004k'); // '5100004-k'
90
+ * formatChileanRut('510000 4-8 '); // '5100004-8' (espacios ignorados)
91
+ */
92
+ function formatChileanRut(raw) {
93
+ const clean = sanitizeDocumentId(raw).replace(/-/g, '').toUpperCase();
94
+ if (clean.length <= 1) {
95
+ return clean;
96
+ }
97
+ const body = clean.slice(0, -1).replace(/[^0-9]/g, '');
98
+ const checkDigit = clean.slice(-1);
99
+ return `${body}-${checkDigit}`;
100
+ }
101
+ /**
102
+ * Calcula el dígito verificador real de un RUT chileno con el
103
+ * algoritmo estándar módulo 11 — devuelve `'0'`-`'9'` o `'K'`.
104
+ * `body` debe ser solo dígitos, sin guion ni dígito verificador (ej.
105
+ * `'20236276'`, no `'20236276-1'`).
106
+ */
107
+ function computeChileanRutCheckDigit(body) {
108
+ let sum = 0;
109
+ let multiplier = 2;
110
+ for (let i = body.length - 1; i >= 0; i--) {
111
+ sum += Number(body[i]) * multiplier;
112
+ multiplier = multiplier < 7 ? multiplier + 1 : 2;
113
+ }
114
+ const remainder = 11 - (sum % 11);
115
+ if (remainder === 11)
116
+ return '0';
117
+ if (remainder === 10)
118
+ return 'K';
119
+ return String(remainder);
120
+ }
121
+ /**
122
+ * Valida un RUT chileno YA formateado (`cuerpo-DV`, ej. `'12345678-9'`)
123
+ * contra el algoritmo módulo 11 real — a diferencia del `pattern` de
124
+ * `document-id.config.ts` en cada portal consumidor (que solo exige la
125
+ * FORMA: dígitos + guion + dígito verificador), esta función confirma
126
+ * que el dígito verificador sea matemáticamente correcto para ese
127
+ * cuerpo. Sigue sin confirmar que la cuenta exista — eso solo lo sabe
128
+ * GAM al autenticar — pero atrapa el error tipográfico más común
129
+ * (invertir/cambiar un dígito) antes de intentar el login.
130
+ *
131
+ * isValidChileanRut('20236276-1'); // true (DV real de ese cuerpo)
132
+ * isValidChileanRut('20236276-8'); // false (DV real es '1', no '8')
133
+ * isValidChileanRut('no-es-rut'); // false (ni siquiera tiene la forma)
134
+ */
135
+ function isValidChileanRut(rut) {
136
+ const match = /^(\d{7,8})-([\dkK])$/.exec(sanitizeDocumentId(rut));
137
+ if (!match) {
138
+ return false;
139
+ }
140
+ const [, body, checkDigit] = match;
141
+ return computeChileanRutCheckDigit(body).toUpperCase() === checkDigit.toUpperCase();
142
+ }
143
+
144
+ /**
145
+ * Signal reactivo con el estado actual de un media query — para cuando un
146
+ * componente necesita SABER en qué lado de un breakpoint está (no solo
147
+ * mostrar/ocultar con CSS). Un `@media` en `styles` no alcanza cuando la
148
+ * decisión no es puramente visual: por ejemplo, un componente hijo que
149
+ * recibe un input booleano (`[collapsed]`) no puede "leer" un breakpoint
150
+ * CSS del padre — necesita que alguien en TypeScript se lo diga.
151
+ *
152
+ * Encontrado al construir `SidebarShell`: el estado `collapsed` (colapso
153
+ * del rail en desktop) se pasaba tal cual a cada `kf-sidebar-nav-item`, sin
154
+ * importar el viewport. En mobile, donde el rail deslizado siempre muestra
155
+ * etiquetas completas (ver estilos de `SidebarShell`), los ítems seguían
156
+ * ocultando su label si el usuario había dejado el rail colapsado en
157
+ * desktop antes de achicar la ventana — un CSS que dice "en mobile se ve
158
+ * ancho" no cambia el valor del input que ya llegó al hijo.
159
+ *
160
+ * Debe llamarse dentro de un contexto de inyección (el `constructor` de un
161
+ * componente, o un campo de clase inicializado ahí — mismo requisito que
162
+ * cualquier función que use `inject()`).
163
+ *
164
+ * protected readonly isMobile = matchMediaSignal('(max-width: 900px)');
165
+ * // ...
166
+ * [collapsed]="collapsed() && !isMobile()"
167
+ */
168
+ function matchMediaSignal(query) {
169
+ const mediaQueryList = matchMedia(query);
170
+ const state = signal(mediaQueryList.matches, /* @ts-ignore */
171
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
172
+ const onChange = (event) => state.set(event.matches);
173
+ mediaQueryList.addEventListener('change', onChange);
174
+ inject(DestroyRef).onDestroy(() => mediaQueryList.removeEventListener('change', onChange));
175
+ return state.asReadonly();
176
+ }
177
+
178
+ /**
179
+ * Navegación estándar de ARIA tabs/listbox/radiogroup: flechas mueven el
180
+ * foco entre opciones y activan la seleccionada. Usado por `kf-tabs`,
181
+ * `kf-folder-tabs` (orientación horizontal) y `kf-selectable-card-group`
182
+ * (vertical) — todos comparten la misma forma (lista de opciones con
183
+ * `value`, botones identificados por `idPrefix + value`).
184
+ *
185
+ * `orientation` decide qué par de flechas avanza/retrocede — mismo patrón
186
+ * que exige el WAI-ARIA Authoring Practices para radiogroup vertical
187
+ * (ArrowDown/ArrowUp) vs. tablist horizontal (ArrowRight/ArrowLeft). Home/End
188
+ * funcionan igual en ambas orientaciones.
189
+ *
190
+ * `select` es responsabilidad del consumidor: esta función solo calcula
191
+ * cuál es la siguiente opción y mueve el foco al botón correspondiente.
192
+ */
193
+ function handleArrowKeyNavigation(event, options, currentValue, idPrefix, select, orientation = 'horizontal') {
194
+ const currentIndex = options.findIndex((option) => option.value === currentValue);
195
+ const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
196
+ const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
197
+ let nextIndex;
198
+ switch (event.key) {
199
+ case nextKey:
200
+ nextIndex = (currentIndex + 1) % options.length;
201
+ break;
202
+ case prevKey:
203
+ nextIndex = (currentIndex - 1 + options.length) % options.length;
204
+ break;
205
+ case 'Home':
206
+ nextIndex = 0;
207
+ break;
208
+ case 'End':
209
+ nextIndex = options.length - 1;
210
+ break;
211
+ default:
212
+ return;
213
+ }
214
+ event.preventDefault();
215
+ const nextOption = options[nextIndex];
216
+ select(nextOption.value);
217
+ const host = event.currentTarget.parentElement;
218
+ const nextId = `${idPrefix}${nextOption.value}`;
219
+ const nextButton = Array.from(host?.querySelectorAll('button') ?? []).find((button) => button.id === nextId);
220
+ nextButton?.focus();
221
+ }
222
+
223
+ /**
224
+ * Normaliza texto para comparación de búsqueda insensible a mayúsculas y
225
+ * diacríticos — así "cor" encuentre "Córdoba" sin necesidad de tildes.
226
+ *
227
+ * Compartida entre `Select` y `MultiSelect` (`/core`, los dos componentes con
228
+ * buscador interno auto-activado por `searchThreshold`) y `QuotePicker`
229
+ * (`/business`).
230
+ */
231
+ function normalizeForSearch(text) {
232
+ return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
233
+ }
234
+
235
+ /*
236
+ * Public API Surface of ui/shared — entry point secundario, `@kaufmann-lab/ui/shared`.
237
+ *
238
+ * `format`/`media-query`/`arrow-key-navigation`/`search-text`: helpers sin UI
239
+ * propia (ningún `Component`/`Directive` con template — puro TS
240
+ * framework-agnostic salvo `media-query`, que solo usa
241
+ * `DestroyRef`/`Signal`/`inject`/`signal` de `@angular/core`, sin depender de
242
+ * ningún componente del catálogo).
243
+ *
244
+ * Este entry point no importa nada de `@kaufmann-lab/ui/core` ni de ningún
245
+ * otro entry point de la librería: es la hoja del grafo de dependencias. Eso
246
+ * permite que cualquier otro (`/core`, `/business`, `/mobile`, `/map`) lo
247
+ * consuma vía `'@kaufmann-lab/ui/shared'`, y que la raíz `'@kaufmann-lab/ui'`
248
+ * lo reexporte, sin crear una dependencia circular. ng-packagr rechaza en
249
+ * build ("Entry point X has a circular dependency on Y") cualquier par de
250
+ * entry points que se necesiten mutuamente — un helper nuevo que dependa de
251
+ * un componente de `/core` no puede vivir acá.
252
+ *
253
+ * Los archivos fuente viven acá (`./lib/shared/`), no reexportados desde otro
254
+ * directorio: ng-packagr calcula un `rootDir` propio por entry point (el
255
+ * directorio del `entryFile`) y rechaza cualquier archivo fuente importado
256
+ * desde afuera de ese `rootDir` (`TS6059`).
257
+ *
258
+ * Los archivos de `lib/shared/` que viven en `@kaufmann-lab/ui/core`
259
+ * (`select-overlay.ts`, `format-input.directive.ts`, y los `.css` sueltos de
260
+ * esa carpeta) no están en este entry point: son UI/overlay y una directiva,
261
+ * no helpers puros.
262
+ */
263
+
264
+ /**
265
+ * Generated bundle index. Do not edit.
266
+ */
267
+
268
+ export { formatChileanRut, formatClockTime, formatCurrency, formatNumber, handleArrowKeyNavigation, isValidChileanRut, matchMediaSignal, normalizeForSearch, parseCurrency, parseNumber, sanitizeDocumentId };
269
+ //# sourceMappingURL=kaufmann-lab-ui-shared.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kaufmann-lab-ui-shared.mjs","sources":["../../../projects/ui/shared/src/lib/shared/format.ts","../../../projects/ui/shared/src/lib/shared/media-query.ts","../../../projects/ui/shared/src/lib/shared/arrow-key-navigation.ts","../../../projects/ui/shared/src/lib/shared/search-text.ts","../../../projects/ui/shared/src/public-api.ts","../../../projects/ui/shared/src/kaufmann-lab-ui-shared.ts"],"sourcesContent":["/** Opciones de `formatNumber` — todas opcionales. */\nexport interface FormatNumberOptions {\n /** Cantidad de decimales a mostrar. Default `0`. */\n decimals?: number;\n /** Locale de `Intl`/`toLocaleString` — decide separador de miles y de\n * decimales. Default `'es'` (punto de miles, coma decimal). */\n locale?: string;\n}\n\n/**\n * Separador de miles genérico — sin signo, sin asumir que el valor es\n * dinero. Cubre casos como kilometraje/horómetro (`formatNumber(45231,\n * { locale: 'en-US' })` → `'45,231'`), no solo moneda.\n *\n * formatNumber(16800); // '16.800'\n * formatNumber(16800.5, { decimals: 2 }); // '16.800,50'\n * formatNumber(45231, { locale: 'en-US' }); // '45,231'\n */\nexport function formatNumber(value: number, options: FormatNumberOptions = {}): string {\n const { decimals = 0, locale = 'es' } = options;\n return value.toLocaleString(locale, {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n });\n}\n\n/** Opciones de `formatCurrency` — extiende `FormatNumberOptions` con el\n * signo/prefijo de moneda. */\nexport interface FormatCurrencyOptions extends FormatNumberOptions {\n /** Símbolo o prefijo antepuesto al monto. Default `'$'`. */\n sign?: string;\n}\n\n/**\n * Formatea un monto de dinero — compone sobre `formatNumber` agregando el\n * signo. Mismos defaults que la convención de OTD (punto de miles, sin\n * decimales, prefijo `$`), overrideables para otra moneda/país/formato:\n *\n * formatCurrency(16800); // '$16.800'\n * formatCurrency(16800.5, { decimals: 2 }); // '$16.800,50'\n * formatCurrency(16800, { sign: '' }); // '16.800'\n * formatCurrency(16800, { sign: 'US$', locale: 'en-US' }); // 'US$16,800'\n */\nexport function formatCurrency(amount: number, options: FormatCurrencyOptions = {}): string {\n const { sign = '$', ...numberOptions } = options;\n return `${sign}${formatNumber(amount, numberOptions)}`;\n}\n\n/**\n * Inverso de `formatNumber` — recupera el número crudo de un valor ya\n * formateado con el mismo `locale`. Solo soporta `locale` cuyo separador de\n * miles y decimal sean `'.'`/`','` (`es`) o `','`/`'.'` (`en-US`) — son los\n * 2 casos reales del ecosistema hoy; si aparece un tercer locale con otros\n * separadores, extender acá antes de usarlo.\n */\nexport function parseNumber(formatted: string, locale = 'es'): number {\n const [thousands, decimal] = locale === 'en-US' ? [',', '.'] : ['.', ','];\n const withoutThousands = formatted.split(thousands).join('');\n const withDecimalPoint = withoutThousands.replace(decimal, '.');\n return Number(withDecimalPoint.replace(/[^0-9.-]/g, ''));\n}\n\n/**\n * Inverso de `formatCurrency` — recupera el número crudo de un monto ya\n * formateado, descartando cualquier signo/prefijo de moneda.\n *\n * parseCurrency('$16.800'); // 16800\n */\nexport function parseCurrency(formatted: string, locale = 'es'): number {\n return parseNumber(formatted, locale);\n}\n\nconst CLOCK_FORMAT = new Intl.DateTimeFormat('es-CL', {\n hour: '2-digit',\n minute: '2-digit',\n hourCycle: 'h23',\n});\n\n/** \"12:40\" — hora en formato de 24 horas. */\nexport function formatClockTime(date: Date): string {\n return CLOCK_FORMAT.format(date);\n}\n\n/**\n * Quita espacios (incluido cualquier espacio en blanco intermedio, no solo\n * al principio/final) y puntos de un identificador de documento — un\n * espacio invisible (ej. pegado por accidente desde otra fuente) es un\n * motivo de rechazo silencioso real que un usuario no sabe explicarse.\n * Genérico: sirve tanto para RUT (`formatChileanRut` ya lo usa por\n * dentro) como para cualquier otro identificador con el mismo problema.\n *\n * sanitizeDocumentId(' 51 00004-8 '); // '5100004-8'\n * sanitizeDocumentId('12.345.678-9'); // '12345678-9'\n */\nexport function sanitizeDocumentId(raw: string): string {\n return raw.replace(/[\\s.]/g, '');\n}\n\n/**\n * Formatea un RUT chileno insertando el guion automáticamente a medida\n * que se escribe — igual que un input de tarjeta de crédito, el usuario\n * solo tipea dígitos (y opcionalmente `k`/`K` como dígito verificador) y\n * el guion aparece solo antes del último carácter. Solo da forma al\n * string — no valida si el dígito verificador es matemáticamente\n * correcto (ver `isValidChileanRut` para eso) ni si la cuenta existe\n * (eso lo confirma GAM del lado servidor al autenticar).\n *\n * formatChileanRut('123456789'); // '12345678-9'\n * formatChileanRut('5100004-8'); // '5100004-8' (ya formateado, idempotente)\n * formatChileanRut('5100004k'); // '5100004-k'\n * formatChileanRut('510000 4-8 '); // '5100004-8' (espacios ignorados)\n */\nexport function formatChileanRut(raw: string): string {\n const clean = sanitizeDocumentId(raw).replace(/-/g, '').toUpperCase();\n if (clean.length <= 1) {\n return clean;\n }\n const body = clean.slice(0, -1).replace(/[^0-9]/g, '');\n const checkDigit = clean.slice(-1);\n return `${body}-${checkDigit}`;\n}\n\n/**\n * Calcula el dígito verificador real de un RUT chileno con el\n * algoritmo estándar módulo 11 — devuelve `'0'`-`'9'` o `'K'`.\n * `body` debe ser solo dígitos, sin guion ni dígito verificador (ej.\n * `'20236276'`, no `'20236276-1'`).\n */\nfunction computeChileanRutCheckDigit(body: string): string {\n let sum = 0;\n let multiplier = 2;\n for (let i = body.length - 1; i >= 0; i--) {\n sum += Number(body[i]) * multiplier;\n multiplier = multiplier < 7 ? multiplier + 1 : 2;\n }\n const remainder = 11 - (sum % 11);\n if (remainder === 11) return '0';\n if (remainder === 10) return 'K';\n return String(remainder);\n}\n\n/**\n * Valida un RUT chileno YA formateado (`cuerpo-DV`, ej. `'12345678-9'`)\n * contra el algoritmo módulo 11 real — a diferencia del `pattern` de\n * `document-id.config.ts` en cada portal consumidor (que solo exige la\n * FORMA: dígitos + guion + dígito verificador), esta función confirma\n * que el dígito verificador sea matemáticamente correcto para ese\n * cuerpo. Sigue sin confirmar que la cuenta exista — eso solo lo sabe\n * GAM al autenticar — pero atrapa el error tipográfico más común\n * (invertir/cambiar un dígito) antes de intentar el login.\n *\n * isValidChileanRut('20236276-1'); // true (DV real de ese cuerpo)\n * isValidChileanRut('20236276-8'); // false (DV real es '1', no '8')\n * isValidChileanRut('no-es-rut'); // false (ni siquiera tiene la forma)\n */\nexport function isValidChileanRut(rut: string): boolean {\n const match = /^(\\d{7,8})-([\\dkK])$/.exec(sanitizeDocumentId(rut));\n if (!match) {\n return false;\n }\n const [, body, checkDigit] = match;\n return computeChileanRutCheckDigit(body).toUpperCase() === checkDigit.toUpperCase();\n}\n","import { DestroyRef, Signal, inject, signal } from '@angular/core';\n\n/**\n * Signal reactivo con el estado actual de un media query — para cuando un\n * componente necesita SABER en qué lado de un breakpoint está (no solo\n * mostrar/ocultar con CSS). Un `@media` en `styles` no alcanza cuando la\n * decisión no es puramente visual: por ejemplo, un componente hijo que\n * recibe un input booleano (`[collapsed]`) no puede \"leer\" un breakpoint\n * CSS del padre — necesita que alguien en TypeScript se lo diga.\n *\n * Encontrado al construir `SidebarShell`: el estado `collapsed` (colapso\n * del rail en desktop) se pasaba tal cual a cada `kf-sidebar-nav-item`, sin\n * importar el viewport. En mobile, donde el rail deslizado siempre muestra\n * etiquetas completas (ver estilos de `SidebarShell`), los ítems seguían\n * ocultando su label si el usuario había dejado el rail colapsado en\n * desktop antes de achicar la ventana — un CSS que dice \"en mobile se ve\n * ancho\" no cambia el valor del input que ya llegó al hijo.\n *\n * Debe llamarse dentro de un contexto de inyección (el `constructor` de un\n * componente, o un campo de clase inicializado ahí — mismo requisito que\n * cualquier función que use `inject()`).\n *\n * protected readonly isMobile = matchMediaSignal('(max-width: 900px)');\n * // ...\n * [collapsed]=\"collapsed() && !isMobile()\"\n */\nexport function matchMediaSignal(query: string): Signal<boolean> {\n const mediaQueryList = matchMedia(query);\n const state = signal(mediaQueryList.matches);\n\n const onChange = (event: MediaQueryListEvent) => state.set(event.matches);\n mediaQueryList.addEventListener('change', onChange);\n\n inject(DestroyRef).onDestroy(() => mediaQueryList.removeEventListener('change', onChange));\n\n return state.asReadonly();\n}\n","/**\n * Navegación estándar de ARIA tabs/listbox/radiogroup: flechas mueven el\n * foco entre opciones y activan la seleccionada. Usado por `kf-tabs`,\n * `kf-folder-tabs` (orientación horizontal) y `kf-selectable-card-group`\n * (vertical) — todos comparten la misma forma (lista de opciones con\n * `value`, botones identificados por `idPrefix + value`).\n *\n * `orientation` decide qué par de flechas avanza/retrocede — mismo patrón\n * que exige el WAI-ARIA Authoring Practices para radiogroup vertical\n * (ArrowDown/ArrowUp) vs. tablist horizontal (ArrowRight/ArrowLeft). Home/End\n * funcionan igual en ambas orientaciones.\n *\n * `select` es responsabilidad del consumidor: esta función solo calcula\n * cuál es la siguiente opción y mueve el foco al botón correspondiente.\n */\nexport function handleArrowKeyNavigation<T extends { value: string }>(\n event: KeyboardEvent,\n options: T[],\n currentValue: string,\n idPrefix: string,\n select: (value: string) => void,\n orientation: 'horizontal' | 'vertical' = 'horizontal',\n): void {\n const currentIndex = options.findIndex((option) => option.value === currentValue);\n const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';\n const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';\n\n let nextIndex: number;\n switch (event.key) {\n case nextKey:\n nextIndex = (currentIndex + 1) % options.length;\n break;\n case prevKey:\n nextIndex = (currentIndex - 1 + options.length) % options.length;\n break;\n case 'Home':\n nextIndex = 0;\n break;\n case 'End':\n nextIndex = options.length - 1;\n break;\n default:\n return;\n }\n\n event.preventDefault();\n const nextOption = options[nextIndex];\n select(nextOption.value);\n\n const host = (event.currentTarget as HTMLElement).parentElement;\n const nextId = `${idPrefix}${nextOption.value}`;\n const nextButton = Array.from(host?.querySelectorAll<HTMLButtonElement>('button') ?? []).find(\n (button) => button.id === nextId,\n );\n nextButton?.focus();\n}\n","/**\n * Normaliza texto para comparación de búsqueda insensible a mayúsculas y\n * diacríticos — así \"cor\" encuentre \"Córdoba\" sin necesidad de tildes.\n *\n * Compartida entre `Select` y `MultiSelect` (`/core`, los dos componentes con\n * buscador interno auto-activado por `searchThreshold`) y `QuotePicker`\n * (`/business`).\n */\nexport function normalizeForSearch(text: string): string {\n return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();\n}\n","/*\n * Public API Surface of ui/shared — entry point secundario, `@kaufmann-lab/ui/shared`.\n *\n * `format`/`media-query`/`arrow-key-navigation`/`search-text`: helpers sin UI\n * propia (ningún `Component`/`Directive` con template — puro TS\n * framework-agnostic salvo `media-query`, que solo usa\n * `DestroyRef`/`Signal`/`inject`/`signal` de `@angular/core`, sin depender de\n * ningún componente del catálogo).\n *\n * Este entry point no importa nada de `@kaufmann-lab/ui/core` ni de ningún\n * otro entry point de la librería: es la hoja del grafo de dependencias. Eso\n * permite que cualquier otro (`/core`, `/business`, `/mobile`, `/map`) lo\n * consuma vía `'@kaufmann-lab/ui/shared'`, y que la raíz `'@kaufmann-lab/ui'`\n * lo reexporte, sin crear una dependencia circular. ng-packagr rechaza en\n * build (\"Entry point X has a circular dependency on Y\") cualquier par de\n * entry points que se necesiten mutuamente — un helper nuevo que dependa de\n * un componente de `/core` no puede vivir acá.\n *\n * Los archivos fuente viven acá (`./lib/shared/`), no reexportados desde otro\n * directorio: ng-packagr calcula un `rootDir` propio por entry point (el\n * directorio del `entryFile`) y rechaza cualquier archivo fuente importado\n * desde afuera de ese `rootDir` (`TS6059`).\n *\n * Los archivos de `lib/shared/` que viven en `@kaufmann-lab/ui/core`\n * (`select-overlay.ts`, `format-input.directive.ts`, y los `.css` sueltos de\n * esa carpeta) no están en este entry point: son UI/overlay y una directiva,\n * no helpers puros.\n */\n\nexport * from './lib/shared/format';\nexport * from './lib/shared/media-query';\nexport * from './lib/shared/arrow-key-navigation';\nexport * from './lib/shared/search-text';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}