codexly-ui 0.8.3 → 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.
@@ -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, Renderer2, ApplicationRef, EnvironmentInjector, DestroyRef, createComponent, untracked, HostListener, viewChild, ViewChild, Injector, ChangeDetectorRef, NgZone, model, contentChildren, ContentChildren, ViewChildren, numberAttribute, booleanAttribute, Input } from '@angular/core';
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
- // Fuerza reflow para que el browser registre la eliminación de clases
2357
- // antes de volver a agregarlas — necesario para reiniciar la animación.
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
- class ClxTooltipDirective {
2433
- // ── Inputs ───────────────────────────────────────────────────────────────
2434
- clxTooltip = input.required(...(ngDevMode ? [{ debugName: "clxTooltip" }] : /* istanbul ignore next */ []));
2435
- clxTooltipPosition = input('top', ...(ngDevMode ? [{ debugName: "clxTooltipPosition" }] : /* istanbul ignore next */ []));
2436
- clxTooltipColor = input('slate', ...(ngDevMode ? [{ debugName: "clxTooltipColor" }] : /* istanbul ignore next */ []));
2437
- clxTooltipSize = input('md', ...(ngDevMode ? [{ debugName: "clxTooltipSize" }] : /* istanbul ignore next */ []));
2438
- clxTooltipDelay = input(0, ...(ngDevMode ? [{ debugName: "clxTooltipDelay" }] : /* istanbul ignore next */ []));
2439
- // ── DI ───────────────────────────────────────────────────────────────────
2440
- _el = inject(ElementRef);
2441
- _renderer = inject(Renderer2);
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
- _destroyRef = inject(DestroyRef);
2447
- // ── State ────────────────────────────────────────────────────────────────
2452
+ _renderer;
2453
+ _isBrowser;
2448
2454
  _ref = null;
2449
- _pendingRef = null;
2450
- _showTimer = null;
2451
- _hideTimer = null;
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
- if (!isPlatformBrowser(this._platformId))
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
- const host = this._el.nativeElement;
2459
- const { signal } = this._abort;
2460
- host.addEventListener('mouseenter', () => this._scheduleShow(), { signal });
2461
- host.addEventListener('mouseleave', () => this._scheduleHide(), { signal });
2462
- host.addEventListener('focus', () => this._scheduleShow(), { signal });
2463
- host.addEventListener('blur', () => this._scheduleHide(), { signal });
2464
- // Touch devices fire 'mouseenter'/'focus' on tap but never a real
2465
- // 'mouseleave' dismiss immediately (no fade) so the bubble can't
2466
- // outlive the tap and float over whatever it triggered (e.g. a modal
2467
- // opening on the same gesture). Capture-phase 'click' fires before the
2468
- // host's own (click) handler — the one that actually opens the modal —
2469
- // so the bubble is gone from the DOM before the modal is created.
2470
- host.addEventListener('touchend', () => this._hideImmediate(), { signal });
2471
- host.addEventListener('touchcancel', () => this._hideImmediate(), { signal });
2472
- host.addEventListener('click', () => this._hideImmediate(), { signal, capture: true });
2473
- this._destroyRef.onDestroy(() => {
2474
- this._destroyed = true;
2475
- this._abort.abort();
2476
- this._clearTimers();
2477
- this._forceDestroyBubble();
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
- // ── Show / Hide ──────────────────────────────────────────────────────────
2481
- _scheduleShow() {
2482
- this._clearHide();
2483
- const delay = this.clxTooltipDelay();
2484
- if (delay > 0) {
2485
- this._showTimer = setTimeout(() => this._show(), delay);
2486
- }
2487
- else {
2488
- this._show();
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
- _scheduleHide() {
2492
- this._clearShow();
2493
- this._hideTimer = setTimeout(() => this._hide(), 80);
2519
+ get _tooltipId() {
2520
+ return 'clx-tooltip-shared';
2494
2521
  }
2495
- _show() {
2496
- if (this._ref)
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
- // Aria
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(el) {
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
- // ── Helpers ──────────────────────────────────────────────────────────────
2626
- _destroyBubble() {
2627
- if (!this._ref)
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._appRef.detachView(this._ref.hostView);
2630
- this._ref.destroy();
2631
- this._ref = null;
2632
- this._el.nativeElement.removeAttribute('aria-describedby');
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
- _forceDestroyBubble() {
2635
- if (this._ref) {
2636
- const el = this._ref.location.nativeElement;
2637
- this._animate.stop(el);
2638
- this._destroyBubble();
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
- if (this._pendingRef) {
2641
- const el = this._pendingRef.location.nativeElement;
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.left.%]="_saturation() * 100"
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.left.%]="(_hue() / 360) * 100"
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.left.%]="_saturation() * 100"
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.left.%]="(_hue() / 360) * 100"
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
 
@@ -9433,10 +9440,16 @@ class ClxAppLayoutComponent {
9433
9440
  // dragging the rest of the flex-sibling content along with it — the classic "resize lags,
9434
9441
  // catches up after you stop" symptom.
9435
9442
  const transition = this._animating()
9436
- ? 'transition-[width,transform] duration-300 ease-in-out lg:transition-[width]'
9443
+ ? 'transition-[width,transform] duration-300 ease-in-out lg:transition-[width] will-change-[width]'
9437
9444
  : 'transition-none';
9438
9445
  return [
9439
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]',
9440
9453
  'bg-clx-surface border-r border-clx-border overflow-hidden shrink-0',
9441
9454
  transition,
9442
9455
  isOverlay ? 'shadow-2xl' : '',
@@ -10044,9 +10057,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
10044
10057
 
10045
10058
  /** Optional replacement for the auto-generated `<h1>` — project this when the title needs
10046
10059
  * inline content next to it (a status badge, a monospace order number, extra spans) that a
10047
- * plain `title` string input can't express. The `title`/`subtitle` inputs are ignored when
10048
- * this is present; ClxPageHeaderComponent's own text-2xl/font-weight/truncate styling is
10049
- * 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. */
10050
10063
  class ClxPageHeaderTitleDirective {
10051
10064
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxPageHeaderTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10052
10065
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.15", type: ClxPageHeaderTitleDirective, isStandalone: true, selector: "[clxPageHeaderTitle]", ngImport: i0 });
@@ -10070,12 +10083,17 @@ const TEXT_SIZE_CLASS = {
10070
10083
  * hand-rolled `<h1 class="text-2xl font-semibold">` + `<p>` pattern repeated across every page.
10071
10084
  *
10072
10085
  * For detail pages whose title needs inline content (a status badge, a monospace order
10073
- * number) that a plain `title` string can't express, project a `[clxPageHeaderTitle]`
10074
- * block instead of using the `title`/`subtitle` inputs.
10086
+ * number) that a plain `headerTitle` string can't express, project a `[clxPageHeaderTitle]`
10087
+ * block instead of using the `headerTitle`/`subtitle` inputs.
10075
10088
  */
10076
10089
  class ClxPageHeaderComponent {
10077
10090
  _themeSvc = inject(ClxThemeService);
10078
- title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
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 */ []));
10079
10097
  subtitle = input(undefined, ...(ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []));
10080
10098
  /** Set to show the back button — `[showBack]="true" (back)="goBack()"`. */
10081
10099
  showBack = input(false, ...(ngDevMode ? [{ debugName: "showBack" }] : /* istanbul ignore next */ []));
@@ -10098,7 +10116,7 @@ class ClxPageHeaderComponent {
10098
10116
  return `${TEXT_SIZE_CLASS[s.size]} font-[${s.weight}] ${colorCls} mt-0.5`;
10099
10117
  }, ...(ngDevMode ? [{ debugName: "subtitleClass" }] : /* istanbul ignore next */ []));
10100
10118
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxPageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10101
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxPageHeaderComponent, isStandalone: true, selector: "clx-page-header", inputs: { title: { classPropertyName: "title", publicName: "title", 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: `
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: `
10102
10120
  @if (showBack()) {
10103
10121
  <button clx-button variant="ghost" [color]="_color()" icon="arrow_back" [iconOnly]="true"
10104
10122
  (click)="back.emit()"></button>
@@ -10111,7 +10129,7 @@ class ClxPageHeaderComponent {
10111
10129
  </div>
10112
10130
  } @else {
10113
10131
  <div class="min-w-0">
10114
- <h1 [class]="titleClass()">{{ title() }}</h1>
10132
+ <h1 [class]="titleClass()">{{ headerTitle() }}</h1>
10115
10133
  @if (subtitle()) {
10116
10134
  <p [class]="subtitleClass()">{{ subtitle() }}</p>
10117
10135
  }
@@ -10144,7 +10162,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
10144
10162
  </div>
10145
10163
  } @else {
10146
10164
  <div class="min-w-0">
10147
- <h1 [class]="titleClass()">{{ title() }}</h1>
10165
+ <h1 [class]="titleClass()">{{ headerTitle() }}</h1>
10148
10166
  @if (subtitle()) {
10149
10167
  <p [class]="subtitleClass()">{{ subtitle() }}</p>
10150
10168
  }
@@ -10154,7 +10172,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
10154
10172
  </div>
10155
10173
  `,
10156
10174
  }]
10157
- }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", 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 }] }] } });
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 }] }] } });
10158
10176
 
10159
10177
  class ClxProfileComponent {
10160
10178
  // ── Inputs ──────────────────────────────────────────────────────────────────
@@ -10754,9 +10772,9 @@ class ClxAlertComponent {
10754
10772
  @if (_timerTotal() > 0) {
10755
10773
  <div class="absolute bottom-0 left-0 right-0 h-1 bg-clx-surface-2 rounded-b-3xl overflow-hidden">
10756
10774
  <div
10757
- class="h-full transition-none rounded-b-3xl"
10775
+ class="h-full w-full origin-left transition-none rounded-b-3xl"
10758
10776
  [class]="_progressColor()"
10759
- [ngStyle]="{ width: _progressPct() + '%' }">
10777
+ [ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }">
10760
10778
  </div>
10761
10779
  </div>
10762
10780
  }
@@ -10826,9 +10844,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
10826
10844
  @if (_timerTotal() > 0) {
10827
10845
  <div class="absolute bottom-0 left-0 right-0 h-1 bg-clx-surface-2 rounded-b-3xl overflow-hidden">
10828
10846
  <div
10829
- class="h-full transition-none rounded-b-3xl"
10847
+ class="h-full w-full origin-left transition-none rounded-b-3xl"
10830
10848
  [class]="_progressColor()"
10831
- [ngStyle]="{ width: _progressPct() + '%' }">
10849
+ [ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }">
10832
10850
  </div>
10833
10851
  </div>
10834
10852
  }
@@ -11127,9 +11145,9 @@ class ClxToastComponent {
11127
11145
  @if (entry().showProgress && entry().duration > 0) {
11128
11146
  <div class="absolute bottom-0 left-0 right-0 h-0.5 bg-clx-surface-2">
11129
11147
  <div
11130
- class="h-full transition-none"
11148
+ class="h-full w-full origin-left transition-none"
11131
11149
  [class]="_progressColor()"
11132
- [ngStyle]="{ width: _progressPct() + '%' }"
11150
+ [ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }"
11133
11151
  ></div>
11134
11152
  </div>
11135
11153
  }
@@ -11198,9 +11216,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
11198
11216
  @if (entry().showProgress && entry().duration > 0) {
11199
11217
  <div class="absolute bottom-0 left-0 right-0 h-0.5 bg-clx-surface-2">
11200
11218
  <div
11201
- class="h-full transition-none"
11219
+ class="h-full w-full origin-left transition-none"
11202
11220
  [class]="_progressColor()"
11203
- [ngStyle]="{ width: _progressPct() + '%' }"
11221
+ [ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }"
11204
11222
  ></div>
11205
11223
  </div>
11206
11224
  }