codexly-ui 0.21.0 → 0.21.1

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, ChangeDetectionStrategy, ViewEncapsulation, Component, InjectionToken, inject, PLATFORM_ID, signal, effect, Injectable, ElementRef, HostAttributeToken, output, NgZone, model, ViewChildren, ApplicationRef, Injector, createComponent, Directive, forwardRef, contentChild, EnvironmentInjector, RendererFactory2, DestroyRef, untracked, HostListener, viewChild, ViewChild, ChangeDetectorRef, contentChildren, ContentChildren, numberAttribute, booleanAttribute, Input } from '@angular/core';
2
+ import { input, computed, ChangeDetectionStrategy, ViewEncapsulation, Component, InjectionToken, signal, inject, PLATFORM_ID, Injectable, effect, ApplicationRef, EnvironmentInjector, RendererFactory2, createComponent, ElementRef, DestroyRef, Directive, HostAttributeToken, output, NgZone, model, ViewChildren, Injector, forwardRef, contentChild, untracked, HostListener, viewChild, ViewChild, ChangeDetectorRef, contentChildren, ContentChildren, numberAttribute, booleanAttribute, Input } from '@angular/core';
3
3
  import { isPlatformBrowser, NgStyle, NgTemplateOutlet, DOCUMENT, NgOptimizedImage, CurrencyPipe, DecimalPipe } from '@angular/common';
4
4
  import { DomSanitizer } from '@angular/platform-browser';
5
5
  import * as i1$1 from '@angular/forms';
@@ -210,30 +210,204 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
210
210
  }]
211
211
  }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }] } });
212
212
 
213
- // ── Context ───────────────────────────────────────────────────────────────────
214
- /** Provided by clx-table-actions so nested clx-button instances default to the theme's
215
- * secondaryColor instead of primaryColor when they don't pass their own [color] — any button
216
- * that DOES pass [color] explicitly still overrides this, same as CLX_CARD_DISABLED_CONTEXT. */
217
- const CLX_TABLE_ACTIONS_CONTEXT = new InjectionToken('CLX_TABLE_ACTIONS_CONTEXT');
218
- const BUTTON_BASE_CLASS = 'relative overflow-hidden inline-flex items-center justify-center font-medium select-none transition-[color,background-color,border-color,box-shadow] duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2';
219
- const BUTTON_SIZE_MAP = {
220
- xxs: 'text-xs px-1.5 py-0.5 gap-0.5',
221
- xs: 'text-xs px-2 py-1 gap-1',
222
- sm: 'text-xs px-3 py-1.5 gap-1.5',
223
- md: 'text-sm px-4 py-2 gap-2',
224
- lg: 'text-base px-5 py-2.5 gap-2.5',
225
- };
226
- const BUTTON_SHAPE_FIXED = {
227
- pill: 'rounded-full',
228
- circle: 'rounded-full',
229
- };
230
- const BUTTON_ICON_ONLY_SIZE_MAP = {
231
- xxs: 'w-5 h-5 p-0 text-xs',
232
- xs: 'w-7 h-7 p-0 text-xs',
233
- sm: 'w-8 h-8 p-0 text-sm',
234
- md: 'w-10 h-10 p-0 text-sm',
235
- lg: 'w-11 h-11 p-0 text-sm',
213
+ const CLX_ANIMATE_DEFAULTS_VALUE = {
214
+ duration: 1000,
215
+ delay: 0,
216
+ repeat: 1,
217
+ easing: 'ease',
218
+ fillMode: 'both',
219
+ once: false,
220
+ threshold: 0.1,
236
221
  };
222
+ const CLX_ANIMATE_DEFAULTS = new InjectionToken('CLX_ANIMATE_DEFAULTS', {
223
+ providedIn: 'root',
224
+ factory: () => CLX_ANIMATE_DEFAULTS_VALUE,
225
+ });
226
+
227
+ const PREFIX = 'clx-anim--';
228
+ class ClxAnimateService {
229
+ platformId;
230
+ defaults;
231
+ isBrowser;
232
+ // ─── Estado de animaciones en curso ───────────────────────────────────────
233
+ _runningCount = signal(0, ...(ngDevMode ? [{ debugName: "_runningCount" }] : /* istanbul ignore next */ []));
234
+ isAnimating = computed(() => this._runningCount() > 0, ...(ngDevMode ? [{ debugName: "isAnimating" }] : /* istanbul ignore next */ []));
235
+ // ─── Cleanup por elemento (WeakMap preserva GC) ───────────────────────────
236
+ _cleanups = new WeakMap();
237
+ // ─── Pool de IntersectionObservers compartidos ────────────────────────────
238
+ _observerPools = new Map();
239
+ constructor() {
240
+ this.platformId = inject(PLATFORM_ID);
241
+ this.defaults = inject(CLX_ANIMATE_DEFAULTS);
242
+ this.isBrowser = isPlatformBrowser(this.platformId);
243
+ if (this.isBrowser)
244
+ this._warmup();
245
+ }
246
+ // ─── Warmup ───────────────────────────────────────────────────────────────
247
+ /**
248
+ * Fuerza al browser a parsear los keyframes antes de la primera interacción
249
+ * para evitar el jank del primer frame. Se ejecuta en idle para no bloquear
250
+ * el critical rendering path.
251
+ */
252
+ _warmup() {
253
+ const run = () => {
254
+ const el = document.createElement('div');
255
+ el.style.cssText = 'position:fixed;top:-9999px;pointer-events:none;opacity:0;';
256
+ el.classList.add(`${PREFIX}animated`, `${PREFIX}bounce`);
257
+ document.body.appendChild(el);
258
+ requestAnimationFrame(() => requestAnimationFrame(() => document.body.removeChild(el)));
259
+ };
260
+ if ('requestIdleCallback' in window) {
261
+ window
262
+ .requestIdleCallback(run, { timeout: 1000 });
263
+ }
264
+ else {
265
+ setTimeout(run, 200);
266
+ }
267
+ }
268
+ // ─── API pública ──────────────────────────────────────────────────────────
269
+ /**
270
+ * Anima cualquier HTMLElement de forma imperativa.
271
+ * Retorna una Promise que resuelve cuando la animación termina.
272
+ */
273
+ animate(element, animationOrConfig, overrides = {}) {
274
+ if (!this.isBrowser)
275
+ return Promise.resolve();
276
+ return this.runAnimation(element, this.buildConfig(animationOrConfig, overrides));
277
+ }
278
+ /** Cancela la animación en curso en el elemento y restaura su estado. */
279
+ stop(element) {
280
+ this._cleanups.get(element)?.();
281
+ }
282
+ /**
283
+ * Registra un elemento en el pool de IntersectionObservers.
284
+ * Reutiliza el observer existente para el threshold dado o crea uno nuevo.
285
+ * Retorna una función para desregistrar el elemento.
286
+ */
287
+ observe(element, threshold, cb) {
288
+ if (!this.isBrowser)
289
+ return () => { };
290
+ let pool = this._observerPools.get(threshold);
291
+ if (!pool) {
292
+ const registry = new Map();
293
+ const observer = new IntersectionObserver((entries) => {
294
+ for (const entry of entries) {
295
+ registry.get(entry.target)?.(entry.isIntersecting);
296
+ }
297
+ }, { threshold });
298
+ pool = { observer, registry };
299
+ this._observerPools.set(threshold, pool);
300
+ }
301
+ pool.registry.set(element, cb);
302
+ pool.observer.observe(element);
303
+ return () => {
304
+ const p = this._observerPools.get(threshold);
305
+ if (!p)
306
+ return;
307
+ p.observer.unobserve(element);
308
+ p.registry.delete(element);
309
+ if (p.registry.size === 0) {
310
+ p.observer.disconnect();
311
+ this._observerPools.delete(threshold);
312
+ }
313
+ };
314
+ }
315
+ // ─── Interno (usado por ClxAnimateDirective) ──────────────────────────────
316
+ runAnimation(element, config) {
317
+ // Solo hay clases previas que limpiar si el elemento ya tenía un cleanup
318
+ // registrado — en ese caso (restart de una animación en curso) sí hace
319
+ // falta forzar el reflow para que el browser registre la eliminación de
320
+ // clases antes de volver a agregarlas. En el caso común (primera
321
+ // animación del elemento, ej. un tooltip recién creado) stop() es un
322
+ // no-op y el reflow forzado es puro costo evitable.
323
+ const hadPreviousAnimation = this._cleanups.has(element);
324
+ this.stop(element);
325
+ if (hadPreviousAnimation)
326
+ void element.offsetHeight;
327
+ const animClass = `${PREFIX}${config.animation}`;
328
+ const isInfinite = config.repeat === 'infinite';
329
+ // A fillMode: 'forwards' animation (an exit fade/slide, e.g. clx-toast's playExitAnimation)
330
+ // is almost always followed by the caller destroying the element on the same tick its promise
331
+ // resolves — stripping animClass in _cleanup right then would drop animation-fill-mode's hold
332
+ // on the last keyframe, snapping the element back to its static CSS state (opaque, in place)
333
+ // for one paintable frame before removal, which read as a flash/"ghost" re-appearance. Classes
334
+ // are only stripped for a non-forwards run; a forwards run keeps them and instead resets
335
+ // them at the START of runAnimation's own reflow-forced restart path above, so a persisted
336
+ // element that's re-animated later (e.g. clx-animate-directive on hover) still resets cleanly.
337
+ const keepClassesOnEnd = config.fillMode === 'forwards';
338
+ element.style.setProperty('--clx-anim-duration', `${config.duration}ms`);
339
+ element.style.setProperty('--clx-anim-delay', `${config.delay}ms`);
340
+ element.style.setProperty('--clx-anim-repeat', isInfinite ? 'infinite' : String(config.repeat ?? 1));
341
+ element.style.setProperty('--clx-anim-easing', config.easing ?? 'ease');
342
+ if (config.fillMode) {
343
+ element.style.setProperty('animation-fill-mode', config.fillMode);
344
+ }
345
+ element.classList.add(`${PREFIX}animated`, animClass);
346
+ if (isInfinite)
347
+ element.classList.add(`${PREFIX}infinite`);
348
+ this._runningCount.update(c => c + 1);
349
+ if (isInfinite) {
350
+ this._cleanups.set(element, () => {
351
+ this._cleanup(element, animClass);
352
+ this._runningCount.update(c => Math.max(0, c - 1));
353
+ });
354
+ return Promise.resolve();
355
+ }
356
+ return new Promise(resolve => {
357
+ const handleEnd = (ev) => {
358
+ ev.stopPropagation();
359
+ if (keepClassesOnEnd) {
360
+ this._cleanups.delete(element);
361
+ }
362
+ else {
363
+ this._cleanup(element, animClass);
364
+ }
365
+ this._runningCount.update(c => Math.max(0, c - 1));
366
+ resolve();
367
+ };
368
+ element.addEventListener('animationend', handleEnd, { once: true });
369
+ this._cleanups.set(element, () => {
370
+ element.removeEventListener('animationend', handleEnd);
371
+ this._cleanup(element, animClass);
372
+ this._runningCount.update(c => Math.max(0, c - 1));
373
+ resolve();
374
+ });
375
+ });
376
+ }
377
+ buildConfig(animationOrConfig, overrides = {}) {
378
+ const base = typeof animationOrConfig === 'string'
379
+ ? { animation: animationOrConfig }
380
+ : animationOrConfig;
381
+ return {
382
+ duration: this.defaults.duration,
383
+ delay: this.defaults.delay,
384
+ repeat: this.defaults.repeat,
385
+ easing: this.defaults.easing,
386
+ fillMode: this.defaults.fillMode,
387
+ once: this.defaults.once,
388
+ threshold: this.defaults.threshold,
389
+ trigger: 'manual',
390
+ ...base,
391
+ ...overrides,
392
+ };
393
+ }
394
+ // ─── Privado ──────────────────────────────────────────────────────────────
395
+ _cleanup(element, animClass) {
396
+ element.classList.remove(`${PREFIX}animated`, animClass, `${PREFIX}infinite`);
397
+ element.style.removeProperty('--clx-anim-duration');
398
+ element.style.removeProperty('--clx-anim-delay');
399
+ element.style.removeProperty('--clx-anim-repeat');
400
+ element.style.removeProperty('--clx-anim-easing');
401
+ element.style.removeProperty('animation-fill-mode');
402
+ this._cleanups.delete(element);
403
+ }
404
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxAnimateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
405
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxAnimateService, providedIn: 'root' });
406
+ }
407
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxAnimateService, decorators: [{
408
+ type: Injectable,
409
+ args: [{ providedIn: 'root' }]
410
+ }], ctorParameters: () => [] });
237
411
 
238
412
  const CLX_FONT_CATALOG = [
239
413
  {
@@ -799,43 +973,368 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
799
973
  args: [{ providedIn: 'root' }]
800
974
  }], ctorParameters: () => [] });
801
975
 
802
- const BADGE_SIZE_MAP = {
803
- xs: 'text-[10px] px-1.5 py-0.5',
976
+ const TOOLTIP_SIZE_MAP = {
804
977
  sm: 'text-xs px-2 py-1',
805
- md: 'text-xs px-2.5 py-1 font-medium',
806
- };
807
- const BADGE_CIRCLE_SIZE_MAP = {
808
- xs: 'text-[10px] w-4 h-4',
809
- sm: 'text-xs w-5 h-5',
810
- md: 'text-xs w-6 h-6 font-medium',
811
- };
812
- const BADGE_SHAPE_FIXED = {
813
- pill: 'rounded-full',
814
- circle: 'rounded-full',
815
- };
816
-
817
- // ── Context ───────────────────────────────────────────────────────────────────
818
- /** Provided by clx-card so nested form controls/buttons can inherit its disabled state. */
819
- const CLX_CARD_DISABLED_CONTEXT = new InjectionToken('CLX_CARD_DISABLED_CONTEXT');
820
- const CARD_PADDING_MAP = {
821
- none: '',
822
- sm: 'p-2',
823
- md: 'p-3',
824
- lg: 'p-5',
825
- };
826
- const CARD_SHADOW_MAP = {
827
- none: 'shadow-none',
828
- sm: 'shadow-sm',
829
- md: 'shadow-md',
830
- lg: 'shadow-lg',
831
- xl: 'shadow-xl',
832
- '2xl': 'shadow-2xl',
978
+ md: 'text-sm px-3 py-1.5 max-w-[220px]',
833
979
  };
834
980
 
835
- const SPINNER_SIZE_MAP = {
836
- xxs: 'w-3 h-3 border-2',
837
- xs: 'w-4 h-4 border-2',
838
- sm: 'w-5 h-5 border-2',
981
+ class ClxTooltipComponent {
982
+ _themeSvc = inject(ClxThemeService);
983
+ text = input.required(...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
984
+ position = input('top', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
985
+ color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
986
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
987
+ _color = computed(() => this.color() ?? this._themeSvc.config().neutralColor, ...(ngDevMode ? [{ debugName: "_color" }] : /* istanbul ignore next */ []));
988
+ _hostClass = computed(() => {
989
+ const t = resolveColor(this._color());
990
+ return `${t.text} rounded-lg shadow-xl ${t.bg} ${TOOLTIP_SIZE_MAP[this.size()]}`;
991
+ }, ...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
992
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
993
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.15", type: ClxTooltipComponent, isStandalone: true, selector: "clx-tooltip", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tooltip" }, properties: { "class": "_hostClass()" } }, ngImport: i0, template: `<span class="whitespace-pre-wrap leading-snug">{{ text() }}</span>`, isInline: true, styles: ["clx-tooltip{position:absolute;display:inline-flex;align-items:center;justify-content:center;pointer-events:none;will-change:opacity,transform}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
994
+ }
995
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipComponent, decorators: [{
996
+ type: Component,
997
+ args: [{ selector: 'clx-tooltip', standalone: true, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: {
998
+ 'role': 'tooltip',
999
+ '[class]': '_hostClass()',
1000
+ }, template: `<span class="whitespace-pre-wrap leading-snug">{{ text() }}</span>`, styles: ["clx-tooltip{position:absolute;display:inline-flex;align-items:center;justify-content:center;pointer-events:none;will-change:opacity,transform}\n"] }]
1001
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
1002
+
1003
+ // ── Gap between trigger and bubble (px) ───────────────────────────────────
1004
+ const GAP = 8;
1005
+ /**
1006
+ * Shared, app-wide tooltip bubble — a single ClxTooltipComponent instance reused by every
1007
+ * [clxTooltip] directive instead of each one creating/destroying its own on every hover.
1008
+ * A table with 75 tooltipped cells used to mean up to 75 independent createComponent/destroy
1009
+ * cycles as the mouse scanned across it; now there's exactly one bubble, repositioned and
1010
+ * relabeled per show(). Ownership is tracked by trigger element so a directive can only hide
1011
+ * the bubble it itself showed (prevents a stale hide() from a slower hover-out racing a newer
1012
+ * hover-in on a different element).
1013
+ */
1014
+ class ClxTooltipService {
1015
+ _appRef = inject(ApplicationRef);
1016
+ _injector = inject(EnvironmentInjector);
1017
+ _animate = inject(ClxAnimateService);
1018
+ _platformId = inject(PLATFORM_ID);
1019
+ _renderer;
1020
+ _isBrowser;
1021
+ _ref = null;
1022
+ _el = null;
1023
+ _owner = null;
1024
+ _hiding = false;
1025
+ constructor() {
1026
+ this._isBrowser = isPlatformBrowser(this._platformId);
1027
+ this._renderer = inject(RendererFactory2).createRenderer(null, null);
1028
+ }
1029
+ /** Shows (or relabels/repositions, if already visible) the shared bubble for `trigger`. */
1030
+ show(trigger, config) {
1031
+ if (!this._isBrowser || !config.text)
1032
+ return;
1033
+ this._owner = trigger;
1034
+ this._hiding = false;
1035
+ const ref = this._ref ?? this._create();
1036
+ ref.setInput('text', config.text);
1037
+ ref.setInput('color', config.color);
1038
+ ref.setInput('size', config.size);
1039
+ ref.setInput('position', config.position);
1040
+ const el = ref.location.nativeElement;
1041
+ this._animate.stop(el);
1042
+ el.style.visibility = 'hidden';
1043
+ el.style.opacity = '';
1044
+ ref.changeDetectorRef.detectChanges();
1045
+ // Single measurement pass — see clx-tooltip.directive.ts's original comment: read both
1046
+ // rects once, resolve the flipped position and its coordinates from that same read, then
1047
+ // apply everything in one write batch instead of interleaving reads/writes.
1048
+ const triggerRect = trigger.getBoundingClientRect();
1049
+ const bubbleRect = el.getBoundingClientRect();
1050
+ const finalPos = this._resolvePosition(config.position, triggerRect, bubbleRect);
1051
+ if (finalPos !== config.position) {
1052
+ ref.setInput('position', finalPos);
1053
+ ref.changeDetectorRef.detectChanges();
1054
+ }
1055
+ this._placeAt(el, triggerRect, bubbleRect, finalPos);
1056
+ el.style.visibility = '';
1057
+ trigger.setAttribute('aria-describedby', this._tooltipId);
1058
+ this._animate.animate(el, { animation: 'fadeIn', duration: 160, trigger: 'manual', fillMode: 'both' });
1059
+ }
1060
+ /** Hides the bubble, but only if `trigger` is the element that currently owns it — a hover-out
1061
+ * from an element that already lost ownership (a newer hover-in interrupted it) is a no-op.
1062
+ * Relies on the 'animationend' DOM event to know when the fade-out finished — but that event
1063
+ * can simply never fire (backgrounded tab throttling animations, layout thrashing from a route
1064
+ * change interrupting the transition, etc.), which would leave the promise unresolved forever
1065
+ * and _owner never cleared — since this bubble lives outside any component's lifecycle (a
1066
+ * singleton appended straight to document.body), nothing else would ever hide it again. A hard
1067
+ * fallback timeout guarantees the bubble is actually hidden and ownership released regardless of
1068
+ * whether the CSS event cooperates. */
1069
+ hide(trigger) {
1070
+ if (!this._isBrowser || this._owner !== trigger || !this._ref)
1071
+ return;
1072
+ this._hiding = true;
1073
+ trigger.removeAttribute('aria-describedby');
1074
+ const el = this._ref.location.nativeElement;
1075
+ const settle = () => {
1076
+ if (this._owner !== trigger || !this._hiding)
1077
+ return;
1078
+ el.style.visibility = 'hidden';
1079
+ this._owner = null;
1080
+ this._hiding = false;
1081
+ };
1082
+ const fallback = setTimeout(settle, 400);
1083
+ this._animate.animate(el, { animation: 'fadeOut', duration: 120, trigger: 'manual', fillMode: 'forwards' })
1084
+ .then(() => {
1085
+ clearTimeout(fallback);
1086
+ settle();
1087
+ });
1088
+ }
1089
+ /** Synchronous, no fade — used on touchend/click so the bubble can't linger over UI the
1090
+ * gesture opens (e.g. a modal). Only acts if `trigger` still owns the bubble. */
1091
+ hideImmediate(trigger) {
1092
+ if (!this._isBrowser || this._owner !== trigger || !this._ref)
1093
+ return;
1094
+ trigger.removeAttribute('aria-describedby');
1095
+ const el = this._ref.location.nativeElement;
1096
+ this._animate.stop(el);
1097
+ el.style.visibility = 'hidden';
1098
+ this._owner = null;
1099
+ this._hiding = false;
1100
+ }
1101
+ get _tooltipId() {
1102
+ return 'clx-tooltip-shared';
1103
+ }
1104
+ _create() {
1105
+ const ref = createComponent(ClxTooltipComponent, { environmentInjector: this._injector });
1106
+ this._appRef.attachView(ref.hostView);
1107
+ const el = ref.location.nativeElement;
1108
+ el.style.position = 'fixed';
1109
+ el.style.visibility = 'hidden';
1110
+ el.style.zIndex = '9999';
1111
+ el.id = this._tooltipId;
1112
+ this._renderer.appendChild(document.body, el);
1113
+ this._ref = ref;
1114
+ return ref;
1115
+ }
1116
+ // ── Positioning ──────────────────────────────────────────────────────────
1117
+ _resolvePosition(requested, trigger, bubble) {
1118
+ const vw = window.innerWidth;
1119
+ const vh = window.innerHeight;
1120
+ const fits = {
1121
+ top: trigger.top - bubble.height - GAP >= 0,
1122
+ bottom: trigger.bottom + bubble.height + GAP <= vh,
1123
+ left: trigger.left - bubble.width - GAP >= 0,
1124
+ right: trigger.right + bubble.width + GAP <= vw,
1125
+ };
1126
+ if (fits[requested])
1127
+ return requested;
1128
+ const opposite = {
1129
+ top: 'bottom', bottom: 'top', left: 'right', right: 'left',
1130
+ };
1131
+ const opp = opposite[requested];
1132
+ if (fits[opp])
1133
+ return opp;
1134
+ for (const pos of ['top', 'bottom', 'left', 'right']) {
1135
+ if (fits[pos])
1136
+ return pos;
1137
+ }
1138
+ return requested; // last resort
1139
+ }
1140
+ _placeAt(el, trigger, bubble, pos) {
1141
+ let top = 0;
1142
+ let left = 0;
1143
+ switch (pos) {
1144
+ case 'top':
1145
+ top = trigger.top - bubble.height - GAP;
1146
+ left = trigger.left + trigger.width / 2 - bubble.width / 2;
1147
+ break;
1148
+ case 'bottom':
1149
+ top = trigger.bottom + GAP;
1150
+ left = trigger.left + trigger.width / 2 - bubble.width / 2;
1151
+ break;
1152
+ case 'left':
1153
+ top = trigger.top + trigger.height / 2 - bubble.height / 2;
1154
+ left = trigger.left - bubble.width - GAP;
1155
+ break;
1156
+ case 'right':
1157
+ top = trigger.top + trigger.height / 2 - bubble.height / 2;
1158
+ left = trigger.right + GAP;
1159
+ break;
1160
+ }
1161
+ const margin = 4;
1162
+ left = Math.max(margin, Math.min(left, window.innerWidth - bubble.width - margin));
1163
+ top = Math.max(margin, Math.min(top, window.innerHeight - bubble.height - margin));
1164
+ el.style.top = `${top}px`;
1165
+ el.style.left = `${left}px`;
1166
+ }
1167
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1168
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, providedIn: 'root' });
1169
+ }
1170
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, decorators: [{
1171
+ type: Injectable,
1172
+ args: [{ providedIn: 'root' }]
1173
+ }], ctorParameters: () => [] });
1174
+
1175
+ class ClxTooltipDirective {
1176
+ // ── Inputs ───────────────────────────────────────────────────────────────
1177
+ clxTooltip = input.required(...(ngDevMode ? [{ debugName: "clxTooltip" }] : /* istanbul ignore next */ []));
1178
+ clxTooltipPosition = input('top', ...(ngDevMode ? [{ debugName: "clxTooltipPosition" }] : /* istanbul ignore next */ []));
1179
+ clxTooltipColor = input('slate', ...(ngDevMode ? [{ debugName: "clxTooltipColor" }] : /* istanbul ignore next */ []));
1180
+ clxTooltipSize = input('md', ...(ngDevMode ? [{ debugName: "clxTooltipSize" }] : /* istanbul ignore next */ []));
1181
+ clxTooltipDelay = input(0, ...(ngDevMode ? [{ debugName: "clxTooltipDelay" }] : /* istanbul ignore next */ []));
1182
+ // ── DI ───────────────────────────────────────────────────────────────────
1183
+ _el = inject(ElementRef);
1184
+ _tooltip = inject(ClxTooltipService);
1185
+ _platformId = inject(PLATFORM_ID);
1186
+ _destroyRef = inject(DestroyRef);
1187
+ // ── State ────────────────────────────────────────────────────────────────
1188
+ _showTimer = null;
1189
+ _hideTimer = null;
1190
+ _abort = new AbortController();
1191
+ constructor() {
1192
+ if (!isPlatformBrowser(this._platformId))
1193
+ return;
1194
+ const host = this._el.nativeElement;
1195
+ const { signal } = this._abort;
1196
+ host.addEventListener('mouseenter', () => this._scheduleShow(), { signal });
1197
+ host.addEventListener('mouseleave', () => this._scheduleHide(), { signal });
1198
+ // :focus-visible, not a bare 'focus' check — a modal/drawer restores focus to its trigger
1199
+ // button programmatically when it closes (standard a11y behavior), which fires 'focus' on
1200
+ // this host with no real hover/keyboard interaction behind it. That reopened the tooltip and
1201
+ // left it stuck floating over the page after the modal was dismissed. :focus-visible only
1202
+ // matches when the browser itself determines the focus came from keyboard navigation, not
1203
+ // from a programmatic .focus() call or a mouse/touch click — exactly the real-vs-restored
1204
+ // distinction needed here.
1205
+ host.addEventListener('focus', () => { if (host.matches(':focus-visible'))
1206
+ this._scheduleShow(); }, { signal });
1207
+ host.addEventListener('blur', () => this._scheduleHide(), { signal });
1208
+ // Touch devices fire 'mouseenter'/'focus' on tap but never a real
1209
+ // 'mouseleave' — dismiss immediately (no fade) so the bubble can't
1210
+ // outlive the tap and float over whatever it triggered (e.g. a modal
1211
+ // opening on the same gesture). Capture-phase 'click' fires before the
1212
+ // host's own (click) handler — the one that actually opens the modal —
1213
+ // so the bubble is gone from the DOM before the modal is created.
1214
+ host.addEventListener('touchend', () => this._tooltip.hideImmediate(host), { signal });
1215
+ host.addEventListener('touchcancel', () => this._tooltip.hideImmediate(host), { signal });
1216
+ host.addEventListener('click', () => this._tooltip.hideImmediate(host), { signal, capture: true });
1217
+ this._destroyRef.onDestroy(() => {
1218
+ this._abort.abort();
1219
+ this._clearTimers();
1220
+ this._tooltip.hideImmediate(host);
1221
+ });
1222
+ }
1223
+ // ── Show / Hide ──────────────────────────────────────────────────────────
1224
+ _scheduleShow() {
1225
+ this._clearHide();
1226
+ const delay = this.clxTooltipDelay();
1227
+ if (delay > 0) {
1228
+ this._showTimer = setTimeout(() => this._show(), delay);
1229
+ }
1230
+ else {
1231
+ this._show();
1232
+ }
1233
+ }
1234
+ _scheduleHide() {
1235
+ this._clearShow();
1236
+ this._hideTimer = setTimeout(() => this._hide(), 80);
1237
+ }
1238
+ _show() {
1239
+ this._tooltip.show(this._el.nativeElement, {
1240
+ text: this.clxTooltip(),
1241
+ position: this.clxTooltipPosition(),
1242
+ color: this.clxTooltipColor(),
1243
+ size: this.clxTooltipSize(),
1244
+ });
1245
+ }
1246
+ _hide() {
1247
+ this._tooltip.hide(this._el.nativeElement);
1248
+ }
1249
+ _clearShow() {
1250
+ if (this._showTimer !== null) {
1251
+ clearTimeout(this._showTimer);
1252
+ this._showTimer = null;
1253
+ }
1254
+ }
1255
+ _clearHide() {
1256
+ if (this._hideTimer !== null) {
1257
+ clearTimeout(this._hideTimer);
1258
+ this._hideTimer = null;
1259
+ }
1260
+ }
1261
+ _clearTimers() {
1262
+ this._clearShow();
1263
+ this._clearHide();
1264
+ }
1265
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1266
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.15", type: ClxTooltipDirective, isStandalone: true, selector: "[clxTooltip]", inputs: { clxTooltip: { classPropertyName: "clxTooltip", publicName: "clxTooltip", isSignal: true, isRequired: true, transformFunction: null }, clxTooltipPosition: { classPropertyName: "clxTooltipPosition", publicName: "clxTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, clxTooltipColor: { classPropertyName: "clxTooltipColor", publicName: "clxTooltipColor", isSignal: true, isRequired: false, transformFunction: null }, clxTooltipSize: { classPropertyName: "clxTooltipSize", publicName: "clxTooltipSize", isSignal: true, isRequired: false, transformFunction: null }, clxTooltipDelay: { classPropertyName: "clxTooltipDelay", publicName: "clxTooltipDelay", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
1267
+ }
1268
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipDirective, decorators: [{
1269
+ type: Directive,
1270
+ args: [{
1271
+ selector: '[clxTooltip]',
1272
+ standalone: true,
1273
+ }]
1274
+ }], ctorParameters: () => [], propDecorators: { clxTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltip", required: true }] }], clxTooltipPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltipPosition", required: false }] }], clxTooltipColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltipColor", required: false }] }], clxTooltipSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltipSize", required: false }] }], clxTooltipDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltipDelay", required: false }] }] } });
1275
+
1276
+ // ── Context ───────────────────────────────────────────────────────────────────
1277
+ /** Provided by clx-table-actions so nested clx-button instances default to the theme's
1278
+ * secondaryColor instead of primaryColor when they don't pass their own [color] — any button
1279
+ * that DOES pass [color] explicitly still overrides this, same as CLX_CARD_DISABLED_CONTEXT. */
1280
+ const CLX_TABLE_ACTIONS_CONTEXT = new InjectionToken('CLX_TABLE_ACTIONS_CONTEXT');
1281
+ const BUTTON_BASE_CLASS = 'relative overflow-hidden inline-flex items-center justify-center font-medium select-none transition-[color,background-color,border-color,box-shadow] duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2';
1282
+ const BUTTON_SIZE_MAP = {
1283
+ xxs: 'text-xs px-1.5 py-0.5 gap-0.5',
1284
+ xs: 'text-xs px-2 py-1 gap-1',
1285
+ sm: 'text-xs px-3 py-1.5 gap-1.5',
1286
+ md: 'text-sm px-4 py-2 gap-2',
1287
+ lg: 'text-base px-5 py-2.5 gap-2.5',
1288
+ };
1289
+ const BUTTON_SHAPE_FIXED = {
1290
+ pill: 'rounded-full',
1291
+ circle: 'rounded-full',
1292
+ };
1293
+ const BUTTON_ICON_ONLY_SIZE_MAP = {
1294
+ xxs: 'w-5 h-5 p-0 text-xs',
1295
+ xs: 'w-7 h-7 p-0 text-xs',
1296
+ sm: 'w-8 h-8 p-0 text-sm',
1297
+ md: 'w-10 h-10 p-0 text-sm',
1298
+ lg: 'w-11 h-11 p-0 text-sm',
1299
+ };
1300
+
1301
+ const BADGE_SIZE_MAP = {
1302
+ xs: 'text-[10px] px-1.5 py-0.5',
1303
+ sm: 'text-xs px-2 py-1',
1304
+ md: 'text-xs px-2.5 py-1 font-medium',
1305
+ };
1306
+ const BADGE_CIRCLE_SIZE_MAP = {
1307
+ xs: 'text-[10px] w-4 h-4',
1308
+ sm: 'text-xs w-5 h-5',
1309
+ md: 'text-xs w-6 h-6 font-medium',
1310
+ };
1311
+ const BADGE_SHAPE_FIXED = {
1312
+ pill: 'rounded-full',
1313
+ circle: 'rounded-full',
1314
+ };
1315
+
1316
+ // ── Context ───────────────────────────────────────────────────────────────────
1317
+ /** Provided by clx-card so nested form controls/buttons can inherit its disabled state. */
1318
+ const CLX_CARD_DISABLED_CONTEXT = new InjectionToken('CLX_CARD_DISABLED_CONTEXT');
1319
+ const CARD_PADDING_MAP = {
1320
+ none: '',
1321
+ sm: 'p-2',
1322
+ md: 'p-3',
1323
+ lg: 'p-5',
1324
+ };
1325
+ const CARD_SHADOW_MAP = {
1326
+ none: 'shadow-none',
1327
+ sm: 'shadow-sm',
1328
+ md: 'shadow-md',
1329
+ lg: 'shadow-lg',
1330
+ xl: 'shadow-xl',
1331
+ '2xl': 'shadow-2xl',
1332
+ };
1333
+
1334
+ const SPINNER_SIZE_MAP = {
1335
+ xxs: 'w-3 h-3 border-2',
1336
+ xs: 'w-4 h-4 border-2',
1337
+ sm: 'w-5 h-5 border-2',
839
1338
  md: 'w-6 h-6 border-[3px]',
840
1339
  lg: 'w-8 h-8 border-[3px]',
841
1340
  xl: 'w-10 h-10 border-4',
@@ -1098,273 +1597,74 @@ class ClxButtonComponent {
1098
1597
  const diameter = Math.max(rect.width, rect.height);
1099
1598
  const id = ++this._nextId;
1100
1599
  this._ripples.update(rs => [...rs, { id, x: e.clientX - rect.left, y: e.clientY - rect.top, diameter }]);
1101
- setTimeout(() => this._ripples.update(rs => rs.filter(r => r.id !== id)), 650);
1102
- }
1103
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1104
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxButtonComponent, isStandalone: true, selector: "button[clx-button], a[clx-button]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, emphasis: { classPropertyName: "emphasis", publicName: "emphasis", isSignal: true, isRequired: false, transformFunction: null }, textColor: { classPropertyName: "textColor", publicName: "textColor", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconPosition: { classPropertyName: "iconPosition", publicName: "iconPosition", isSignal: true, isRequired: false, transformFunction: null }, iconOnly: { classPropertyName: "iconOnly", publicName: "iconOnly", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "_onRipple($event)" }, properties: { "class": "_hostClass()", "style.opacity": "_inactive() ? \"0.5\" : null", "style.cursor": "_inactive() ? \"not-allowed\" : \"pointer\"", "attr.disabled": "_inactive() ? true : null", "attr.aria-busy": "loading()" } }, ngImport: i0, template: `
1105
- @if (icon() && iconPosition() === 'left' && !loading()) {
1106
- <span clx-icon [name]="icon()!" [size]="_iconSize()"></span>
1107
- }
1108
- @if (loading()) {
1109
- <clx-spinner [size]="_iconSize()" [color]="_spinnerColor()" />
1110
- }
1111
- <ng-content />
1112
- @if (icon() && iconPosition() === 'right' && !loading()) {
1113
- <span clx-icon [name]="icon()!" [size]="_iconSize()"></span>
1114
- }
1115
- <span class="clx-ripple-clip">
1116
- @for (r of _ripples(); track r.id) {
1117
- <span
1118
- class="clx-ripple-wave"
1119
- [style.background-color]="_rippleColor()"
1120
- [style.width.px]="r.diameter"
1121
- [style.height.px]="r.diameter"
1122
- [style.left.px]="r.x - r.diameter / 2"
1123
- [style.top.px]="r.y - r.diameter / 2"
1124
- ></span>
1125
- }
1126
- </span>
1127
- @if (badge() !== undefined && badge() !== null) {
1128
- <span [class]="_badgeClass()">{{ badge() }}</span>
1129
- }
1130
- `, isInline: true, styles: ["@keyframes clx-ripple-anim{to{transform:scale(4);opacity:0}}.clx-ripple-clip{position:absolute;inset:0;overflow:hidden;border-radius:inherit;pointer-events:none}.clx-ripple-wave{position:absolute;border-radius:50%;transform:scale(0);animation:clx-ripple-anim .6s linear;pointer-events:none}\n"], dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSpinnerComponent, selector: "clx-spinner", inputs: ["size", "color", "variant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1131
- }
1132
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxButtonComponent, decorators: [{
1133
- type: Component,
1134
- args: [{ selector: 'button[clx-button], a[clx-button]', standalone: true, imports: [ClxIconComponent, ClxSpinnerComponent], template: `
1135
- @if (icon() && iconPosition() === 'left' && !loading()) {
1136
- <span clx-icon [name]="icon()!" [size]="_iconSize()"></span>
1137
- }
1138
- @if (loading()) {
1139
- <clx-spinner [size]="_iconSize()" [color]="_spinnerColor()" />
1140
- }
1141
- <ng-content />
1142
- @if (icon() && iconPosition() === 'right' && !loading()) {
1143
- <span clx-icon [name]="icon()!" [size]="_iconSize()"></span>
1144
- }
1145
- <span class="clx-ripple-clip">
1146
- @for (r of _ripples(); track r.id) {
1147
- <span
1148
- class="clx-ripple-wave"
1149
- [style.background-color]="_rippleColor()"
1150
- [style.width.px]="r.diameter"
1151
- [style.height.px]="r.diameter"
1152
- [style.left.px]="r.x - r.diameter / 2"
1153
- [style.top.px]="r.y - r.diameter / 2"
1154
- ></span>
1155
- }
1156
- </span>
1157
- @if (badge() !== undefined && badge() !== null) {
1158
- <span [class]="_badgeClass()">{{ badge() }}</span>
1159
- }
1160
- `, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1161
- '[class]': '_hostClass()',
1162
- '[style.opacity]': '_inactive() ? "0.5" : null',
1163
- '[style.cursor]': '_inactive() ? "not-allowed" : "pointer"',
1164
- '[attr.disabled]': '_inactive() ? true : null',
1165
- '[attr.aria-busy]': 'loading()',
1166
- '(click)': '_onRipple($event)',
1167
- }, styles: ["@keyframes clx-ripple-anim{to{transform:scale(4);opacity:0}}.clx-ripple-clip{position:absolute;inset:0;overflow:hidden;border-radius:inherit;pointer-events:none}.clx-ripple-wave{position:absolute;border-radius:50%;transform:scale(0);animation:clx-ripple-anim .6s linear;pointer-events:none}\n"] }]
1168
- }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], emphasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emphasis", required: false }] }], textColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "textColor", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPosition", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeColor", required: false }] }] } });
1169
-
1170
- const CLX_ANIMATE_DEFAULTS_VALUE = {
1171
- duration: 1000,
1172
- delay: 0,
1173
- repeat: 1,
1174
- easing: 'ease',
1175
- fillMode: 'both',
1176
- once: false,
1177
- threshold: 0.1,
1178
- };
1179
- const CLX_ANIMATE_DEFAULTS = new InjectionToken('CLX_ANIMATE_DEFAULTS', {
1180
- providedIn: 'root',
1181
- factory: () => CLX_ANIMATE_DEFAULTS_VALUE,
1182
- });
1183
-
1184
- const PREFIX = 'clx-anim--';
1185
- class ClxAnimateService {
1186
- platformId;
1187
- defaults;
1188
- isBrowser;
1189
- // ─── Estado de animaciones en curso ───────────────────────────────────────
1190
- _runningCount = signal(0, ...(ngDevMode ? [{ debugName: "_runningCount" }] : /* istanbul ignore next */ []));
1191
- isAnimating = computed(() => this._runningCount() > 0, ...(ngDevMode ? [{ debugName: "isAnimating" }] : /* istanbul ignore next */ []));
1192
- // ─── Cleanup por elemento (WeakMap preserva GC) ───────────────────────────
1193
- _cleanups = new WeakMap();
1194
- // ─── Pool de IntersectionObservers compartidos ────────────────────────────
1195
- _observerPools = new Map();
1196
- constructor() {
1197
- this.platformId = inject(PLATFORM_ID);
1198
- this.defaults = inject(CLX_ANIMATE_DEFAULTS);
1199
- this.isBrowser = isPlatformBrowser(this.platformId);
1200
- if (this.isBrowser)
1201
- this._warmup();
1202
- }
1203
- // ─── Warmup ───────────────────────────────────────────────────────────────
1204
- /**
1205
- * Fuerza al browser a parsear los keyframes antes de la primera interacción
1206
- * para evitar el jank del primer frame. Se ejecuta en idle para no bloquear
1207
- * el critical rendering path.
1208
- */
1209
- _warmup() {
1210
- const run = () => {
1211
- const el = document.createElement('div');
1212
- el.style.cssText = 'position:fixed;top:-9999px;pointer-events:none;opacity:0;';
1213
- el.classList.add(`${PREFIX}animated`, `${PREFIX}bounce`);
1214
- document.body.appendChild(el);
1215
- requestAnimationFrame(() => requestAnimationFrame(() => document.body.removeChild(el)));
1216
- };
1217
- if ('requestIdleCallback' in window) {
1218
- window
1219
- .requestIdleCallback(run, { timeout: 1000 });
1220
- }
1221
- else {
1222
- setTimeout(run, 200);
1223
- }
1224
- }
1225
- // ─── API pública ──────────────────────────────────────────────────────────
1226
- /**
1227
- * Anima cualquier HTMLElement de forma imperativa.
1228
- * Retorna una Promise que resuelve cuando la animación termina.
1229
- */
1230
- animate(element, animationOrConfig, overrides = {}) {
1231
- if (!this.isBrowser)
1232
- return Promise.resolve();
1233
- return this.runAnimation(element, this.buildConfig(animationOrConfig, overrides));
1234
- }
1235
- /** Cancela la animación en curso en el elemento y restaura su estado. */
1236
- stop(element) {
1237
- this._cleanups.get(element)?.();
1238
- }
1239
- /**
1240
- * Registra un elemento en el pool de IntersectionObservers.
1241
- * Reutiliza el observer existente para el threshold dado o crea uno nuevo.
1242
- * Retorna una función para desregistrar el elemento.
1243
- */
1244
- observe(element, threshold, cb) {
1245
- if (!this.isBrowser)
1246
- return () => { };
1247
- let pool = this._observerPools.get(threshold);
1248
- if (!pool) {
1249
- const registry = new Map();
1250
- const observer = new IntersectionObserver((entries) => {
1251
- for (const entry of entries) {
1252
- registry.get(entry.target)?.(entry.isIntersecting);
1253
- }
1254
- }, { threshold });
1255
- pool = { observer, registry };
1256
- this._observerPools.set(threshold, pool);
1257
- }
1258
- pool.registry.set(element, cb);
1259
- pool.observer.observe(element);
1260
- return () => {
1261
- const p = this._observerPools.get(threshold);
1262
- if (!p)
1263
- return;
1264
- p.observer.unobserve(element);
1265
- p.registry.delete(element);
1266
- if (p.registry.size === 0) {
1267
- p.observer.disconnect();
1268
- this._observerPools.delete(threshold);
1269
- }
1270
- };
1271
- }
1272
- // ─── Interno (usado por ClxAnimateDirective) ──────────────────────────────
1273
- runAnimation(element, config) {
1274
- // Solo hay clases previas que limpiar si el elemento ya tenía un cleanup
1275
- // registrado — en ese caso (restart de una animación en curso) sí hace
1276
- // falta forzar el reflow para que el browser registre la eliminación de
1277
- // clases antes de volver a agregarlas. En el caso común (primera
1278
- // animación del elemento, ej. un tooltip recién creado) stop() es un
1279
- // no-op y el reflow forzado es puro costo evitable.
1280
- const hadPreviousAnimation = this._cleanups.has(element);
1281
- this.stop(element);
1282
- if (hadPreviousAnimation)
1283
- void element.offsetHeight;
1284
- const animClass = `${PREFIX}${config.animation}`;
1285
- const isInfinite = config.repeat === 'infinite';
1286
- // A fillMode: 'forwards' animation (an exit fade/slide, e.g. clx-toast's playExitAnimation)
1287
- // is almost always followed by the caller destroying the element on the same tick its promise
1288
- // resolves — stripping animClass in _cleanup right then would drop animation-fill-mode's hold
1289
- // on the last keyframe, snapping the element back to its static CSS state (opaque, in place)
1290
- // for one paintable frame before removal, which read as a flash/"ghost" re-appearance. Classes
1291
- // are only stripped for a non-forwards run; a forwards run keeps them and instead resets
1292
- // them at the START of runAnimation's own reflow-forced restart path above, so a persisted
1293
- // element that's re-animated later (e.g. clx-animate-directive on hover) still resets cleanly.
1294
- const keepClassesOnEnd = config.fillMode === 'forwards';
1295
- element.style.setProperty('--clx-anim-duration', `${config.duration}ms`);
1296
- element.style.setProperty('--clx-anim-delay', `${config.delay}ms`);
1297
- element.style.setProperty('--clx-anim-repeat', isInfinite ? 'infinite' : String(config.repeat ?? 1));
1298
- element.style.setProperty('--clx-anim-easing', config.easing ?? 'ease');
1299
- if (config.fillMode) {
1300
- element.style.setProperty('animation-fill-mode', config.fillMode);
1301
- }
1302
- element.classList.add(`${PREFIX}animated`, animClass);
1303
- if (isInfinite)
1304
- element.classList.add(`${PREFIX}infinite`);
1305
- this._runningCount.update(c => c + 1);
1306
- if (isInfinite) {
1307
- this._cleanups.set(element, () => {
1308
- this._cleanup(element, animClass);
1309
- this._runningCount.update(c => Math.max(0, c - 1));
1310
- });
1311
- return Promise.resolve();
1312
- }
1313
- return new Promise(resolve => {
1314
- const handleEnd = (ev) => {
1315
- ev.stopPropagation();
1316
- if (keepClassesOnEnd) {
1317
- this._cleanups.delete(element);
1318
- }
1319
- else {
1320
- this._cleanup(element, animClass);
1321
- }
1322
- this._runningCount.update(c => Math.max(0, c - 1));
1323
- resolve();
1324
- };
1325
- element.addEventListener('animationend', handleEnd, { once: true });
1326
- this._cleanups.set(element, () => {
1327
- element.removeEventListener('animationend', handleEnd);
1328
- this._cleanup(element, animClass);
1329
- this._runningCount.update(c => Math.max(0, c - 1));
1330
- resolve();
1331
- });
1332
- });
1333
- }
1334
- buildConfig(animationOrConfig, overrides = {}) {
1335
- const base = typeof animationOrConfig === 'string'
1336
- ? { animation: animationOrConfig }
1337
- : animationOrConfig;
1338
- return {
1339
- duration: this.defaults.duration,
1340
- delay: this.defaults.delay,
1341
- repeat: this.defaults.repeat,
1342
- easing: this.defaults.easing,
1343
- fillMode: this.defaults.fillMode,
1344
- once: this.defaults.once,
1345
- threshold: this.defaults.threshold,
1346
- trigger: 'manual',
1347
- ...base,
1348
- ...overrides,
1349
- };
1600
+ setTimeout(() => this._ripples.update(rs => rs.filter(r => r.id !== id)), 650);
1350
1601
  }
1351
- // ─── Privado ──────────────────────────────────────────────────────────────
1352
- _cleanup(element, animClass) {
1353
- element.classList.remove(`${PREFIX}animated`, animClass, `${PREFIX}infinite`);
1354
- element.style.removeProperty('--clx-anim-duration');
1355
- element.style.removeProperty('--clx-anim-delay');
1356
- element.style.removeProperty('--clx-anim-repeat');
1357
- element.style.removeProperty('--clx-anim-easing');
1358
- element.style.removeProperty('animation-fill-mode');
1359
- this._cleanups.delete(element);
1602
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1603
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxButtonComponent, isStandalone: true, selector: "button[clx-button], a[clx-button]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, emphasis: { classPropertyName: "emphasis", publicName: "emphasis", isSignal: true, isRequired: false, transformFunction: null }, textColor: { classPropertyName: "textColor", publicName: "textColor", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconPosition: { classPropertyName: "iconPosition", publicName: "iconPosition", isSignal: true, isRequired: false, transformFunction: null }, iconOnly: { classPropertyName: "iconOnly", publicName: "iconOnly", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "_onRipple($event)" }, properties: { "class": "_hostClass()", "style.opacity": "_inactive() ? \"0.5\" : null", "style.cursor": "_inactive() ? \"not-allowed\" : \"pointer\"", "attr.disabled": "_inactive() ? true : null", "attr.aria-busy": "loading()" } }, ngImport: i0, template: `
1604
+ @if (icon() && iconPosition() === 'left' && !loading()) {
1605
+ <span clx-icon [name]="icon()!" [size]="_iconSize()"></span>
1360
1606
  }
1361
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxAnimateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1362
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxAnimateService, providedIn: 'root' });
1607
+ @if (loading()) {
1608
+ <clx-spinner [size]="_iconSize()" [color]="_spinnerColor()" />
1609
+ }
1610
+ <ng-content />
1611
+ @if (icon() && iconPosition() === 'right' && !loading()) {
1612
+ <span clx-icon [name]="icon()!" [size]="_iconSize()"></span>
1613
+ }
1614
+ <span class="clx-ripple-clip">
1615
+ @for (r of _ripples(); track r.id) {
1616
+ <span
1617
+ class="clx-ripple-wave"
1618
+ [style.background-color]="_rippleColor()"
1619
+ [style.width.px]="r.diameter"
1620
+ [style.height.px]="r.diameter"
1621
+ [style.left.px]="r.x - r.diameter / 2"
1622
+ [style.top.px]="r.y - r.diameter / 2"
1623
+ ></span>
1624
+ }
1625
+ </span>
1626
+ @if (badge() !== undefined && badge() !== null) {
1627
+ <span [class]="_badgeClass()">{{ badge() }}</span>
1628
+ }
1629
+ `, isInline: true, styles: ["@keyframes clx-ripple-anim{to{transform:scale(4);opacity:0}}.clx-ripple-clip{position:absolute;inset:0;overflow:hidden;border-radius:inherit;pointer-events:none}.clx-ripple-wave{position:absolute;border-radius:50%;transform:scale(0);animation:clx-ripple-anim .6s linear;pointer-events:none}\n"], dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSpinnerComponent, selector: "clx-spinner", inputs: ["size", "color", "variant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1363
1630
  }
1364
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxAnimateService, decorators: [{
1365
- type: Injectable,
1366
- args: [{ providedIn: 'root' }]
1367
- }], ctorParameters: () => [] });
1631
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxButtonComponent, decorators: [{
1632
+ type: Component,
1633
+ args: [{ selector: 'button[clx-button], a[clx-button]', standalone: true, imports: [ClxIconComponent, ClxSpinnerComponent], template: `
1634
+ @if (icon() && iconPosition() === 'left' && !loading()) {
1635
+ <span clx-icon [name]="icon()!" [size]="_iconSize()"></span>
1636
+ }
1637
+ @if (loading()) {
1638
+ <clx-spinner [size]="_iconSize()" [color]="_spinnerColor()" />
1639
+ }
1640
+ <ng-content />
1641
+ @if (icon() && iconPosition() === 'right' && !loading()) {
1642
+ <span clx-icon [name]="icon()!" [size]="_iconSize()"></span>
1643
+ }
1644
+ <span class="clx-ripple-clip">
1645
+ @for (r of _ripples(); track r.id) {
1646
+ <span
1647
+ class="clx-ripple-wave"
1648
+ [style.background-color]="_rippleColor()"
1649
+ [style.width.px]="r.diameter"
1650
+ [style.height.px]="r.diameter"
1651
+ [style.left.px]="r.x - r.diameter / 2"
1652
+ [style.top.px]="r.y - r.diameter / 2"
1653
+ ></span>
1654
+ }
1655
+ </span>
1656
+ @if (badge() !== undefined && badge() !== null) {
1657
+ <span [class]="_badgeClass()">{{ badge() }}</span>
1658
+ }
1659
+ `, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1660
+ '[class]': '_hostClass()',
1661
+ '[style.opacity]': '_inactive() ? "0.5" : null',
1662
+ '[style.cursor]': '_inactive() ? "not-allowed" : "pointer"',
1663
+ '[attr.disabled]': '_inactive() ? true : null',
1664
+ '[attr.aria-busy]': 'loading()',
1665
+ '(click)': '_onRipple($event)',
1666
+ }, styles: ["@keyframes clx-ripple-anim{to{transform:scale(4);opacity:0}}.clx-ripple-clip{position:absolute;inset:0;overflow:hidden;border-radius:inherit;pointer-events:none}.clx-ripple-wave{position:absolute;border-radius:50%;transform:scale(0);animation:clx-ripple-anim .6s linear;pointer-events:none}\n"] }]
1667
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], emphasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "emphasis", required: false }] }], textColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "textColor", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPosition", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeColor", required: false }] }] } });
1368
1668
 
1369
1669
  // ─── Injection tokens ──────────────────────────────────────────────────────────
1370
1670
  const CLX_TOAST_DEFAULTS = new InjectionToken('CLX_TOAST_DEFAULTS', {
@@ -2021,6 +2321,9 @@ class ClxCopyTextComponent {
2021
2321
  toast = inject(ClxToastService);
2022
2322
  text = input.required(...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
2023
2323
  copiedMessage = input('Copiado', ...(ngDevMode ? [{ debugName: "copiedMessage" }] : /* istanbul ignore next */ []));
2324
+ /** clx-tooltip shown on hover (same directive as the table actions). */
2325
+ tooltip = input('Copiar', ...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
2326
+ tooltipColor = input('slate', ...(ngDevMode ? [{ debugName: "tooltipColor" }] : /* istanbul ignore next */ []));
2024
2327
  copy(event) {
2025
2328
  event.stopPropagation();
2026
2329
  event.preventDefault();
@@ -2052,17 +2355,20 @@ class ClxCopyTextComponent {
2052
2355
  return ok;
2053
2356
  }
2054
2357
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxCopyTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2055
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.15", type: ClxCopyTextComponent, isStandalone: true, selector: "clx-copy-text", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, copiedMessage: { classPropertyName: "copiedMessage", publicName: "copiedMessage", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "button", "tabindex": "0" }, listeners: { "click": "copy($event)", "keydown.enter": "copy($event)" }, properties: { "attr.aria-label": "\"Copiar \" + text()", "attr.title": "\"Copiar\"" }, classAttribute: "inline-flex items-center gap-1.5 max-w-full cursor-pointer rounded-md hover:bg-clx-surface-3 px-1 -mx-1 transition-colors" }, ngImport: i0, template: `
2056
- <span class="truncate">{{ text() }}</span>
2057
- <span clx-icon name="content_copy" size="xs" class="text-clx-text-subtle shrink-0"></span>
2058
- `, isInline: true, dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2358
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.15", type: ClxCopyTextComponent, isStandalone: true, selector: "clx-copy-text", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, copiedMessage: { classPropertyName: "copiedMessage", publicName: "copiedMessage", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, tooltipColor: { classPropertyName: "tooltipColor", publicName: "tooltipColor", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "button", "tabindex": "0" }, listeners: { "click": "copy($event)", "keydown.enter": "copy($event)" }, properties: { "attr.aria-label": "\"Copiar \" + text()" }, classAttribute: "inline-flex items-center gap-1.5 max-w-full cursor-pointer rounded-md hover:bg-clx-surface-3 px-1 -mx-1 transition-colors" }, ngImport: i0, template: `
2359
+ <span class="inline-flex items-center gap-1.5 min-w-0"
2360
+ [clxTooltip]="tooltip()" [clxTooltipColor]="tooltipColor()" clxTooltipPosition="top" clxTooltipSize="sm">
2361
+ <span class="truncate">{{ text() }}</span>
2362
+ <span clx-icon name="content_copy" size="xs" class="text-clx-text-subtle shrink-0"></span>
2363
+ </span>
2364
+ `, isInline: true, dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "directive", type: ClxTooltipDirective, selector: "[clxTooltip]", inputs: ["clxTooltip", "clxTooltipPosition", "clxTooltipColor", "clxTooltipSize", "clxTooltipDelay"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2059
2365
  }
2060
2366
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxCopyTextComponent, decorators: [{
2061
2367
  type: Component,
2062
2368
  args: [{
2063
2369
  selector: 'clx-copy-text',
2064
2370
  standalone: true,
2065
- imports: [ClxIconComponent],
2371
+ imports: [ClxIconComponent, ClxTooltipDirective],
2066
2372
  encapsulation: ViewEncapsulation.None,
2067
2373
  changeDetection: ChangeDetectionStrategy.OnPush,
2068
2374
  host: {
@@ -2070,16 +2376,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
2070
2376
  role: 'button',
2071
2377
  tabindex: '0',
2072
2378
  '[attr.aria-label]': '"Copiar " + text()',
2073
- '[attr.title]': '"Copiar"',
2074
2379
  '(click)': 'copy($event)',
2075
2380
  '(keydown.enter)': 'copy($event)',
2076
2381
  },
2077
2382
  template: `
2078
- <span class="truncate">{{ text() }}</span>
2079
- <span clx-icon name="content_copy" size="xs" class="text-clx-text-subtle shrink-0"></span>
2383
+ <span class="inline-flex items-center gap-1.5 min-w-0"
2384
+ [clxTooltip]="tooltip()" [clxTooltipColor]="tooltipColor()" clxTooltipPosition="top" clxTooltipSize="sm">
2385
+ <span class="truncate">{{ text() }}</span>
2386
+ <span clx-icon name="content_copy" size="xs" class="text-clx-text-subtle shrink-0"></span>
2387
+ </span>
2080
2388
  `,
2081
2389
  }]
2082
- }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], copiedMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "copiedMessage", required: false }] }] } });
2390
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], copiedMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "copiedMessage", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], tooltipColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipColor", required: false }] }] } });
2083
2391
 
2084
2392
  const SOCIAL_ICON_DEFS = {
2085
2393
  instagram: {
@@ -3426,516 +3734,216 @@ class ClxStepperComponent {
3426
3734
  </ng-template>
3427
3735
  `, isInline: true, dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3428
3736
  }
3429
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxStepperComponent, decorators: [{
3430
- type: Component,
3431
- args: [{
3432
- selector: 'clx-stepper',
3433
- standalone: true,
3434
- imports: [ClxIconComponent, NgTemplateOutlet],
3435
- encapsulation: ViewEncapsulation.None,
3436
- changeDetection: ChangeDetectionStrategy.OnPush,
3437
- host: { '[class]': '_hostClass()' },
3438
- template: `
3439
- @for (step of steps(); track $index; let last = $last; let i = $index) {
3440
-
3441
- <!-- Horizontal: circle+label form one fixed-width column, connector is a separate flex-1
3442
- track next to it — so the label centers under its own circle, not under circle+connector.
3443
- Vertical: circle+connector form one column, label sits beside it. -->
3444
- <div [class]="_stepWrapClass()">
3445
-
3446
- @if (orientation() === 'horizontal') {
3447
- <div [class]="_circleColClass()">
3448
- <div [class]="_circleClass(step.status)">
3449
- <ng-container [ngTemplateOutlet]="circleContent" [ngTemplateOutletContext]="{ step, i }" />
3450
- </div>
3451
- <div [class]="_labelWrapClass()">
3452
- <span [class]="_labelClass(step.status)">{{ step.label }}</span>
3453
- @if (step.description) {
3454
- <span [class]="'text-clx-text-subtle ' + _tokens().desc">{{ step.description }}</span>
3455
- }
3456
- </div>
3457
- </div>
3458
- @if (!last) {
3459
- <div [class]="_connectorClass(step.status)"></div>
3460
- }
3461
- } @else {
3462
- <div [class]="_circleColClass()">
3463
- <div [class]="_circleClass(step.status)">
3464
- <ng-container [ngTemplateOutlet]="circleContent" [ngTemplateOutletContext]="{ step, i }" />
3465
- </div>
3466
- @if (!last) {
3467
- <div [class]="_connectorClass(step.status)"></div>
3468
- }
3469
- </div>
3470
- <div [class]="_labelWrapClass()">
3471
- <span [class]="_labelClass(step.status)">{{ step.label }}</span>
3472
- @if (step.description) {
3473
- <span [class]="'text-clx-text-subtle ' + _tokens().desc">{{ step.description }}</span>
3474
- }
3475
- </div>
3476
- }
3477
-
3478
- </div>
3479
- }
3480
-
3481
- <ng-template #circleContent let-step="step" let-i="i">
3482
- @switch (step.status) {
3483
- @case ('completed') {
3484
- <span clx-icon name="check" [size]="_tokens().iconSize"></span>
3485
- }
3486
- @case ('error') {
3487
- <span clx-icon name="close" [size]="_tokens().iconSize"></span>
3488
- }
3489
- @default {
3490
- @if (step.icon) {
3491
- <span clx-icon [name]="step.icon" [size]="_tokens().iconSize"></span>
3492
- } @else {
3493
- <span [class]="'font-bold leading-none select-none ' + _tokens().label">{{ i + 1 }}</span>
3494
- }
3495
- }
3496
- }
3497
- </ng-template>
3498
- `,
3499
- }]
3500
- }], propDecorators: { steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: true }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3501
-
3502
- const CLX_TIMELINE_CONTEXT = new InjectionToken('CLX_TIMELINE_CONTEXT');
3503
- // ── Line style ────────────────────────────────────────────────────────────────
3504
- const TIMELINE_LINE_STYLE = {
3505
- dotted: 'border-l-2 border-dashed border-slate-200',
3506
- solid: 'border-l-2 border-solid border-slate-200',
3507
- none: '',
3508
- };
3509
- // ── Size map ──────────────────────────────────────────────────────────────────
3510
- const TIMELINE_SIZE_MAP = {
3511
- sm: { dot: 'w-8 h-8', iconSize: 'xs', item: 'px-3 pt-2', gap: 'gap-3', title: 'text-sm', desc: 'text-xs', time: 'text-xs', contentPb: 'pb-5' },
3512
- md: { dot: 'w-10 h-10', iconSize: 'sm', item: 'px-4 pt-3', gap: 'gap-4', title: 'text-sm', desc: 'text-xs', time: 'text-xs', contentPb: 'pb-6' },
3513
- lg: { dot: 'w-12 h-12', iconSize: 'md', item: 'px-5 pt-4', gap: 'gap-5', title: 'text-base', desc: 'text-sm', time: 'text-sm', contentPb: 'pb-8' },
3514
- };
3515
-
3516
- class ClxTimelineComponent {
3517
- _themeSvc = inject(ClxThemeService);
3518
- color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3519
- resolvedColor = computed(() => this.color() ?? this._themeSvc.config().primaryColor, ...(ngDevMode ? [{ debugName: "resolvedColor" }] : /* istanbul ignore next */ []));
3520
- lineStyle = input('dotted', ...(ngDevMode ? [{ debugName: "lineStyle" }] : /* istanbul ignore next */ []));
3521
- mode = input('left', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
3522
- size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3523
- sizeTokens = computed(() => TIMELINE_SIZE_MAP[this.size()], ...(ngDevMode ? [{ debugName: "sizeTokens" }] : /* istanbul ignore next */ []));
3524
- _hostClass = computed(() => 'flex flex-col w-full', ...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
3525
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3526
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.15", type: ClxTimelineComponent, isStandalone: true, selector: "clx-timeline", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, lineStyle: { classPropertyName: "lineStyle", publicName: "lineStyle", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "list" }, properties: { "class": "_hostClass()" } }, providers: [
3527
- { provide: CLX_TIMELINE_CONTEXT, useExisting: forwardRef(() => ClxTimelineComponent) },
3528
- ], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3529
- }
3530
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineComponent, decorators: [{
3531
- type: Component,
3532
- args: [{
3533
- selector: 'clx-timeline',
3534
- standalone: true,
3535
- imports: [],
3536
- template: `<ng-content />`,
3537
- encapsulation: ViewEncapsulation.None,
3538
- changeDetection: ChangeDetectionStrategy.OnPush,
3539
- host: {
3540
- '[class]': '_hostClass()',
3541
- 'role': 'list',
3542
- },
3543
- providers: [
3544
- { provide: CLX_TIMELINE_CONTEXT, useExisting: forwardRef(() => ClxTimelineComponent) },
3545
- ],
3546
- }]
3547
- }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], lineStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "lineStyle", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3548
-
3549
- class ClxTimelineItemComponent {
3550
- _ctx = inject(CLX_TIMELINE_CONTEXT);
3551
- icon = input('circle', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
3552
- color = input(null, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3553
- title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
3554
- description = input('', ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
3555
- time = input('', ...(ngDevMode ? [{ debugName: "time" }] : /* istanbul ignore next */ []));
3556
- last = input(false, ...(ngDevMode ? [{ debugName: "last" }] : /* istanbul ignore next */ []));
3557
- _resolvedColor = computed(() => resolveColor(this.color() ?? this._ctx.resolvedColor()), ...(ngDevMode ? [{ debugName: "_resolvedColor" }] : /* istanbul ignore next */ []));
3558
- _iconClass = computed(() => this._resolvedColor().textSubtle, ...(ngDevMode ? [{ debugName: "_iconClass" }] : /* istanbul ignore next */ []));
3559
- _hostClass = computed(() => {
3560
- const { gap, item } = this._ctx.sizeTokens();
3561
- return `flex ${gap} w-full ${item}`;
3562
- }, ...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
3563
- _dotClass = computed(() => {
3564
- const { dot } = this._ctx.sizeTokens();
3565
- return `${dot} rounded-full flex items-center justify-center shrink-0 ${this._resolvedColor().bgSubtle}`;
3566
- }, ...(ngDevMode ? [{ debugName: "_dotClass" }] : /* istanbul ignore next */ []));
3567
- _lineClass = computed(() => {
3568
- if (this.last())
3569
- return 'flex-1';
3570
- const lineStyle = TIMELINE_LINE_STYLE[this._ctx.lineStyle()];
3571
- return `flex-1 w-0 mt-1 ${lineStyle}`.trim();
3572
- }, ...(ngDevMode ? [{ debugName: "_lineClass" }] : /* istanbul ignore next */ []));
3573
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3574
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxTimelineItemComponent, isStandalone: true, selector: "clx-timeline-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", 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 }, time: { classPropertyName: "time", publicName: "time", isSignal: true, isRequired: false, transformFunction: null }, last: { classPropertyName: "last", publicName: "last", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "listitem" }, properties: { "class": "_hostClass()" } }, ngImport: i0, template: `
3575
- <div class="flex flex-col items-center shrink-0">
3576
- <div [class]="_dotClass()">
3577
- <span clx-icon [name]="icon()" [size]="_ctx.sizeTokens().iconSize" [class]="_iconClass()"></span>
3578
- </div>
3579
- <div [class]="_lineClass()"></div>
3580
- </div>
3581
-
3582
- <div [class]="'flex-1 min-w-0 ' + _ctx.sizeTokens().contentPb">
3583
- <div class="flex items-start justify-between gap-2">
3584
- <p [class]="'font-(--clx-timeline-title-weight) text-clx-text-label leading-snug ' + _ctx.sizeTokens().title">
3585
- {{ title() }}
3586
- </p>
3587
- @if (time()) {
3588
- <span [class]="'shrink-0 text-clx-text-subtle text-right leading-snug ' + _ctx.sizeTokens().time">
3589
- {{ time() }}
3590
- </span>
3591
- }
3592
- </div>
3593
- @if (description()) {
3594
- <p [class]="'text-clx-text-muted mt-0.5 ' + _ctx.sizeTokens().desc">{{ description() }}</p>
3595
- }
3596
- <ng-content />
3597
- </div>
3598
- `, isInline: true, dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3599
- }
3600
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineItemComponent, decorators: [{
3737
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxStepperComponent, decorators: [{
3601
3738
  type: Component,
3602
3739
  args: [{
3603
- selector: 'clx-timeline-item',
3740
+ selector: 'clx-stepper',
3604
3741
  standalone: true,
3605
- imports: [ClxIconComponent],
3606
- template: `
3607
- <div class="flex flex-col items-center shrink-0">
3608
- <div [class]="_dotClass()">
3609
- <span clx-icon [name]="icon()" [size]="_ctx.sizeTokens().iconSize" [class]="_iconClass()"></span>
3610
- </div>
3611
- <div [class]="_lineClass()"></div>
3612
- </div>
3613
-
3614
- <div [class]="'flex-1 min-w-0 ' + _ctx.sizeTokens().contentPb">
3615
- <div class="flex items-start justify-between gap-2">
3616
- <p [class]="'font-(--clx-timeline-title-weight) text-clx-text-label leading-snug ' + _ctx.sizeTokens().title">
3617
- {{ title() }}
3618
- </p>
3619
- @if (time()) {
3620
- <span [class]="'shrink-0 text-clx-text-subtle text-right leading-snug ' + _ctx.sizeTokens().time">
3621
- {{ time() }}
3622
- </span>
3623
- }
3624
- </div>
3625
- @if (description()) {
3626
- <p [class]="'text-clx-text-muted mt-0.5 ' + _ctx.sizeTokens().desc">{{ description() }}</p>
3627
- }
3628
- <ng-content />
3629
- </div>
3630
- `,
3742
+ imports: [ClxIconComponent, NgTemplateOutlet],
3631
3743
  encapsulation: ViewEncapsulation.None,
3632
3744
  changeDetection: ChangeDetectionStrategy.OnPush,
3633
- host: {
3634
- '[class]': '_hostClass()',
3635
- 'role': 'listitem',
3636
- },
3637
- }]
3638
- }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], time: [{ type: i0.Input, args: [{ isSignal: true, alias: "time", required: false }] }], last: [{ type: i0.Input, args: [{ isSignal: true, alias: "last", required: false }] }] } });
3639
-
3640
- const TOOLTIP_SIZE_MAP = {
3641
- sm: 'text-xs px-2 py-1',
3642
- md: 'text-sm px-3 py-1.5 max-w-[220px]',
3643
- };
3745
+ host: { '[class]': '_hostClass()' },
3746
+ template: `
3747
+ @for (step of steps(); track $index; let last = $last; let i = $index) {
3644
3748
 
3645
- class ClxTooltipComponent {
3646
- _themeSvc = inject(ClxThemeService);
3647
- text = input.required(...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
3648
- position = input('top', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
3649
- color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3650
- size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3651
- _color = computed(() => this.color() ?? this._themeSvc.config().neutralColor, ...(ngDevMode ? [{ debugName: "_color" }] : /* istanbul ignore next */ []));
3652
- _hostClass = computed(() => {
3653
- const t = resolveColor(this._color());
3654
- return `${t.text} rounded-lg shadow-xl ${t.bg} ${TOOLTIP_SIZE_MAP[this.size()]}`;
3655
- }, ...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
3656
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3657
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.15", type: ClxTooltipComponent, isStandalone: true, selector: "clx-tooltip", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tooltip" }, properties: { "class": "_hostClass()" } }, ngImport: i0, template: `<span class="whitespace-pre-wrap leading-snug">{{ text() }}</span>`, isInline: true, styles: ["clx-tooltip{position:absolute;display:inline-flex;align-items:center;justify-content:center;pointer-events:none;will-change:opacity,transform}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3658
- }
3659
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipComponent, decorators: [{
3660
- type: Component,
3661
- args: [{ selector: 'clx-tooltip', standalone: true, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: {
3662
- 'role': 'tooltip',
3663
- '[class]': '_hostClass()',
3664
- }, template: `<span class="whitespace-pre-wrap leading-snug">{{ text() }}</span>`, styles: ["clx-tooltip{position:absolute;display:inline-flex;align-items:center;justify-content:center;pointer-events:none;will-change:opacity,transform}\n"] }]
3665
- }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3749
+ <!-- Horizontal: circle+label form one fixed-width column, connector is a separate flex-1
3750
+ track next to it — so the label centers under its own circle, not under circle+connector.
3751
+ Vertical: circle+connector form one column, label sits beside it. -->
3752
+ <div [class]="_stepWrapClass()">
3666
3753
 
3667
- // ── Gap between trigger and bubble (px) ───────────────────────────────────
3668
- const GAP = 8;
3669
- /**
3670
- * Shared, app-wide tooltip bubble — a single ClxTooltipComponent instance reused by every
3671
- * [clxTooltip] directive instead of each one creating/destroying its own on every hover.
3672
- * A table with 75 tooltipped cells used to mean up to 75 independent createComponent/destroy
3673
- * cycles as the mouse scanned across it; now there's exactly one bubble, repositioned and
3674
- * relabeled per show(). Ownership is tracked by trigger element so a directive can only hide
3675
- * the bubble it itself showed (prevents a stale hide() from a slower hover-out racing a newer
3676
- * hover-in on a different element).
3677
- */
3678
- class ClxTooltipService {
3679
- _appRef = inject(ApplicationRef);
3680
- _injector = inject(EnvironmentInjector);
3681
- _animate = inject(ClxAnimateService);
3682
- _platformId = inject(PLATFORM_ID);
3683
- _renderer;
3684
- _isBrowser;
3685
- _ref = null;
3686
- _el = null;
3687
- _owner = null;
3688
- _hiding = false;
3689
- constructor() {
3690
- this._isBrowser = isPlatformBrowser(this._platformId);
3691
- this._renderer = inject(RendererFactory2).createRenderer(null, null);
3692
- }
3693
- /** Shows (or relabels/repositions, if already visible) the shared bubble for `trigger`. */
3694
- show(trigger, config) {
3695
- if (!this._isBrowser || !config.text)
3696
- return;
3697
- this._owner = trigger;
3698
- this._hiding = false;
3699
- const ref = this._ref ?? this._create();
3700
- ref.setInput('text', config.text);
3701
- ref.setInput('color', config.color);
3702
- ref.setInput('size', config.size);
3703
- ref.setInput('position', config.position);
3704
- const el = ref.location.nativeElement;
3705
- this._animate.stop(el);
3706
- el.style.visibility = 'hidden';
3707
- el.style.opacity = '';
3708
- ref.changeDetectorRef.detectChanges();
3709
- // Single measurement pass — see clx-tooltip.directive.ts's original comment: read both
3710
- // rects once, resolve the flipped position and its coordinates from that same read, then
3711
- // apply everything in one write batch instead of interleaving reads/writes.
3712
- const triggerRect = trigger.getBoundingClientRect();
3713
- const bubbleRect = el.getBoundingClientRect();
3714
- const finalPos = this._resolvePosition(config.position, triggerRect, bubbleRect);
3715
- if (finalPos !== config.position) {
3716
- ref.setInput('position', finalPos);
3717
- ref.changeDetectorRef.detectChanges();
3718
- }
3719
- this._placeAt(el, triggerRect, bubbleRect, finalPos);
3720
- el.style.visibility = '';
3721
- trigger.setAttribute('aria-describedby', this._tooltipId);
3722
- this._animate.animate(el, { animation: 'fadeIn', duration: 160, trigger: 'manual', fillMode: 'both' });
3723
- }
3724
- /** Hides the bubble, but only if `trigger` is the element that currently owns it — a hover-out
3725
- * from an element that already lost ownership (a newer hover-in interrupted it) is a no-op.
3726
- * Relies on the 'animationend' DOM event to know when the fade-out finished — but that event
3727
- * can simply never fire (backgrounded tab throttling animations, layout thrashing from a route
3728
- * change interrupting the transition, etc.), which would leave the promise unresolved forever
3729
- * and _owner never cleared — since this bubble lives outside any component's lifecycle (a
3730
- * singleton appended straight to document.body), nothing else would ever hide it again. A hard
3731
- * fallback timeout guarantees the bubble is actually hidden and ownership released regardless of
3732
- * whether the CSS event cooperates. */
3733
- hide(trigger) {
3734
- if (!this._isBrowser || this._owner !== trigger || !this._ref)
3735
- return;
3736
- this._hiding = true;
3737
- trigger.removeAttribute('aria-describedby');
3738
- const el = this._ref.location.nativeElement;
3739
- const settle = () => {
3740
- if (this._owner !== trigger || !this._hiding)
3741
- return;
3742
- el.style.visibility = 'hidden';
3743
- this._owner = null;
3744
- this._hiding = false;
3745
- };
3746
- const fallback = setTimeout(settle, 400);
3747
- this._animate.animate(el, { animation: 'fadeOut', duration: 120, trigger: 'manual', fillMode: 'forwards' })
3748
- .then(() => {
3749
- clearTimeout(fallback);
3750
- settle();
3751
- });
3752
- }
3753
- /** Synchronous, no fade — used on touchend/click so the bubble can't linger over UI the
3754
- * gesture opens (e.g. a modal). Only acts if `trigger` still owns the bubble. */
3755
- hideImmediate(trigger) {
3756
- if (!this._isBrowser || this._owner !== trigger || !this._ref)
3757
- return;
3758
- trigger.removeAttribute('aria-describedby');
3759
- const el = this._ref.location.nativeElement;
3760
- this._animate.stop(el);
3761
- el.style.visibility = 'hidden';
3762
- this._owner = null;
3763
- this._hiding = false;
3764
- }
3765
- get _tooltipId() {
3766
- return 'clx-tooltip-shared';
3767
- }
3768
- _create() {
3769
- const ref = createComponent(ClxTooltipComponent, { environmentInjector: this._injector });
3770
- this._appRef.attachView(ref.hostView);
3771
- const el = ref.location.nativeElement;
3772
- el.style.position = 'fixed';
3773
- el.style.visibility = 'hidden';
3774
- el.style.zIndex = '9999';
3775
- el.id = this._tooltipId;
3776
- this._renderer.appendChild(document.body, el);
3777
- this._ref = ref;
3778
- return ref;
3779
- }
3780
- // ── Positioning ──────────────────────────────────────────────────────────
3781
- _resolvePosition(requested, trigger, bubble) {
3782
- const vw = window.innerWidth;
3783
- const vh = window.innerHeight;
3784
- const fits = {
3785
- top: trigger.top - bubble.height - GAP >= 0,
3786
- bottom: trigger.bottom + bubble.height + GAP <= vh,
3787
- left: trigger.left - bubble.width - GAP >= 0,
3788
- right: trigger.right + bubble.width + GAP <= vw,
3789
- };
3790
- if (fits[requested])
3791
- return requested;
3792
- const opposite = {
3793
- top: 'bottom', bottom: 'top', left: 'right', right: 'left',
3794
- };
3795
- const opp = opposite[requested];
3796
- if (fits[opp])
3797
- return opp;
3798
- for (const pos of ['top', 'bottom', 'left', 'right']) {
3799
- if (fits[pos])
3800
- return pos;
3801
- }
3802
- return requested; // last resort
3803
- }
3804
- _placeAt(el, trigger, bubble, pos) {
3805
- let top = 0;
3806
- let left = 0;
3807
- switch (pos) {
3808
- case 'top':
3809
- top = trigger.top - bubble.height - GAP;
3810
- left = trigger.left + trigger.width / 2 - bubble.width / 2;
3811
- break;
3812
- case 'bottom':
3813
- top = trigger.bottom + GAP;
3814
- left = trigger.left + trigger.width / 2 - bubble.width / 2;
3815
- break;
3816
- case 'left':
3817
- top = trigger.top + trigger.height / 2 - bubble.height / 2;
3818
- left = trigger.left - bubble.width - GAP;
3819
- break;
3820
- case 'right':
3821
- top = trigger.top + trigger.height / 2 - bubble.height / 2;
3822
- left = trigger.right + GAP;
3823
- break;
3754
+ @if (orientation() === 'horizontal') {
3755
+ <div [class]="_circleColClass()">
3756
+ <div [class]="_circleClass(step.status)">
3757
+ <ng-container [ngTemplateOutlet]="circleContent" [ngTemplateOutletContext]="{ step, i }" />
3758
+ </div>
3759
+ <div [class]="_labelWrapClass()">
3760
+ <span [class]="_labelClass(step.status)">{{ step.label }}</span>
3761
+ @if (step.description) {
3762
+ <span [class]="'text-clx-text-subtle ' + _tokens().desc">{{ step.description }}</span>
3763
+ }
3764
+ </div>
3765
+ </div>
3766
+ @if (!last) {
3767
+ <div [class]="_connectorClass(step.status)"></div>
3768
+ }
3769
+ } @else {
3770
+ <div [class]="_circleColClass()">
3771
+ <div [class]="_circleClass(step.status)">
3772
+ <ng-container [ngTemplateOutlet]="circleContent" [ngTemplateOutletContext]="{ step, i }" />
3773
+ </div>
3774
+ @if (!last) {
3775
+ <div [class]="_connectorClass(step.status)"></div>
3776
+ }
3777
+ </div>
3778
+ <div [class]="_labelWrapClass()">
3779
+ <span [class]="_labelClass(step.status)">{{ step.label }}</span>
3780
+ @if (step.description) {
3781
+ <span [class]="'text-clx-text-subtle ' + _tokens().desc">{{ step.description }}</span>
3782
+ }
3783
+ </div>
3824
3784
  }
3825
- const margin = 4;
3826
- left = Math.max(margin, Math.min(left, window.innerWidth - bubble.width - margin));
3827
- top = Math.max(margin, Math.min(top, window.innerHeight - bubble.height - margin));
3828
- el.style.top = `${top}px`;
3829
- el.style.left = `${left}px`;
3830
- }
3831
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3832
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, providedIn: 'root' });
3833
- }
3834
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, decorators: [{
3835
- type: Injectable,
3836
- args: [{ providedIn: 'root' }]
3837
- }], ctorParameters: () => [] });
3838
3785
 
3839
- class ClxTooltipDirective {
3840
- // ── Inputs ───────────────────────────────────────────────────────────────
3841
- clxTooltip = input.required(...(ngDevMode ? [{ debugName: "clxTooltip" }] : /* istanbul ignore next */ []));
3842
- clxTooltipPosition = input('top', ...(ngDevMode ? [{ debugName: "clxTooltipPosition" }] : /* istanbul ignore next */ []));
3843
- clxTooltipColor = input('slate', ...(ngDevMode ? [{ debugName: "clxTooltipColor" }] : /* istanbul ignore next */ []));
3844
- clxTooltipSize = input('md', ...(ngDevMode ? [{ debugName: "clxTooltipSize" }] : /* istanbul ignore next */ []));
3845
- clxTooltipDelay = input(0, ...(ngDevMode ? [{ debugName: "clxTooltipDelay" }] : /* istanbul ignore next */ []));
3846
- // ── DI ───────────────────────────────────────────────────────────────────
3847
- _el = inject(ElementRef);
3848
- _tooltip = inject(ClxTooltipService);
3849
- _platformId = inject(PLATFORM_ID);
3850
- _destroyRef = inject(DestroyRef);
3851
- // ── State ────────────────────────────────────────────────────────────────
3852
- _showTimer = null;
3853
- _hideTimer = null;
3854
- _abort = new AbortController();
3855
- constructor() {
3856
- if (!isPlatformBrowser(this._platformId))
3857
- return;
3858
- const host = this._el.nativeElement;
3859
- const { signal } = this._abort;
3860
- host.addEventListener('mouseenter', () => this._scheduleShow(), { signal });
3861
- host.addEventListener('mouseleave', () => this._scheduleHide(), { signal });
3862
- // :focus-visible, not a bare 'focus' check — a modal/drawer restores focus to its trigger
3863
- // button programmatically when it closes (standard a11y behavior), which fires 'focus' on
3864
- // this host with no real hover/keyboard interaction behind it. That reopened the tooltip and
3865
- // left it stuck floating over the page after the modal was dismissed. :focus-visible only
3866
- // matches when the browser itself determines the focus came from keyboard navigation, not
3867
- // from a programmatic .focus() call or a mouse/touch click — exactly the real-vs-restored
3868
- // distinction needed here.
3869
- host.addEventListener('focus', () => { if (host.matches(':focus-visible'))
3870
- this._scheduleShow(); }, { signal });
3871
- host.addEventListener('blur', () => this._scheduleHide(), { signal });
3872
- // Touch devices fire 'mouseenter'/'focus' on tap but never a real
3873
- // 'mouseleave' — dismiss immediately (no fade) so the bubble can't
3874
- // outlive the tap and float over whatever it triggered (e.g. a modal
3875
- // opening on the same gesture). Capture-phase 'click' fires before the
3876
- // host's own (click) handler — the one that actually opens the modal —
3877
- // so the bubble is gone from the DOM before the modal is created.
3878
- host.addEventListener('touchend', () => this._tooltip.hideImmediate(host), { signal });
3879
- host.addEventListener('touchcancel', () => this._tooltip.hideImmediate(host), { signal });
3880
- host.addEventListener('click', () => this._tooltip.hideImmediate(host), { signal, capture: true });
3881
- this._destroyRef.onDestroy(() => {
3882
- this._abort.abort();
3883
- this._clearTimers();
3884
- this._tooltip.hideImmediate(host);
3885
- });
3786
+ </div>
3886
3787
  }
3887
- // ── Show / Hide ──────────────────────────────────────────────────────────
3888
- _scheduleShow() {
3889
- this._clearHide();
3890
- const delay = this.clxTooltipDelay();
3891
- if (delay > 0) {
3892
- this._showTimer = setTimeout(() => this._show(), delay);
3788
+
3789
+ <ng-template #circleContent let-step="step" let-i="i">
3790
+ @switch (step.status) {
3791
+ @case ('completed') {
3792
+ <span clx-icon name="check" [size]="_tokens().iconSize"></span>
3893
3793
  }
3894
- else {
3895
- this._show();
3794
+ @case ('error') {
3795
+ <span clx-icon name="close" [size]="_tokens().iconSize"></span>
3896
3796
  }
3897
- }
3898
- _scheduleHide() {
3899
- this._clearShow();
3900
- this._hideTimer = setTimeout(() => this._hide(), 80);
3901
- }
3902
- _show() {
3903
- this._tooltip.show(this._el.nativeElement, {
3904
- text: this.clxTooltip(),
3905
- position: this.clxTooltipPosition(),
3906
- color: this.clxTooltipColor(),
3907
- size: this.clxTooltipSize(),
3908
- });
3909
- }
3910
- _hide() {
3911
- this._tooltip.hide(this._el.nativeElement);
3912
- }
3913
- _clearShow() {
3914
- if (this._showTimer !== null) {
3915
- clearTimeout(this._showTimer);
3916
- this._showTimer = null;
3797
+ @default {
3798
+ @if (step.icon) {
3799
+ <span clx-icon [name]="step.icon" [size]="_tokens().iconSize"></span>
3800
+ } @else {
3801
+ <span [class]="'font-bold leading-none select-none ' + _tokens().label">{{ i + 1 }}</span>
3802
+ }
3917
3803
  }
3918
- }
3919
- _clearHide() {
3920
- if (this._hideTimer !== null) {
3921
- clearTimeout(this._hideTimer);
3922
- this._hideTimer = null;
3804
+ }
3805
+ </ng-template>
3806
+ `,
3807
+ }]
3808
+ }], propDecorators: { steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: true }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3809
+
3810
+ const CLX_TIMELINE_CONTEXT = new InjectionToken('CLX_TIMELINE_CONTEXT');
3811
+ // ── Line style ────────────────────────────────────────────────────────────────
3812
+ const TIMELINE_LINE_STYLE = {
3813
+ dotted: 'border-l-2 border-dashed border-slate-200',
3814
+ solid: 'border-l-2 border-solid border-slate-200',
3815
+ none: '',
3816
+ };
3817
+ // ── Size map ──────────────────────────────────────────────────────────────────
3818
+ const TIMELINE_SIZE_MAP = {
3819
+ sm: { dot: 'w-8 h-8', iconSize: 'xs', item: 'px-3 pt-2', gap: 'gap-3', title: 'text-sm', desc: 'text-xs', time: 'text-xs', contentPb: 'pb-5' },
3820
+ md: { dot: 'w-10 h-10', iconSize: 'sm', item: 'px-4 pt-3', gap: 'gap-4', title: 'text-sm', desc: 'text-xs', time: 'text-xs', contentPb: 'pb-6' },
3821
+ lg: { dot: 'w-12 h-12', iconSize: 'md', item: 'px-5 pt-4', gap: 'gap-5', title: 'text-base', desc: 'text-sm', time: 'text-sm', contentPb: 'pb-8' },
3822
+ };
3823
+
3824
+ class ClxTimelineComponent {
3825
+ _themeSvc = inject(ClxThemeService);
3826
+ color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3827
+ resolvedColor = computed(() => this.color() ?? this._themeSvc.config().primaryColor, ...(ngDevMode ? [{ debugName: "resolvedColor" }] : /* istanbul ignore next */ []));
3828
+ lineStyle = input('dotted', ...(ngDevMode ? [{ debugName: "lineStyle" }] : /* istanbul ignore next */ []));
3829
+ mode = input('left', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
3830
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3831
+ sizeTokens = computed(() => TIMELINE_SIZE_MAP[this.size()], ...(ngDevMode ? [{ debugName: "sizeTokens" }] : /* istanbul ignore next */ []));
3832
+ _hostClass = computed(() => 'flex flex-col w-full', ...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
3833
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3834
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.15", type: ClxTimelineComponent, isStandalone: true, selector: "clx-timeline", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, lineStyle: { classPropertyName: "lineStyle", publicName: "lineStyle", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "list" }, properties: { "class": "_hostClass()" } }, providers: [
3835
+ { provide: CLX_TIMELINE_CONTEXT, useExisting: forwardRef(() => ClxTimelineComponent) },
3836
+ ], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3837
+ }
3838
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineComponent, decorators: [{
3839
+ type: Component,
3840
+ args: [{
3841
+ selector: 'clx-timeline',
3842
+ standalone: true,
3843
+ imports: [],
3844
+ template: `<ng-content />`,
3845
+ encapsulation: ViewEncapsulation.None,
3846
+ changeDetection: ChangeDetectionStrategy.OnPush,
3847
+ host: {
3848
+ '[class]': '_hostClass()',
3849
+ 'role': 'list',
3850
+ },
3851
+ providers: [
3852
+ { provide: CLX_TIMELINE_CONTEXT, useExisting: forwardRef(() => ClxTimelineComponent) },
3853
+ ],
3854
+ }]
3855
+ }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], lineStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "lineStyle", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3856
+
3857
+ class ClxTimelineItemComponent {
3858
+ _ctx = inject(CLX_TIMELINE_CONTEXT);
3859
+ icon = input('circle', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
3860
+ color = input(null, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3861
+ title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
3862
+ description = input('', ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
3863
+ time = input('', ...(ngDevMode ? [{ debugName: "time" }] : /* istanbul ignore next */ []));
3864
+ last = input(false, ...(ngDevMode ? [{ debugName: "last" }] : /* istanbul ignore next */ []));
3865
+ _resolvedColor = computed(() => resolveColor(this.color() ?? this._ctx.resolvedColor()), ...(ngDevMode ? [{ debugName: "_resolvedColor" }] : /* istanbul ignore next */ []));
3866
+ _iconClass = computed(() => this._resolvedColor().textSubtle, ...(ngDevMode ? [{ debugName: "_iconClass" }] : /* istanbul ignore next */ []));
3867
+ _hostClass = computed(() => {
3868
+ const { gap, item } = this._ctx.sizeTokens();
3869
+ return `flex ${gap} w-full ${item}`;
3870
+ }, ...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
3871
+ _dotClass = computed(() => {
3872
+ const { dot } = this._ctx.sizeTokens();
3873
+ return `${dot} rounded-full flex items-center justify-center shrink-0 ${this._resolvedColor().bgSubtle}`;
3874
+ }, ...(ngDevMode ? [{ debugName: "_dotClass" }] : /* istanbul ignore next */ []));
3875
+ _lineClass = computed(() => {
3876
+ if (this.last())
3877
+ return 'flex-1';
3878
+ const lineStyle = TIMELINE_LINE_STYLE[this._ctx.lineStyle()];
3879
+ return `flex-1 w-0 mt-1 ${lineStyle}`.trim();
3880
+ }, ...(ngDevMode ? [{ debugName: "_lineClass" }] : /* istanbul ignore next */ []));
3881
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3882
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxTimelineItemComponent, isStandalone: true, selector: "clx-timeline-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", 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 }, time: { classPropertyName: "time", publicName: "time", isSignal: true, isRequired: false, transformFunction: null }, last: { classPropertyName: "last", publicName: "last", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "listitem" }, properties: { "class": "_hostClass()" } }, ngImport: i0, template: `
3883
+ <div class="flex flex-col items-center shrink-0">
3884
+ <div [class]="_dotClass()">
3885
+ <span clx-icon [name]="icon()" [size]="_ctx.sizeTokens().iconSize" [class]="_iconClass()"></span>
3886
+ </div>
3887
+ <div [class]="_lineClass()"></div>
3888
+ </div>
3889
+
3890
+ <div [class]="'flex-1 min-w-0 ' + _ctx.sizeTokens().contentPb">
3891
+ <div class="flex items-start justify-between gap-2">
3892
+ <p [class]="'font-(--clx-timeline-title-weight) text-clx-text-label leading-snug ' + _ctx.sizeTokens().title">
3893
+ {{ title() }}
3894
+ </p>
3895
+ @if (time()) {
3896
+ <span [class]="'shrink-0 text-clx-text-subtle text-right leading-snug ' + _ctx.sizeTokens().time">
3897
+ {{ time() }}
3898
+ </span>
3923
3899
  }
3924
- }
3925
- _clearTimers() {
3926
- this._clearShow();
3927
- this._clearHide();
3928
- }
3929
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3930
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.15", type: ClxTooltipDirective, isStandalone: true, selector: "[clxTooltip]", inputs: { clxTooltip: { classPropertyName: "clxTooltip", publicName: "clxTooltip", isSignal: true, isRequired: true, transformFunction: null }, clxTooltipPosition: { classPropertyName: "clxTooltipPosition", publicName: "clxTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, clxTooltipColor: { classPropertyName: "clxTooltipColor", publicName: "clxTooltipColor", isSignal: true, isRequired: false, transformFunction: null }, clxTooltipSize: { classPropertyName: "clxTooltipSize", publicName: "clxTooltipSize", isSignal: true, isRequired: false, transformFunction: null }, clxTooltipDelay: { classPropertyName: "clxTooltipDelay", publicName: "clxTooltipDelay", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
3900
+ </div>
3901
+ @if (description()) {
3902
+ <p [class]="'text-clx-text-muted mt-0.5 ' + _ctx.sizeTokens().desc">{{ description() }}</p>
3903
+ }
3904
+ <ng-content />
3905
+ </div>
3906
+ `, isInline: true, dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3931
3907
  }
3932
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipDirective, decorators: [{
3933
- type: Directive,
3908
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineItemComponent, decorators: [{
3909
+ type: Component,
3934
3910
  args: [{
3935
- selector: '[clxTooltip]',
3911
+ selector: 'clx-timeline-item',
3936
3912
  standalone: true,
3913
+ imports: [ClxIconComponent],
3914
+ template: `
3915
+ <div class="flex flex-col items-center shrink-0">
3916
+ <div [class]="_dotClass()">
3917
+ <span clx-icon [name]="icon()" [size]="_ctx.sizeTokens().iconSize" [class]="_iconClass()"></span>
3918
+ </div>
3919
+ <div [class]="_lineClass()"></div>
3920
+ </div>
3921
+
3922
+ <div [class]="'flex-1 min-w-0 ' + _ctx.sizeTokens().contentPb">
3923
+ <div class="flex items-start justify-between gap-2">
3924
+ <p [class]="'font-(--clx-timeline-title-weight) text-clx-text-label leading-snug ' + _ctx.sizeTokens().title">
3925
+ {{ title() }}
3926
+ </p>
3927
+ @if (time()) {
3928
+ <span [class]="'shrink-0 text-clx-text-subtle text-right leading-snug ' + _ctx.sizeTokens().time">
3929
+ {{ time() }}
3930
+ </span>
3931
+ }
3932
+ </div>
3933
+ @if (description()) {
3934
+ <p [class]="'text-clx-text-muted mt-0.5 ' + _ctx.sizeTokens().desc">{{ description() }}</p>
3935
+ }
3936
+ <ng-content />
3937
+ </div>
3938
+ `,
3939
+ encapsulation: ViewEncapsulation.None,
3940
+ changeDetection: ChangeDetectionStrategy.OnPush,
3941
+ host: {
3942
+ '[class]': '_hostClass()',
3943
+ 'role': 'listitem',
3944
+ },
3937
3945
  }]
3938
- }], ctorParameters: () => [], propDecorators: { clxTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltip", required: true }] }], clxTooltipPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltipPosition", required: false }] }], clxTooltipColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltipColor", required: false }] }], clxTooltipSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltipSize", required: false }] }], clxTooltipDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxTooltipDelay", required: false }] }] } });
3946
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], time: [{ type: i0.Input, args: [{ isSignal: true, alias: "time", required: false }] }], last: [{ type: i0.Input, args: [{ isSignal: true, alias: "last", required: false }] }] } });
3939
3947
 
3940
3948
  const INPUT_STATUS_MAP = {
3941
3949
  default: {