codexly-ui 0.8.2 → 0.9.0
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 +238 -192
- package/fesm2022/codexly-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/codexly-ui.d.ts +14 -21
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, signal, effect, Injectable, ElementRef, HostAttributeToken, output, Directive, forwardRef, contentChild,
|
|
2
|
+
import { input, computed, ChangeDetectionStrategy, ViewEncapsulation, Component, InjectionToken, inject, PLATFORM_ID, signal, effect, Injectable, ElementRef, HostAttributeToken, output, Directive, forwardRef, contentChild, ApplicationRef, EnvironmentInjector, RendererFactory2, createComponent, DestroyRef, untracked, HostListener, viewChild, ViewChild, Injector, ChangeDetectorRef, NgZone, model, contentChildren, ContentChildren, ViewChildren, numberAttribute, booleanAttribute, Input } from '@angular/core';
|
|
3
3
|
import { isPlatformBrowser, NgTemplateOutlet, DOCUMENT, NgStyle, CurrencyPipe, DecimalPipe } from '@angular/common';
|
|
4
4
|
import * as i1$1 from '@angular/forms';
|
|
5
5
|
import { NG_VALUE_ACCESSOR, FormControl, NgControl, ReactiveFormsModule, FormGroup, Validators, FormsModule } from '@angular/forms';
|
|
@@ -2352,10 +2352,16 @@ class ClxAnimateService {
|
|
|
2352
2352
|
}
|
|
2353
2353
|
// ─── Interno (usado por ClxAnimateDirective) ──────────────────────────────
|
|
2354
2354
|
runAnimation(element, config) {
|
|
2355
|
+
// Solo hay clases previas que limpiar si el elemento ya tenía un cleanup
|
|
2356
|
+
// registrado — en ese caso (restart de una animación en curso) sí hace
|
|
2357
|
+
// falta forzar el reflow para que el browser registre la eliminación de
|
|
2358
|
+
// clases antes de volver a agregarlas. En el caso común (primera
|
|
2359
|
+
// animación del elemento, ej. un tooltip recién creado) stop() es un
|
|
2360
|
+
// no-op y el reflow forzado es puro costo evitable.
|
|
2361
|
+
const hadPreviousAnimation = this._cleanups.has(element);
|
|
2355
2362
|
this.stop(element);
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
void element.offsetHeight;
|
|
2363
|
+
if (hadPreviousAnimation)
|
|
2364
|
+
void element.offsetHeight;
|
|
2359
2365
|
const animClass = `${PREFIX}${config.animation}`;
|
|
2360
2366
|
const isInfinite = config.repeat === 'infinite';
|
|
2361
2367
|
element.style.setProperty('--clx-anim-duration', `${config.duration}ms`);
|
|
@@ -2429,83 +2435,92 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
2429
2435
|
|
|
2430
2436
|
// ── Gap between trigger and bubble (px) ───────────────────────────────────
|
|
2431
2437
|
const GAP = 8;
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2438
|
+
/**
|
|
2439
|
+
* Shared, app-wide tooltip bubble — a single ClxTooltipComponent instance reused by every
|
|
2440
|
+
* [clxTooltip] directive instead of each one creating/destroying its own on every hover.
|
|
2441
|
+
* A table with 75 tooltipped cells used to mean up to 75 independent createComponent/destroy
|
|
2442
|
+
* cycles as the mouse scanned across it; now there's exactly one bubble, repositioned and
|
|
2443
|
+
* relabeled per show(). Ownership is tracked by trigger element so a directive can only hide
|
|
2444
|
+
* the bubble it itself showed (prevents a stale hide() from a slower hover-out racing a newer
|
|
2445
|
+
* hover-in on a different element).
|
|
2446
|
+
*/
|
|
2447
|
+
class ClxTooltipService {
|
|
2442
2448
|
_appRef = inject(ApplicationRef);
|
|
2443
2449
|
_injector = inject(EnvironmentInjector);
|
|
2444
2450
|
_animate = inject(ClxAnimateService);
|
|
2445
2451
|
_platformId = inject(PLATFORM_ID);
|
|
2446
|
-
|
|
2447
|
-
|
|
2452
|
+
_renderer;
|
|
2453
|
+
_isBrowser;
|
|
2448
2454
|
_ref = null;
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
_tooltipId = `clx-tooltip-${Math.random().toString(36).slice(2, 8)}`;
|
|
2453
|
-
_abort = new AbortController();
|
|
2454
|
-
_destroyed = false;
|
|
2455
|
+
_el = null;
|
|
2456
|
+
_owner = null;
|
|
2457
|
+
_hiding = false;
|
|
2455
2458
|
constructor() {
|
|
2456
|
-
|
|
2459
|
+
this._isBrowser = isPlatformBrowser(this._platformId);
|
|
2460
|
+
this._renderer = inject(RendererFactory2).createRenderer(null, null);
|
|
2461
|
+
}
|
|
2462
|
+
/** Shows (or relabels/repositions, if already visible) the shared bubble for `trigger`. */
|
|
2463
|
+
show(trigger, config) {
|
|
2464
|
+
if (!this._isBrowser || !config.text)
|
|
2457
2465
|
return;
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2466
|
+
this._owner = trigger;
|
|
2467
|
+
this._hiding = false;
|
|
2468
|
+
const ref = this._ref ?? this._create();
|
|
2469
|
+
ref.setInput('text', config.text);
|
|
2470
|
+
ref.setInput('color', config.color);
|
|
2471
|
+
ref.setInput('size', config.size);
|
|
2472
|
+
ref.setInput('position', config.position);
|
|
2473
|
+
const el = ref.location.nativeElement;
|
|
2474
|
+
this._animate.stop(el);
|
|
2475
|
+
el.style.visibility = 'hidden';
|
|
2476
|
+
el.style.opacity = '';
|
|
2477
|
+
ref.changeDetectorRef.detectChanges();
|
|
2478
|
+
// Single measurement pass — see clx-tooltip.directive.ts's original comment: read both
|
|
2479
|
+
// rects once, resolve the flipped position and its coordinates from that same read, then
|
|
2480
|
+
// apply everything in one write batch instead of interleaving reads/writes.
|
|
2481
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
2482
|
+
const bubbleRect = el.getBoundingClientRect();
|
|
2483
|
+
const finalPos = this._resolvePosition(config.position, triggerRect, bubbleRect);
|
|
2484
|
+
if (finalPos !== config.position) {
|
|
2485
|
+
ref.setInput('position', finalPos);
|
|
2486
|
+
ref.changeDetectorRef.detectChanges();
|
|
2487
|
+
}
|
|
2488
|
+
this._placeAt(el, triggerRect, bubbleRect, finalPos);
|
|
2489
|
+
el.style.visibility = '';
|
|
2490
|
+
trigger.setAttribute('aria-describedby', this._tooltipId);
|
|
2491
|
+
this._animate.animate(el, { animation: 'fadeIn', duration: 160, trigger: 'manual', fillMode: 'both' });
|
|
2492
|
+
}
|
|
2493
|
+
/** Hides the bubble, but only if `trigger` is the element that currently owns it — a hover-out
|
|
2494
|
+
* from an element that already lost ownership (a newer hover-in interrupted it) is a no-op. */
|
|
2495
|
+
hide(trigger) {
|
|
2496
|
+
if (!this._isBrowser || this._owner !== trigger || !this._ref)
|
|
2497
|
+
return;
|
|
2498
|
+
this._hiding = true;
|
|
2499
|
+
trigger.removeAttribute('aria-describedby');
|
|
2500
|
+
const el = this._ref.location.nativeElement;
|
|
2501
|
+
this._animate.animate(el, { animation: 'fadeOut', duration: 120, trigger: 'manual', fillMode: 'forwards' })
|
|
2502
|
+
.then(() => {
|
|
2503
|
+
if (this._owner === trigger && this._hiding)
|
|
2504
|
+
this._owner = null;
|
|
2478
2505
|
});
|
|
2479
2506
|
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2507
|
+
/** Synchronous, no fade — used on touchend/click so the bubble can't linger over UI the
|
|
2508
|
+
* gesture opens (e.g. a modal). Only acts if `trigger` still owns the bubble. */
|
|
2509
|
+
hideImmediate(trigger) {
|
|
2510
|
+
if (!this._isBrowser || this._owner !== trigger || !this._ref)
|
|
2511
|
+
return;
|
|
2512
|
+
trigger.removeAttribute('aria-describedby');
|
|
2513
|
+
const el = this._ref.location.nativeElement;
|
|
2514
|
+
this._animate.stop(el);
|
|
2515
|
+
el.style.visibility = 'hidden';
|
|
2516
|
+
this._owner = null;
|
|
2517
|
+
this._hiding = false;
|
|
2490
2518
|
}
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
this._hideTimer = setTimeout(() => this._hide(), 80);
|
|
2519
|
+
get _tooltipId() {
|
|
2520
|
+
return 'clx-tooltip-shared';
|
|
2494
2521
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
return;
|
|
2498
|
-
if (!this.clxTooltip())
|
|
2499
|
-
return;
|
|
2500
|
-
// Create component
|
|
2501
|
-
const ref = createComponent(ClxTooltipComponent, {
|
|
2502
|
-
environmentInjector: this._injector,
|
|
2503
|
-
});
|
|
2504
|
-
ref.setInput('text', this.clxTooltip());
|
|
2505
|
-
ref.setInput('color', this.clxTooltipColor());
|
|
2506
|
-
ref.setInput('size', this.clxTooltipSize());
|
|
2507
|
-
// We'll set position after measurement
|
|
2508
|
-
ref.setInput('position', this.clxTooltipPosition());
|
|
2522
|
+
_create() {
|
|
2523
|
+
const ref = createComponent(ClxTooltipComponent, { environmentInjector: this._injector });
|
|
2509
2524
|
this._appRef.attachView(ref.hostView);
|
|
2510
2525
|
const el = ref.location.nativeElement;
|
|
2511
2526
|
el.style.position = 'fixed';
|
|
@@ -2513,63 +2528,13 @@ class ClxTooltipDirective {
|
|
|
2513
2528
|
el.style.zIndex = '9999';
|
|
2514
2529
|
el.id = this._tooltipId;
|
|
2515
2530
|
this._renderer.appendChild(document.body, el);
|
|
2516
|
-
ref.changeDetectorRef.detectChanges();
|
|
2517
|
-
// Resolve final position (with viewport flip)
|
|
2518
|
-
const finalPos = this._resolvePosition(el);
|
|
2519
|
-
ref.setInput('position', finalPos);
|
|
2520
|
-
ref.changeDetectorRef.detectChanges();
|
|
2521
|
-
// Place at resolved coordinates
|
|
2522
|
-
this._placeAt(el, finalPos);
|
|
2523
|
-
el.style.visibility = '';
|
|
2524
2531
|
this._ref = ref;
|
|
2525
|
-
|
|
2526
|
-
this._el.nativeElement.setAttribute('aria-describedby', this._tooltipId);
|
|
2527
|
-
// Animate in
|
|
2528
|
-
this._animate.animate(el, {
|
|
2529
|
-
animation: 'fadeIn',
|
|
2530
|
-
duration: 160,
|
|
2531
|
-
trigger: 'manual',
|
|
2532
|
-
fillMode: 'both',
|
|
2533
|
-
});
|
|
2534
|
-
}
|
|
2535
|
-
_hide() {
|
|
2536
|
-
const ref = this._ref;
|
|
2537
|
-
if (!ref)
|
|
2538
|
-
return;
|
|
2539
|
-
this._ref = null;
|
|
2540
|
-
this._pendingRef = ref;
|
|
2541
|
-
this._el.nativeElement.removeAttribute('aria-describedby');
|
|
2542
|
-
const el = ref.location.nativeElement;
|
|
2543
|
-
this._animate.animate(el, {
|
|
2544
|
-
animation: 'fadeOut',
|
|
2545
|
-
duration: 120,
|
|
2546
|
-
trigger: 'manual',
|
|
2547
|
-
fillMode: 'forwards',
|
|
2548
|
-
}).then(() => {
|
|
2549
|
-
this._pendingRef = null;
|
|
2550
|
-
this._appRef.detachView(ref.hostView);
|
|
2551
|
-
ref.destroy();
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
/** Synchronous teardown, no fade — used on touchend so the bubble can't linger over UI the tap opens. */
|
|
2555
|
-
_hideImmediate() {
|
|
2556
|
-
this._clearTimers();
|
|
2557
|
-
this._destroyBubble();
|
|
2558
|
-
if (this._pendingRef) {
|
|
2559
|
-
const el = this._pendingRef.location.nativeElement;
|
|
2560
|
-
this._animate.stop(el);
|
|
2561
|
-
this._appRef.detachView(this._pendingRef.hostView);
|
|
2562
|
-
this._pendingRef.destroy();
|
|
2563
|
-
this._pendingRef = null;
|
|
2564
|
-
}
|
|
2532
|
+
return ref;
|
|
2565
2533
|
}
|
|
2566
2534
|
// ── Positioning ──────────────────────────────────────────────────────────
|
|
2567
|
-
_resolvePosition(
|
|
2568
|
-
const requested = this.clxTooltipPosition();
|
|
2535
|
+
_resolvePosition(requested, trigger, bubble) {
|
|
2569
2536
|
const vw = window.innerWidth;
|
|
2570
2537
|
const vh = window.innerHeight;
|
|
2571
|
-
const trigger = this._el.nativeElement.getBoundingClientRect();
|
|
2572
|
-
const bubble = el.getBoundingClientRect();
|
|
2573
2538
|
const fits = {
|
|
2574
2539
|
top: trigger.top - bubble.height - GAP >= 0,
|
|
2575
2540
|
bottom: trigger.bottom + bubble.height + GAP <= vh,
|
|
@@ -2578,23 +2543,19 @@ class ClxTooltipDirective {
|
|
|
2578
2543
|
};
|
|
2579
2544
|
if (fits[requested])
|
|
2580
2545
|
return requested;
|
|
2581
|
-
// Flip axis
|
|
2582
2546
|
const opposite = {
|
|
2583
2547
|
top: 'bottom', bottom: 'top', left: 'right', right: 'left',
|
|
2584
2548
|
};
|
|
2585
2549
|
const opp = opposite[requested];
|
|
2586
2550
|
if (fits[opp])
|
|
2587
2551
|
return opp;
|
|
2588
|
-
// Fallback: any position that fits
|
|
2589
2552
|
for (const pos of ['top', 'bottom', 'left', 'right']) {
|
|
2590
2553
|
if (fits[pos])
|
|
2591
2554
|
return pos;
|
|
2592
2555
|
}
|
|
2593
2556
|
return requested; // last resort
|
|
2594
2557
|
}
|
|
2595
|
-
_placeAt(el, pos) {
|
|
2596
|
-
const trigger = this._el.nativeElement.getBoundingClientRect();
|
|
2597
|
-
const bubble = el.getBoundingClientRect();
|
|
2558
|
+
_placeAt(el, trigger, bubble, pos) {
|
|
2598
2559
|
let top = 0;
|
|
2599
2560
|
let left = 0;
|
|
2600
2561
|
switch (pos) {
|
|
@@ -2615,36 +2576,86 @@ class ClxTooltipDirective {
|
|
|
2615
2576
|
left = trigger.right + GAP;
|
|
2616
2577
|
break;
|
|
2617
2578
|
}
|
|
2618
|
-
// Clamp to viewport edges with 4px margin
|
|
2619
2579
|
const margin = 4;
|
|
2620
2580
|
left = Math.max(margin, Math.min(left, window.innerWidth - bubble.width - margin));
|
|
2621
2581
|
top = Math.max(margin, Math.min(top, window.innerHeight - bubble.height - margin));
|
|
2622
2582
|
el.style.top = `${top}px`;
|
|
2623
2583
|
el.style.left = `${left}px`;
|
|
2624
2584
|
}
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2585
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2586
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, providedIn: 'root' });
|
|
2587
|
+
}
|
|
2588
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTooltipService, decorators: [{
|
|
2589
|
+
type: Injectable,
|
|
2590
|
+
args: [{ providedIn: 'root' }]
|
|
2591
|
+
}], ctorParameters: () => [] });
|
|
2592
|
+
|
|
2593
|
+
class ClxTooltipDirective {
|
|
2594
|
+
// ── Inputs ───────────────────────────────────────────────────────────────
|
|
2595
|
+
clxTooltip = input.required(...(ngDevMode ? [{ debugName: "clxTooltip" }] : /* istanbul ignore next */ []));
|
|
2596
|
+
clxTooltipPosition = input('top', ...(ngDevMode ? [{ debugName: "clxTooltipPosition" }] : /* istanbul ignore next */ []));
|
|
2597
|
+
clxTooltipColor = input('slate', ...(ngDevMode ? [{ debugName: "clxTooltipColor" }] : /* istanbul ignore next */ []));
|
|
2598
|
+
clxTooltipSize = input('md', ...(ngDevMode ? [{ debugName: "clxTooltipSize" }] : /* istanbul ignore next */ []));
|
|
2599
|
+
clxTooltipDelay = input(0, ...(ngDevMode ? [{ debugName: "clxTooltipDelay" }] : /* istanbul ignore next */ []));
|
|
2600
|
+
// ── DI ───────────────────────────────────────────────────────────────────
|
|
2601
|
+
_el = inject(ElementRef);
|
|
2602
|
+
_tooltip = inject(ClxTooltipService);
|
|
2603
|
+
_platformId = inject(PLATFORM_ID);
|
|
2604
|
+
_destroyRef = inject(DestroyRef);
|
|
2605
|
+
// ── State ────────────────────────────────────────────────────────────────
|
|
2606
|
+
_showTimer = null;
|
|
2607
|
+
_hideTimer = null;
|
|
2608
|
+
_abort = new AbortController();
|
|
2609
|
+
constructor() {
|
|
2610
|
+
if (!isPlatformBrowser(this._platformId))
|
|
2628
2611
|
return;
|
|
2629
|
-
this.
|
|
2630
|
-
this.
|
|
2631
|
-
this.
|
|
2632
|
-
|
|
2612
|
+
const host = this._el.nativeElement;
|
|
2613
|
+
const { signal } = this._abort;
|
|
2614
|
+
host.addEventListener('mouseenter', () => this._scheduleShow(), { signal });
|
|
2615
|
+
host.addEventListener('mouseleave', () => this._scheduleHide(), { signal });
|
|
2616
|
+
host.addEventListener('focus', () => this._scheduleShow(), { signal });
|
|
2617
|
+
host.addEventListener('blur', () => this._scheduleHide(), { signal });
|
|
2618
|
+
// Touch devices fire 'mouseenter'/'focus' on tap but never a real
|
|
2619
|
+
// 'mouseleave' — dismiss immediately (no fade) so the bubble can't
|
|
2620
|
+
// outlive the tap and float over whatever it triggered (e.g. a modal
|
|
2621
|
+
// opening on the same gesture). Capture-phase 'click' fires before the
|
|
2622
|
+
// host's own (click) handler — the one that actually opens the modal —
|
|
2623
|
+
// so the bubble is gone from the DOM before the modal is created.
|
|
2624
|
+
host.addEventListener('touchend', () => this._tooltip.hideImmediate(host), { signal });
|
|
2625
|
+
host.addEventListener('touchcancel', () => this._tooltip.hideImmediate(host), { signal });
|
|
2626
|
+
host.addEventListener('click', () => this._tooltip.hideImmediate(host), { signal, capture: true });
|
|
2627
|
+
this._destroyRef.onDestroy(() => {
|
|
2628
|
+
this._abort.abort();
|
|
2629
|
+
this._clearTimers();
|
|
2630
|
+
this._tooltip.hideImmediate(host);
|
|
2631
|
+
});
|
|
2633
2632
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2633
|
+
// ── Show / Hide ──────────────────────────────────────────────────────────
|
|
2634
|
+
_scheduleShow() {
|
|
2635
|
+
this._clearHide();
|
|
2636
|
+
const delay = this.clxTooltipDelay();
|
|
2637
|
+
if (delay > 0) {
|
|
2638
|
+
this._showTimer = setTimeout(() => this._show(), delay);
|
|
2639
2639
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
this._animate.stop(el);
|
|
2643
|
-
this._appRef.detachView(this._pendingRef.hostView);
|
|
2644
|
-
this._pendingRef.destroy();
|
|
2645
|
-
this._pendingRef = null;
|
|
2640
|
+
else {
|
|
2641
|
+
this._show();
|
|
2646
2642
|
}
|
|
2647
2643
|
}
|
|
2644
|
+
_scheduleHide() {
|
|
2645
|
+
this._clearShow();
|
|
2646
|
+
this._hideTimer = setTimeout(() => this._hide(), 80);
|
|
2647
|
+
}
|
|
2648
|
+
_show() {
|
|
2649
|
+
this._tooltip.show(this._el.nativeElement, {
|
|
2650
|
+
text: this.clxTooltip(),
|
|
2651
|
+
position: this.clxTooltipPosition(),
|
|
2652
|
+
color: this.clxTooltipColor(),
|
|
2653
|
+
size: this.clxTooltipSize(),
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
_hide() {
|
|
2657
|
+
this._tooltip.hide(this._el.nativeElement);
|
|
2658
|
+
}
|
|
2648
2659
|
_clearShow() {
|
|
2649
2660
|
if (this._showTimer !== null) {
|
|
2650
2661
|
clearTimeout(this._showTimer);
|
|
@@ -6114,12 +6125,11 @@ class ClxColorPickerComponent {
|
|
|
6114
6125
|
<div class="absolute inset-0" [style.background-color]="_hueBackground()"></div>
|
|
6115
6126
|
<div class="absolute inset-0" style="background: linear-gradient(to right, #ffffff, transparent)"></div>
|
|
6116
6127
|
<div class="absolute inset-0" style="background: linear-gradient(to top, #000000, transparent)"></div>
|
|
6117
|
-
<!-- Cursor
|
|
6128
|
+
<!-- Cursor — positioned via transform (compositor-only) instead of left/top so
|
|
6129
|
+
every pointermove during a drag doesn't force a layout recalculation. -->
|
|
6118
6130
|
<div
|
|
6119
|
-
class="absolute w-3.5 h-3.5 rounded-full border-2 border-white shadow ring-1 ring-black/20 pointer-events-none"
|
|
6120
|
-
[style.
|
|
6121
|
-
[style.top.%]="(1 - _brightness()) * 100"
|
|
6122
|
-
style="transform: translate(-50%, -50%)">
|
|
6131
|
+
class="absolute top-0 left-0 w-3.5 h-3.5 rounded-full border-2 border-white shadow ring-1 ring-black/20 pointer-events-none"
|
|
6132
|
+
[style.transform]="'translate(calc(' + (_saturation() * 100) + '% - 50%), calc(' + ((1 - _brightness()) * 100) + '% - 50%))'">
|
|
6123
6133
|
</div>
|
|
6124
6134
|
</div>
|
|
6125
6135
|
|
|
@@ -6132,10 +6142,9 @@ class ClxColorPickerComponent {
|
|
|
6132
6142
|
style="background: linear-gradient(to right,#ff0000,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000)">
|
|
6133
6143
|
</div>
|
|
6134
6144
|
<div
|
|
6135
|
-
class="absolute top-1/2 w-4 h-4 rounded-full border-2 border-white shadow ring-1 ring-black/20 pointer-events-none"
|
|
6136
|
-
[style.
|
|
6137
|
-
[style.background-color]="_hueBackground()"
|
|
6138
|
-
style="transform: translate(-50%, -50%)">
|
|
6145
|
+
class="absolute top-1/2 left-0 w-4 h-4 rounded-full border-2 border-white shadow ring-1 ring-black/20 pointer-events-none"
|
|
6146
|
+
[style.transform]="'translate(calc(' + ((_hue() / 360) * 100) + '% - 50%), -50%)'"
|
|
6147
|
+
[style.background-color]="_hueBackground()">
|
|
6139
6148
|
</div>
|
|
6140
6149
|
</div>
|
|
6141
6150
|
|
|
@@ -6283,12 +6292,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
6283
6292
|
<div class="absolute inset-0" [style.background-color]="_hueBackground()"></div>
|
|
6284
6293
|
<div class="absolute inset-0" style="background: linear-gradient(to right, #ffffff, transparent)"></div>
|
|
6285
6294
|
<div class="absolute inset-0" style="background: linear-gradient(to top, #000000, transparent)"></div>
|
|
6286
|
-
<!-- Cursor
|
|
6295
|
+
<!-- Cursor — positioned via transform (compositor-only) instead of left/top so
|
|
6296
|
+
every pointermove during a drag doesn't force a layout recalculation. -->
|
|
6287
6297
|
<div
|
|
6288
|
-
class="absolute w-3.5 h-3.5 rounded-full border-2 border-white shadow ring-1 ring-black/20 pointer-events-none"
|
|
6289
|
-
[style.
|
|
6290
|
-
[style.top.%]="(1 - _brightness()) * 100"
|
|
6291
|
-
style="transform: translate(-50%, -50%)">
|
|
6298
|
+
class="absolute top-0 left-0 w-3.5 h-3.5 rounded-full border-2 border-white shadow ring-1 ring-black/20 pointer-events-none"
|
|
6299
|
+
[style.transform]="'translate(calc(' + (_saturation() * 100) + '% - 50%), calc(' + ((1 - _brightness()) * 100) + '% - 50%))'">
|
|
6292
6300
|
</div>
|
|
6293
6301
|
</div>
|
|
6294
6302
|
|
|
@@ -6301,10 +6309,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
6301
6309
|
style="background: linear-gradient(to right,#ff0000,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000)">
|
|
6302
6310
|
</div>
|
|
6303
6311
|
<div
|
|
6304
|
-
class="absolute top-1/2 w-4 h-4 rounded-full border-2 border-white shadow ring-1 ring-black/20 pointer-events-none"
|
|
6305
|
-
[style.
|
|
6306
|
-
[style.background-color]="_hueBackground()"
|
|
6307
|
-
style="transform: translate(-50%, -50%)">
|
|
6312
|
+
class="absolute top-1/2 left-0 w-4 h-4 rounded-full border-2 border-white shadow ring-1 ring-black/20 pointer-events-none"
|
|
6313
|
+
[style.transform]="'translate(calc(' + ((_hue() / 360) * 100) + '% - 50%), -50%)'"
|
|
6314
|
+
[style.background-color]="_hueBackground()">
|
|
6308
6315
|
</div>
|
|
6309
6316
|
</div>
|
|
6310
6317
|
|
|
@@ -9399,6 +9406,9 @@ class ClxAppLayoutComponent {
|
|
|
9399
9406
|
collapsed = signal(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
|
|
9400
9407
|
_hovered = signal(false, ...(ngDevMode ? [{ debugName: "_hovered" }] : /* istanbul ignore next */ []));
|
|
9401
9408
|
_isMobile = signal(true, ...(ngDevMode ? [{ debugName: "_isMobile" }] : /* istanbul ignore next */ []));
|
|
9409
|
+
/** True only for the duration of an explicit toggle/hover transition — see _sidebarCls(). */
|
|
9410
|
+
_animating = signal(false, ...(ngDevMode ? [{ debugName: "_animating" }] : /* istanbul ignore next */ []));
|
|
9411
|
+
_animatingTimer;
|
|
9402
9412
|
collapsedChange = output();
|
|
9403
9413
|
expandedChange = output();
|
|
9404
9414
|
constructor() {
|
|
@@ -9422,12 +9432,28 @@ class ClxAppLayoutComponent {
|
|
|
9422
9432
|
width = 'w-72';
|
|
9423
9433
|
}
|
|
9424
9434
|
const isOverlay = isDesktopCollapsed && hovered;
|
|
9435
|
+
// `width` only transitions while `_animating` is true (set for the duration of an explicit
|
|
9436
|
+
// collapse/expand/hover toggle — see _withTransition()). Left permanently on, this transition
|
|
9437
|
+
// would also animate every layout pass a native window resize causes (the sidebar's computed
|
|
9438
|
+
// width changes as a flex/fixed side-effect of the viewport reflow even when its Tailwind
|
|
9439
|
+
// width class hasn't), turning a single instant reflow into a 300ms interpolated one and
|
|
9440
|
+
// dragging the rest of the flex-sibling content along with it — the classic "resize lags,
|
|
9441
|
+
// catches up after you stop" symptom.
|
|
9442
|
+
const transition = this._animating()
|
|
9443
|
+
? 'transition-[width,transform] duration-300 ease-in-out lg:transition-[width] will-change-[width]'
|
|
9444
|
+
: 'transition-none';
|
|
9425
9445
|
return [
|
|
9426
9446
|
`fixed inset-y-0 left-0 z-30 ${width} flex flex-col`,
|
|
9447
|
+
// `contain: layout paint` (via arbitrary Tailwind classes below) scopes the reflow/repaint
|
|
9448
|
+
// the width transition forces to the sidebar's own subtree instead of letting the browser
|
|
9449
|
+
// consider it a candidate to affect the rest of the document — the sidebar is `fixed` so it
|
|
9450
|
+
// was never going to move sibling content either way, but without containment the engine
|
|
9451
|
+
// still walks a wider invalidation region on every animation frame.
|
|
9452
|
+
'[contain:layout_paint]',
|
|
9427
9453
|
'bg-clx-surface border-r border-clx-border overflow-hidden shrink-0',
|
|
9428
|
-
|
|
9454
|
+
transition,
|
|
9429
9455
|
isOverlay ? 'shadow-2xl' : '',
|
|
9430
|
-
'lg:
|
|
9456
|
+
'lg:relative lg:inset-auto lg:z-30 lg:translate-x-0',
|
|
9431
9457
|
(!this._isMobile() || this._sidebarOpen()) ? 'translate-x-0' : '-translate-x-full',
|
|
9432
9458
|
].join(' ');
|
|
9433
9459
|
}, ...(ngDevMode ? [{ debugName: "_sidebarCls" }] : /* istanbul ignore next */ []));
|
|
@@ -9458,23 +9484,38 @@ class ClxAppLayoutComponent {
|
|
|
9458
9484
|
}
|
|
9459
9485
|
ngOnDestroy() {
|
|
9460
9486
|
this._mql?.removeEventListener('change', this._mqlListener);
|
|
9487
|
+
clearTimeout(this._animatingTimer);
|
|
9488
|
+
}
|
|
9489
|
+
/** Arms the width/transform transition for one toggle's duration, then switches it back off
|
|
9490
|
+
* so a later window resize reflow doesn't get caught by a live CSS transition on `width`. */
|
|
9491
|
+
_withTransition(fn) {
|
|
9492
|
+
this._animating.set(true);
|
|
9493
|
+
fn();
|
|
9494
|
+
clearTimeout(this._animatingTimer);
|
|
9495
|
+
this._animatingTimer = setTimeout(() => this._animating.set(false), 320);
|
|
9461
9496
|
}
|
|
9462
9497
|
_toggleCollapsed() {
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9498
|
+
this._withTransition(() => {
|
|
9499
|
+
const next = !this.collapsed();
|
|
9500
|
+
this.collapsed.set(next);
|
|
9501
|
+
this._hovered.set(false);
|
|
9502
|
+
this.collapsedChange.emit(next);
|
|
9503
|
+
});
|
|
9467
9504
|
}
|
|
9468
9505
|
_onSidebarEnter() {
|
|
9469
9506
|
if (this.collapsed() && !this._isMobile()) {
|
|
9470
|
-
this.
|
|
9471
|
-
|
|
9507
|
+
this._withTransition(() => {
|
|
9508
|
+
this._hovered.set(true);
|
|
9509
|
+
this.expandedChange.emit(true);
|
|
9510
|
+
});
|
|
9472
9511
|
}
|
|
9473
9512
|
}
|
|
9474
9513
|
_onSidebarLeave() {
|
|
9475
9514
|
if (this._hovered()) {
|
|
9476
|
-
this.
|
|
9477
|
-
|
|
9515
|
+
this._withTransition(() => {
|
|
9516
|
+
this._hovered.set(false);
|
|
9517
|
+
this.expandedChange.emit(false);
|
|
9518
|
+
});
|
|
9478
9519
|
}
|
|
9479
9520
|
}
|
|
9480
9521
|
toggleSidebar() { this._sidebarOpen.update(v => !v); }
|
|
@@ -10016,9 +10057,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
10016
10057
|
|
|
10017
10058
|
/** Optional replacement for the auto-generated `<h1>` — project this when the title needs
|
|
10018
10059
|
* inline content next to it (a status badge, a monospace order number, extra spans) that a
|
|
10019
|
-
* plain `
|
|
10020
|
-
* this is present; ClxPageHeaderComponent's own text-2xl/font-weight/truncate
|
|
10021
|
-
* NOT applied automatically to projected content — style the projected `<h1>` yourself. */
|
|
10060
|
+
* plain `headerTitle` string input can't express. The `headerTitle`/`subtitle` inputs are
|
|
10061
|
+
* ignored when this is present; ClxPageHeaderComponent's own text-2xl/font-weight/truncate
|
|
10062
|
+
* styling is NOT applied automatically to projected content — style the projected `<h1>` yourself. */
|
|
10022
10063
|
class ClxPageHeaderTitleDirective {
|
|
10023
10064
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxPageHeaderTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
10024
10065
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.15", type: ClxPageHeaderTitleDirective, isStandalone: true, selector: "[clxPageHeaderTitle]", ngImport: i0 });
|
|
@@ -10042,12 +10083,17 @@ const TEXT_SIZE_CLASS = {
|
|
|
10042
10083
|
* hand-rolled `<h1 class="text-2xl font-semibold">` + `<p>` pattern repeated across every page.
|
|
10043
10084
|
*
|
|
10044
10085
|
* For detail pages whose title needs inline content (a status badge, a monospace order
|
|
10045
|
-
* number) that a plain `
|
|
10046
|
-
* block instead of using the `
|
|
10086
|
+
* number) that a plain `headerTitle` string can't express, project a `[clxPageHeaderTitle]`
|
|
10087
|
+
* block instead of using the `headerTitle`/`subtitle` inputs.
|
|
10047
10088
|
*/
|
|
10048
10089
|
class ClxPageHeaderComponent {
|
|
10049
10090
|
_themeSvc = inject(ClxThemeService);
|
|
10050
|
-
|
|
10091
|
+
// Named `headerTitle` (not `title`) deliberately — `title` is also a native HTML attribute,
|
|
10092
|
+
// and Angular doesn't strip it from the host element just because it's also bound to an
|
|
10093
|
+
// @Input(). `<clx-page-header title="X">` used to leave a real `title="X"` attribute on the
|
|
10094
|
+
// component's host, which the browser reads as a native tooltip — every page header showed
|
|
10095
|
+
// a redundant browser tooltip repeating its own title on hover.
|
|
10096
|
+
headerTitle = input('', ...(ngDevMode ? [{ debugName: "headerTitle" }] : /* istanbul ignore next */ []));
|
|
10051
10097
|
subtitle = input(undefined, ...(ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []));
|
|
10052
10098
|
/** Set to show the back button — `[showBack]="true" (back)="goBack()"`. */
|
|
10053
10099
|
showBack = input(false, ...(ngDevMode ? [{ debugName: "showBack" }] : /* istanbul ignore next */ []));
|
|
@@ -10070,7 +10116,7 @@ class ClxPageHeaderComponent {
|
|
|
10070
10116
|
return `${TEXT_SIZE_CLASS[s.size]} font-[${s.weight}] ${colorCls} mt-0.5`;
|
|
10071
10117
|
}, ...(ngDevMode ? [{ debugName: "subtitleClass" }] : /* istanbul ignore next */ []));
|
|
10072
10118
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxPageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10073
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxPageHeaderComponent, isStandalone: true, selector: "clx-page-header", inputs: {
|
|
10119
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxPageHeaderComponent, isStandalone: true, selector: "clx-page-header", inputs: { headerTitle: { classPropertyName: "headerTitle", publicName: "headerTitle", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, showBack: { classPropertyName: "showBack", publicName: "showBack", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { back: "back" }, host: { classAttribute: "flex items-center gap-3 flex-wrap" }, queries: [{ propertyName: "_titleSlot", first: true, predicate: ClxPageHeaderTitleDirective, descendants: true, isSignal: true }], exportAs: ["clxPageHeader"], ngImport: i0, template: `
|
|
10074
10120
|
@if (showBack()) {
|
|
10075
10121
|
<button clx-button variant="ghost" [color]="_color()" icon="arrow_back" [iconOnly]="true"
|
|
10076
10122
|
(click)="back.emit()"></button>
|
|
@@ -10083,7 +10129,7 @@ class ClxPageHeaderComponent {
|
|
|
10083
10129
|
</div>
|
|
10084
10130
|
} @else {
|
|
10085
10131
|
<div class="min-w-0">
|
|
10086
|
-
<h1 [class]="titleClass()">{{
|
|
10132
|
+
<h1 [class]="titleClass()">{{ headerTitle() }}</h1>
|
|
10087
10133
|
@if (subtitle()) {
|
|
10088
10134
|
<p [class]="subtitleClass()">{{ subtitle() }}</p>
|
|
10089
10135
|
}
|
|
@@ -10116,7 +10162,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
10116
10162
|
</div>
|
|
10117
10163
|
} @else {
|
|
10118
10164
|
<div class="min-w-0">
|
|
10119
|
-
<h1 [class]="titleClass()">{{
|
|
10165
|
+
<h1 [class]="titleClass()">{{ headerTitle() }}</h1>
|
|
10120
10166
|
@if (subtitle()) {
|
|
10121
10167
|
<p [class]="subtitleClass()">{{ subtitle() }}</p>
|
|
10122
10168
|
}
|
|
@@ -10126,7 +10172,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
10126
10172
|
</div>
|
|
10127
10173
|
`,
|
|
10128
10174
|
}]
|
|
10129
|
-
}], propDecorators: {
|
|
10175
|
+
}], propDecorators: { headerTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerTitle", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], showBack: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBack", required: false }] }], back: [{ type: i0.Output, args: ["back"] }], _titleSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ClxPageHeaderTitleDirective), { isSignal: true }] }] } });
|
|
10130
10176
|
|
|
10131
10177
|
class ClxProfileComponent {
|
|
10132
10178
|
// ── Inputs ──────────────────────────────────────────────────────────────────
|
|
@@ -10726,9 +10772,9 @@ class ClxAlertComponent {
|
|
|
10726
10772
|
@if (_timerTotal() > 0) {
|
|
10727
10773
|
<div class="absolute bottom-0 left-0 right-0 h-1 bg-clx-surface-2 rounded-b-3xl overflow-hidden">
|
|
10728
10774
|
<div
|
|
10729
|
-
class="h-full transition-none rounded-b-3xl"
|
|
10775
|
+
class="h-full w-full origin-left transition-none rounded-b-3xl"
|
|
10730
10776
|
[class]="_progressColor()"
|
|
10731
|
-
[ngStyle]="{
|
|
10777
|
+
[ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }">
|
|
10732
10778
|
</div>
|
|
10733
10779
|
</div>
|
|
10734
10780
|
}
|
|
@@ -10798,9 +10844,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
10798
10844
|
@if (_timerTotal() > 0) {
|
|
10799
10845
|
<div class="absolute bottom-0 left-0 right-0 h-1 bg-clx-surface-2 rounded-b-3xl overflow-hidden">
|
|
10800
10846
|
<div
|
|
10801
|
-
class="h-full transition-none rounded-b-3xl"
|
|
10847
|
+
class="h-full w-full origin-left transition-none rounded-b-3xl"
|
|
10802
10848
|
[class]="_progressColor()"
|
|
10803
|
-
[ngStyle]="{
|
|
10849
|
+
[ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }">
|
|
10804
10850
|
</div>
|
|
10805
10851
|
</div>
|
|
10806
10852
|
}
|
|
@@ -11099,9 +11145,9 @@ class ClxToastComponent {
|
|
|
11099
11145
|
@if (entry().showProgress && entry().duration > 0) {
|
|
11100
11146
|
<div class="absolute bottom-0 left-0 right-0 h-0.5 bg-clx-surface-2">
|
|
11101
11147
|
<div
|
|
11102
|
-
class="h-full transition-none"
|
|
11148
|
+
class="h-full w-full origin-left transition-none"
|
|
11103
11149
|
[class]="_progressColor()"
|
|
11104
|
-
[ngStyle]="{
|
|
11150
|
+
[ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }"
|
|
11105
11151
|
></div>
|
|
11106
11152
|
</div>
|
|
11107
11153
|
}
|
|
@@ -11170,9 +11216,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
11170
11216
|
@if (entry().showProgress && entry().duration > 0) {
|
|
11171
11217
|
<div class="absolute bottom-0 left-0 right-0 h-0.5 bg-clx-surface-2">
|
|
11172
11218
|
<div
|
|
11173
|
-
class="h-full transition-none"
|
|
11219
|
+
class="h-full w-full origin-left transition-none"
|
|
11174
11220
|
[class]="_progressColor()"
|
|
11175
|
-
[ngStyle]="{
|
|
11221
|
+
[ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }"
|
|
11176
11222
|
></div>
|
|
11177
11223
|
</div>
|
|
11178
11224
|
}
|