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.
- package/fesm2022/codexly-ui.mjs +831 -823
- package/fesm2022/codexly-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/codexly-ui.d.ts +6 -3
package/fesm2022/codexly-ui.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, computed, ChangeDetectionStrategy, ViewEncapsulation, Component, InjectionToken, inject, PLATFORM_ID,
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
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:
|
|
1365
|
-
type:
|
|
1366
|
-
args: [{
|
|
1367
|
-
|
|
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()"
|
|
2056
|
-
<span class="
|
|
2057
|
-
|
|
2058
|
-
|
|
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="
|
|
2079
|
-
|
|
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-
|
|
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
|
-
|
|
3635
|
-
|
|
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
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
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
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
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
|
-
|
|
3895
|
-
|
|
3794
|
+
@case ('error') {
|
|
3795
|
+
<span clx-icon name="close" [size]="_tokens().iconSize"></span>
|
|
3896
3796
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
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
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
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
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
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:
|
|
3933
|
-
type:
|
|
3908
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTimelineItemComponent, decorators: [{
|
|
3909
|
+
type: Component,
|
|
3934
3910
|
args: [{
|
|
3935
|
-
selector: '
|
|
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
|
-
}],
|
|
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: {
|