@ethlete/core 5.0.0-next.26 → 5.0.0-next.28

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,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, ElementRef, Directive, assertInInjectionContext, DestroyRef, TemplateRef, QueryList, NgZone, signal, isSignal, untracked, DOCUMENT, linkedSignal, isDevMode, computed, afterNextRender, effect, EnvironmentInjector, runInInjectionContext, Injector, booleanAttribute, numberAttribute, RendererStyleFlags2, PLATFORM_ID, ApplicationRef, RendererFactory2, createComponent, model, ViewContainerRef, input, output, Pipe, ViewEncapsulation, Component } from '@angular/core';
2
+ import { InjectionToken, inject, ElementRef, Directive, assertInInjectionContext, DestroyRef, TemplateRef, QueryList, RendererFactory2, RendererStyleFlags2, isSignal, DOCUMENT, signal, linkedSignal, isDevMode, computed, afterNextRender, effect, NgZone, untracked, EnvironmentInjector, runInInjectionContext, Injector, booleanAttribute, numberAttribute, PLATFORM_ID, ApplicationRef, createComponent, model, ViewContainerRef, input, output, Pipe, ViewEncapsulation, Component } from '@angular/core';
3
3
  import { takeUntilDestroyed, toSignal, toObservable, outputFromObservable } from '@angular/core/rxjs-interop';
4
- import { Subject, BehaviorSubject, debounceTime, combineLatest, map, merge, fromEvent, filter, tap, switchMap, startWith, of, timer, takeUntil, pairwise, distinctUntilChanged, take, Observable, concatMap, EMPTY, concat, defer, exhaustMap, share } from 'rxjs';
4
+ import { Subject, BehaviorSubject, debounceTime, combineLatest, map, merge, fromEvent, filter, tap, switchMap, startWith, of, defer, from, catchError, timer, takeUntil, pairwise, distinctUntilChanged, take, Observable, concatMap, EMPTY, concat, exhaustMap, share } from 'rxjs';
5
5
  import { FormGroup, FormArray, FormControl } from '@angular/forms';
6
6
  import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals';
7
- import { BreakpointObserver } from '@angular/cdk/layout';
8
7
  import { coerceElement } from '@angular/cdk/coercion';
9
8
  import { DOCUMENT as DOCUMENT$1, isPlatformBrowser } from '@angular/common';
9
+ import { BreakpointObserver } from '@angular/cdk/layout';
10
10
  import { Router, NavigationEnd, NavigationSkipped } from '@angular/router';
11
11
  import { Overlay } from '@angular/cdk/overlay';
12
12
  import { ComponentPortal } from '@angular/cdk/portal';
@@ -132,6 +132,29 @@ const createComponentId = (prefix) => {
132
132
  componentIds.set(prefix, id + 1);
133
133
  return `${prefix}-${id}`;
134
134
  };
135
+ let randomIdFallbackCounter = 0;
136
+ /**
137
+ * A unique id string. Uses `crypto.randomUUID()` when available, but falls back gracefully when it is
138
+ * not — crucially in **insecure contexts** (a plain-HTTP page served from a LAN IP, not `localhost`),
139
+ * where `crypto.randomUUID` is `undefined`. Calling it there throws, so anything using it during e.g.
140
+ * an overlay/menu open would break on such hosts. Prefer this over `crypto.randomUUID()` directly.
141
+ */
142
+ const randomId = () => {
143
+ const cryptoObj = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
144
+ if (typeof cryptoObj?.randomUUID === 'function') {
145
+ return cryptoObj.randomUUID();
146
+ }
147
+ // `getRandomValues` is available even in insecure contexts — build an RFC-4122-ish v4 id from it.
148
+ if (typeof cryptoObj?.getRandomValues === 'function') {
149
+ const bytes = Array.from(cryptoObj.getRandomValues(new Uint8Array(16)));
150
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
151
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
152
+ const hex = bytes.map((byte) => (byte ?? 0).toString(16).padStart(2, '0'));
153
+ return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;
154
+ }
155
+ // last resort: no Web Crypto at all — a session-unique (not cryptographic) id is enough for DOM ids
156
+ return `et-id-${(randomIdFallbackCounter++).toString(36)}-${Date.now().toString(36)}`;
157
+ };
135
158
 
136
159
  const createDestroy = () => {
137
160
  assertInInjectionContext(createDestroy);
@@ -360,6 +383,49 @@ const Validators = {
360
383
  ValidateAtLeastOneRequired,
361
384
  };
362
385
 
386
+ const copyToClipboard = (text) => defer(() => {
387
+ if (typeof navigator === 'undefined' || typeof document === 'undefined') {
388
+ return of(false);
389
+ }
390
+ if (navigator.clipboard) {
391
+ return from(navigator.clipboard.writeText(text)).pipe(map(() => true),
392
+ // The async Clipboard API can be blocked (missing permission, insecure context) — try the legacy path.
393
+ catchError(() => of(copyToClipboardViaExecCommand(text))));
394
+ }
395
+ return of(copyToClipboardViaExecCommand(text));
396
+ });
397
+ const readFromClipboard = () => defer(() => {
398
+ if (typeof navigator === 'undefined' || !navigator.clipboard) {
399
+ return of(null);
400
+ }
401
+ return from(navigator.clipboard.readText()).pipe(catchError(() => of(null)));
402
+ });
403
+ const copyToClipboardViaExecCommand = (text) => {
404
+ const previouslyFocusedElement = document.activeElement;
405
+ const textarea = document.createElement('textarea');
406
+ textarea.value = text;
407
+ textarea.setAttribute('readonly', '');
408
+ textarea.setAttribute('aria-hidden', 'true');
409
+ textarea.style.position = 'fixed';
410
+ textarea.style.top = '-9999px';
411
+ textarea.style.opacity = '0';
412
+ document.body.appendChild(textarea);
413
+ textarea.select();
414
+ textarea.setSelectionRange(0, text.length);
415
+ let didCopy = false;
416
+ try {
417
+ didCopy = document.execCommand('copy');
418
+ }
419
+ catch {
420
+ // execCommand throws in some environments instead of returning false.
421
+ }
422
+ textarea.remove();
423
+ if (previouslyFocusedElement instanceof HTMLElement) {
424
+ previouslyFocusedElement.focus();
425
+ }
426
+ return didCopy;
427
+ };
428
+
363
429
  /* eslint-disable @typescript-eslint/no-explicit-any */
364
430
  /* eslint-disable no-var */
365
431
  /**
@@ -641,179 +707,189 @@ class KeyPressManager {
641
707
  }
642
708
  }
643
709
 
644
- /**
645
- * Creates a signal that animates the transition of a numeric signal's value over time.
646
- */
647
- const signalAnimatedNumber = (source, options = {}) => {
648
- const ngZone = inject(NgZone);
649
- const duration = options.duration ?? 1000;
650
- const easing = options.easing ?? easeOut;
651
- const round = options.round ?? ((v) => Math.round(v));
652
- const initialValue = options.initialValue ?? 0;
653
- const animatedValue = signal(initialValue, /* @ts-ignore */
654
- ...(ngDevMode ? [{ debugName: "animatedValue" }] : /* istanbul ignore next */ []));
655
- let rafId = null;
656
- let currentAnimatedValue = initialValue;
657
- const cancelAnimation = () => {
658
- if (rafId !== null) {
659
- cancelAnimationFrame(rafId);
660
- rafId = null;
661
- }
710
+ const [provideRenderer, injectRenderer] = createRootProvider(() => {
711
+ const rendererFactory = inject(RendererFactory2);
712
+ const renderer = rendererFactory.createRenderer(null, null);
713
+ const addClass = (element, ...classes) => {
714
+ classes.forEach((cls) => renderer.addClass(element, cls));
662
715
  };
663
- const readonlySignal = animatedValue.asReadonly();
664
- const play = () => {
665
- const targetValue = isSignal(source) ? source() : source;
666
- cancelAnimation();
667
- const startValue = currentAnimatedValue;
668
- const startTime = performance.now();
669
- const delta = targetValue - startValue;
670
- // Skip animation if no change
671
- if (delta === 0) {
672
- options.onAnimationEnd?.();
673
- return readonlySignal;
716
+ const removeClass = (element, ...classes) => {
717
+ classes.forEach((cls) => renderer.removeClass(element, cls));
718
+ };
719
+ const toggleClass = (element, className, force) => {
720
+ const shouldAdd = force ?? !element.classList.contains(className);
721
+ if (shouldAdd) {
722
+ renderer.addClass(element, className);
674
723
  }
675
- options.onAnimationStart?.();
676
- ngZone.runOutsideAngular(() => {
677
- const animate = (timestamp) => {
678
- const elapsed = timestamp - startTime;
679
- const progress = Math.min(elapsed / duration, 1);
680
- const easedProgress = easing(progress);
681
- const value = startValue + delta * easedProgress;
682
- currentAnimatedValue = value;
683
- untracked(() => {
684
- animatedValue.set(round(value));
685
- });
686
- if (progress < 1) {
687
- rafId = requestAnimationFrame(animate);
688
- }
689
- else {
690
- rafId = null;
691
- ngZone.run(() => {
692
- options.onAnimationEnd?.();
693
- });
694
- }
695
- };
696
- rafId = requestAnimationFrame(animate);
697
- });
698
- return readonlySignal;
724
+ else {
725
+ renderer.removeClass(element, className);
726
+ }
727
+ return shouldAdd;
699
728
  };
700
- const reset = () => {
701
- cancelAnimation();
702
- currentAnimatedValue = initialValue;
703
- animatedValue.set(initialValue);
704
- return readonlySignal;
729
+ const setStyle = (element, style, flags) => {
730
+ Object.entries(style).forEach(([key, value]) => {
731
+ if (!Number.isNaN(Number(key)))
732
+ return;
733
+ if (value !== null && value !== undefined) {
734
+ renderer.setStyle(element, key, value, flags);
735
+ }
736
+ else {
737
+ renderer.removeStyle(element, key, flags);
738
+ }
739
+ });
705
740
  };
706
- const stop = () => {
707
- cancelAnimation();
708
- return readonlySignal;
741
+ const removeStyle = (element, style, flags) => {
742
+ if (!Number.isNaN(Number(style)))
743
+ return;
744
+ renderer.removeStyle(element, style, flags);
709
745
  };
710
- readonlySignal.play = play;
711
- readonlySignal.reset = reset;
712
- readonlySignal.stop = stop;
713
- return readonlySignal;
714
- };
715
- /* Linear easing function */
716
- const easeLinear = (t) => t;
717
- /* Ease in (slow start) */
718
- const easeIn = (t) => t * t;
719
- /* Ease out (slow end) */
720
- const easeOut = (t) => t * (2 - t);
721
- /* Ease in-out (slow start and end) */
722
- const easeInOut = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
723
- /** Elastic easing function (overshoots) */
724
- const easeElastic = (t) => {
725
- const c4 = (2 * Math.PI) / 3;
726
- return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
727
- };
728
- /** Back easing function (overshoots slightly then settles) */
729
- const easeOutBack = (t) => {
730
- const c1 = 1.70158;
731
- const c3 = c1 + 1;
732
- return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
733
- };
734
- /** Back easing function with more overshoot */
735
- const easeOutBackStrong = (t) => {
736
- const c1 = 2.5;
737
- const c3 = c1 + 1;
738
- return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
739
- };
740
-
741
- const BREAKPOINT_ORDER = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
742
- /**
743
- * Default viewport config based on Tailwind CSS.
744
- * @see https://tailwindcss.com/docs/screens
745
- */
746
- const DEFAULT_VIEWPORT_CONFIG = {
747
- breakpoints: {
748
- xs: [0, 639],
749
- sm: [640, 767],
750
- md: [768, 1023],
751
- lg: [1024, 1279],
752
- xl: [1280, 1535],
753
- '2xl': [1536, Infinity],
754
- },
755
- };
756
- const [provideViewportConfig, injectViewportConfig] = createStaticRootProvider(DEFAULT_VIEWPORT_CONFIG, {
757
- name: 'Viewport Config',
758
- });
759
- const [provideBreakpointObserver, injectBreakpointObserver] = createRootProvider(() => {
760
- const breakpointObserver = inject(BreakpointObserver);
761
- const viewportConfig = injectViewportConfig();
762
- const isMediaQueryMatched = (mediaQuery) => breakpointObserver.isMatched(mediaQuery);
763
- const observeMediaQuery = (mediaQuery) => {
764
- return toSignal(breakpointObserver.observe(mediaQuery).pipe(map((x) => x.matches), startWith(isMediaQueryMatched(mediaQuery))), { requireSync: true });
746
+ const removeStyles = (element, ...styles) => {
747
+ styles.forEach((style) => removeStyle(element, style));
765
748
  };
766
- const observeBreakpoint = (options) => observeMediaQuery(buildMediaQueryString(options));
767
- const isBreakpointMatched = (options) => isMediaQueryMatched(buildMediaQueryString(options));
768
- const getBreakpointSize = (type, option) => {
769
- const index = option === 'min' ? 0 : 1;
770
- const size = viewportConfig.breakpoints[type][index];
771
- if (size === Infinity || size === 0) {
772
- return size;
749
+ const setCssProperty = (element, name, value) => {
750
+ if (value !== null && value !== undefined) {
751
+ renderer.setStyle(element, name, value, RendererStyleFlags2.DashCase);
773
752
  }
774
- if (option === 'min') {
775
- return size;
753
+ else {
754
+ renderer.removeStyle(element, name, RendererStyleFlags2.DashCase);
776
755
  }
777
- // Due to scaling, the actual size of the viewport may be a decimal number.
778
- // Eg. on Windows 11 with 150% scaling, the viewport size may be 1535.33px
779
- // and thus not matching any of the default breakpoints.
780
- return size + 0.9;
781
756
  };
782
- const buildMediaQueryString = (options) => {
783
- if (!options.min && !options.max) {
784
- throw new Error('At least one of min or max must be defined');
757
+ const setCssProperties = (element, properties) => {
758
+ Object.entries(properties).forEach(([name, value]) => {
759
+ setCssProperty(element, name, value);
760
+ });
761
+ };
762
+ const setAttribute = (element, name, value, namespace) => {
763
+ if (value !== null && value !== undefined) {
764
+ renderer.setAttribute(element, name, value, namespace);
785
765
  }
786
- const mediaQueryParts = [];
787
- if (options.min) {
788
- if (typeof options.min === 'number') {
789
- mediaQueryParts.push(`(min-width: ${options.min}px)`);
790
- }
791
- else {
792
- mediaQueryParts.push(`(min-width: ${getBreakpointSize(options.min, 'min')}px)`);
793
- }
766
+ else {
767
+ renderer.removeAttribute(element, name, namespace);
794
768
  }
795
- if (options.min && options.max) {
796
- mediaQueryParts.push('and');
769
+ };
770
+ const removeAttribute = (element, ...names) => {
771
+ names.forEach((name) => renderer.removeAttribute(element, name));
772
+ };
773
+ const setProperty = (element, name, value) => {
774
+ renderer.setProperty(element, name, value);
775
+ };
776
+ const setProperties = (element, properties) => {
777
+ Object.entries(properties).forEach(([name, value]) => {
778
+ renderer.setProperty(element, name, value);
779
+ });
780
+ };
781
+ const createElement = (tagName, namespace) => {
782
+ return renderer.createElement(tagName, namespace);
783
+ };
784
+ const createText = (value) => {
785
+ return renderer.createText(value);
786
+ };
787
+ const createComment = (value) => {
788
+ return renderer.createComment(value);
789
+ };
790
+ const appendChild = (parent, newChild) => {
791
+ renderer.appendChild(parent, newChild);
792
+ };
793
+ const insertBefore = (parent, newChild, refChild) => {
794
+ renderer.insertBefore(parent, newChild, refChild);
795
+ };
796
+ const removeChild = (parent, oldChild, isHostElement) => {
797
+ renderer.removeChild(parent, oldChild, isHostElement);
798
+ };
799
+ const selectRootElement = (selectorOrNode, preserveContent) => {
800
+ return renderer.selectRootElement(selectorOrNode, preserveContent);
801
+ };
802
+ const parentNode = (node) => {
803
+ return renderer.parentNode(node);
804
+ };
805
+ const nextSibling = (node) => {
806
+ return renderer.nextSibling(node);
807
+ };
808
+ const setValue = (node, value) => {
809
+ renderer.setValue(node, value);
810
+ };
811
+ const listen = (target, eventName, callback) => {
812
+ return renderer.listen(target, eventName, callback);
813
+ };
814
+ const destroyNode = (node) => {
815
+ if (renderer.destroyNode) {
816
+ renderer.destroyNode(node);
797
817
  }
798
- if (options.max) {
799
- if (typeof options.max === 'number') {
800
- mediaQueryParts.push(`(max-width: ${options.max}px)`);
818
+ };
819
+ const replaceChild = (parent, newChild, oldChild) => {
820
+ renderer.insertBefore(parent, newChild, oldChild);
821
+ renderer.removeChild(parent, oldChild);
822
+ };
823
+ const empty = (element) => {
824
+ while (element.firstChild) {
825
+ renderer.removeChild(element, element.firstChild);
826
+ }
827
+ };
828
+ const setTextContent = (element, text) => {
829
+ renderer.setProperty(element, 'textContent', text);
830
+ };
831
+ const setInnerHTML = (element, html) => {
832
+ renderer.setProperty(element, 'innerHTML', html);
833
+ };
834
+ const setAttributes = (element, attributes, namespace) => {
835
+ Object.entries(attributes).forEach(([name, value]) => {
836
+ setAttribute(element, name, value, namespace);
837
+ });
838
+ };
839
+ const setDataAttributes = (element, data) => {
840
+ Object.entries(data).forEach(([key, value]) => {
841
+ setAttribute(element, `data-${key}`, value);
842
+ });
843
+ };
844
+ const moveBefore = (config) => {
845
+ const { newParent, child, before = null } = config;
846
+ if ('moveBefore' in newParent) {
847
+ try {
848
+ newParent.moveBefore(child, before);
801
849
  }
802
- else {
803
- mediaQueryParts.push(`(max-width: ${getBreakpointSize(options.max, 'max')}px)`);
850
+ catch {
851
+ newParent.insertBefore(child, before);
804
852
  }
805
853
  }
806
- return mediaQueryParts.join(' ');
854
+ else {
855
+ newParent.insertBefore(child, before);
856
+ }
807
857
  };
808
858
  return {
809
- observeBreakpoint,
810
- isBreakpointMatched,
811
- getBreakpointSize,
812
- buildMediaQueryString,
813
- observeMediaQuery,
814
- isMediaQueryMatched,
859
+ addClass,
860
+ removeClass,
861
+ toggleClass,
862
+ setStyle,
863
+ removeStyle,
864
+ removeStyles,
865
+ setAttribute,
866
+ removeAttribute,
867
+ setProperty,
868
+ setProperties,
869
+ createElement,
870
+ createText,
871
+ createComment,
872
+ appendChild,
873
+ insertBefore,
874
+ removeChild,
875
+ selectRootElement,
876
+ parentNode,
877
+ nextSibling,
878
+ setValue,
879
+ listen,
880
+ destroyNode,
881
+ replaceChild,
882
+ empty,
883
+ setTextContent,
884
+ setInnerHTML,
885
+ setAttributes,
886
+ setDataAttributes,
887
+ setCssProperty,
888
+ setCssProperties,
889
+ moveBefore,
890
+ renderer,
815
891
  };
816
- }, { name: 'Breakpoint Observer' });
892
+ }, { name: 'Angular Renderer' });
817
893
 
818
894
  const isElementSignal = (el) => {
819
895
  if (!isSignal(el))
@@ -1255,6 +1331,270 @@ const injectScrollbarDimensions = memoizeSignal(() => {
1255
1331
  });
1256
1332
  });
1257
1333
 
1334
+ /**
1335
+ * Smoothly animates an element's `block-size` when its content changes size (e.g. a list filtering,
1336
+ * a loading state resolving). Set up once in an injection context; it observes the given content
1337
+ * element(s) and animates the host from its previous height to its new natural height.
1338
+ *
1339
+ * Robustness notes: the baseline height is captured on the first render (via `afterNextRender`) and
1340
+ * only later changes animate, so the initial layout settling never plays as a grow-from-0.
1341
+ * A change that interrupts an in-flight animation continues from the current animated height, and
1342
+ * zero/pre-layout measurements are ignored. Respects `prefers-reduced-motion`.
1343
+ *
1344
+ * The animation starts synchronously inside the `ResizeObserver` callback — it runs after layout
1345
+ * but **before paint**, so the host never paints a frame at its new natural size. Routing the
1346
+ * resize through a signal + effect instead would apply the animation one change-detection cycle
1347
+ * (= one painted frame) late: a growing panel would flash at its final size, snap back, and only
1348
+ * then animate.
1349
+ */
1350
+ const injectAnimatedBlockSize = (config) => {
1351
+ const hostBinding = config.host ?? inject(ElementRef);
1352
+ const renderer = injectRenderer();
1353
+ const prefersReducedMotion = injectPrefersReducedMotion();
1354
+ const destroyRef = inject(DestroyRef);
1355
+ const hostSignal = firstElementSignal(buildElementSignal(hostBinding));
1356
+ const observeTargets = Array.isArray(config.observe) ? config.observe : [config.observe];
1357
+ const observedElements = observeTargets.map((target) => firstElementSignal(buildElementSignal(target)));
1358
+ const duration = config.duration ?? 160;
1359
+ const easing = config.easing ?? 'ease';
1360
+ let lastBlockSize = null;
1361
+ let animation = null;
1362
+ let ready = false;
1363
+ const hostElement = () => hostSignal().currentElement;
1364
+ const run = () => {
1365
+ const host = hostElement();
1366
+ if (!host)
1367
+ return;
1368
+ // continue from the current animated height when interrupting a running animation
1369
+ const from = animation?.playState === 'running' ? host.getBoundingClientRect().height : lastBlockSize;
1370
+ animation?.cancel();
1371
+ animation = null;
1372
+ const to = host.getBoundingClientRect().height;
1373
+ // never record or animate a zero / pre-layout height
1374
+ if (to === 0)
1375
+ return;
1376
+ lastBlockSize = to;
1377
+ if (from === null || from < 1 || Math.abs(from - to) < 1 || prefersReducedMotion()) {
1378
+ return;
1379
+ }
1380
+ if (config.resizingClass && renderer) {
1381
+ renderer.addClass(host, config.resizingClass);
1382
+ }
1383
+ const nextAnimation = host.animate([{ blockSize: `${from}px` }, { blockSize: `${to}px` }], { duration, easing });
1384
+ const cleanup = () => {
1385
+ if (config.resizingClass && renderer) {
1386
+ renderer.removeClass(host, config.resizingClass);
1387
+ }
1388
+ if (animation === nextAnimation) {
1389
+ animation = null;
1390
+ }
1391
+ };
1392
+ nextAnimation.finished.then(cleanup).catch(cleanup);
1393
+ animation = nextAnimation;
1394
+ };
1395
+ // no zone/signal indirection — see the pre-paint timing note in the doc comment above
1396
+ const observer = new ResizeObserver(() => {
1397
+ if (!ready)
1398
+ return;
1399
+ run();
1400
+ });
1401
+ effect(() => {
1402
+ const elements = observedElements.map((element) => element().currentElement);
1403
+ untracked(() => {
1404
+ observer.disconnect();
1405
+ for (const element of elements) {
1406
+ if (element) {
1407
+ observer.observe(element);
1408
+ }
1409
+ }
1410
+ });
1411
+ });
1412
+ // Capture the settled height of the first render as the baseline, then only animate later changes.
1413
+ afterNextRender(() => {
1414
+ const host = hostElement();
1415
+ lastBlockSize = host ? host.getBoundingClientRect().height || null : null;
1416
+ ready = true;
1417
+ });
1418
+ destroyRef.onDestroy(() => {
1419
+ observer.disconnect();
1420
+ animation?.cancel();
1421
+ });
1422
+ };
1423
+
1424
+ /**
1425
+ * Creates a signal that animates the transition of a numeric signal's value over time.
1426
+ */
1427
+ const signalAnimatedNumber = (source, options = {}) => {
1428
+ const ngZone = inject(NgZone);
1429
+ const duration = options.duration ?? 1000;
1430
+ const easing = options.easing ?? easeOut;
1431
+ const round = options.round ?? ((v) => Math.round(v));
1432
+ const initialValue = options.initialValue ?? 0;
1433
+ const animatedValue = signal(initialValue, /* @ts-ignore */
1434
+ ...(ngDevMode ? [{ debugName: "animatedValue" }] : /* istanbul ignore next */ []));
1435
+ let rafId = null;
1436
+ let currentAnimatedValue = initialValue;
1437
+ const cancelAnimation = () => {
1438
+ if (rafId !== null) {
1439
+ cancelAnimationFrame(rafId);
1440
+ rafId = null;
1441
+ }
1442
+ };
1443
+ const readonlySignal = animatedValue.asReadonly();
1444
+ const play = () => {
1445
+ const targetValue = isSignal(source) ? source() : source;
1446
+ cancelAnimation();
1447
+ const startValue = currentAnimatedValue;
1448
+ const startTime = performance.now();
1449
+ const delta = targetValue - startValue;
1450
+ // Skip animation if no change
1451
+ if (delta === 0) {
1452
+ options.onAnimationEnd?.();
1453
+ return readonlySignal;
1454
+ }
1455
+ options.onAnimationStart?.();
1456
+ ngZone.runOutsideAngular(() => {
1457
+ const animate = (timestamp) => {
1458
+ const elapsed = timestamp - startTime;
1459
+ const progress = Math.min(elapsed / duration, 1);
1460
+ const easedProgress = easing(progress);
1461
+ const value = startValue + delta * easedProgress;
1462
+ currentAnimatedValue = value;
1463
+ untracked(() => {
1464
+ animatedValue.set(round(value));
1465
+ });
1466
+ if (progress < 1) {
1467
+ rafId = requestAnimationFrame(animate);
1468
+ }
1469
+ else {
1470
+ rafId = null;
1471
+ ngZone.run(() => {
1472
+ options.onAnimationEnd?.();
1473
+ });
1474
+ }
1475
+ };
1476
+ rafId = requestAnimationFrame(animate);
1477
+ });
1478
+ return readonlySignal;
1479
+ };
1480
+ const reset = () => {
1481
+ cancelAnimation();
1482
+ currentAnimatedValue = initialValue;
1483
+ animatedValue.set(initialValue);
1484
+ return readonlySignal;
1485
+ };
1486
+ const stop = () => {
1487
+ cancelAnimation();
1488
+ return readonlySignal;
1489
+ };
1490
+ readonlySignal.play = play;
1491
+ readonlySignal.reset = reset;
1492
+ readonlySignal.stop = stop;
1493
+ return readonlySignal;
1494
+ };
1495
+ /* Linear easing function */
1496
+ const easeLinear = (t) => t;
1497
+ /* Ease in (slow start) */
1498
+ const easeIn = (t) => t * t;
1499
+ /* Ease out (slow end) */
1500
+ const easeOut = (t) => t * (2 - t);
1501
+ /* Ease in-out (slow start and end) */
1502
+ const easeInOut = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
1503
+ /** Elastic easing function (overshoots) */
1504
+ const easeElastic = (t) => {
1505
+ const c4 = (2 * Math.PI) / 3;
1506
+ return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
1507
+ };
1508
+ /** Back easing function (overshoots slightly then settles) */
1509
+ const easeOutBack = (t) => {
1510
+ const c1 = 1.70158;
1511
+ const c3 = c1 + 1;
1512
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
1513
+ };
1514
+ /** Back easing function with more overshoot */
1515
+ const easeOutBackStrong = (t) => {
1516
+ const c1 = 2.5;
1517
+ const c3 = c1 + 1;
1518
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
1519
+ };
1520
+
1521
+ const BREAKPOINT_ORDER = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
1522
+ /**
1523
+ * Default viewport config based on Tailwind CSS.
1524
+ * @see https://tailwindcss.com/docs/screens
1525
+ */
1526
+ const DEFAULT_VIEWPORT_CONFIG = {
1527
+ breakpoints: {
1528
+ xs: [0, 639],
1529
+ sm: [640, 767],
1530
+ md: [768, 1023],
1531
+ lg: [1024, 1279],
1532
+ xl: [1280, 1535],
1533
+ '2xl': [1536, Infinity],
1534
+ },
1535
+ };
1536
+ const [provideViewportConfig, injectViewportConfig] = createStaticRootProvider(DEFAULT_VIEWPORT_CONFIG, {
1537
+ name: 'Viewport Config',
1538
+ });
1539
+ const [provideBreakpointObserver, injectBreakpointObserver] = createRootProvider(() => {
1540
+ const breakpointObserver = inject(BreakpointObserver);
1541
+ const viewportConfig = injectViewportConfig();
1542
+ const isMediaQueryMatched = (mediaQuery) => breakpointObserver.isMatched(mediaQuery);
1543
+ const observeMediaQuery = (mediaQuery) => {
1544
+ return toSignal(breakpointObserver.observe(mediaQuery).pipe(map((x) => x.matches), startWith(isMediaQueryMatched(mediaQuery))), { requireSync: true });
1545
+ };
1546
+ const observeBreakpoint = (options) => observeMediaQuery(buildMediaQueryString(options));
1547
+ const isBreakpointMatched = (options) => isMediaQueryMatched(buildMediaQueryString(options));
1548
+ const getBreakpointSize = (type, option) => {
1549
+ const index = option === 'min' ? 0 : 1;
1550
+ const size = viewportConfig.breakpoints[type][index];
1551
+ if (size === Infinity || size === 0) {
1552
+ return size;
1553
+ }
1554
+ if (option === 'min') {
1555
+ return size;
1556
+ }
1557
+ // Due to scaling, the actual size of the viewport may be a decimal number.
1558
+ // Eg. on Windows 11 with 150% scaling, the viewport size may be 1535.33px
1559
+ // and thus not matching any of the default breakpoints.
1560
+ return size + 0.9;
1561
+ };
1562
+ const buildMediaQueryString = (options) => {
1563
+ if (!options.min && !options.max) {
1564
+ throw new Error('At least one of min or max must be defined');
1565
+ }
1566
+ const mediaQueryParts = [];
1567
+ if (options.min) {
1568
+ if (typeof options.min === 'number') {
1569
+ mediaQueryParts.push(`(min-width: ${options.min}px)`);
1570
+ }
1571
+ else {
1572
+ mediaQueryParts.push(`(min-width: ${getBreakpointSize(options.min, 'min')}px)`);
1573
+ }
1574
+ }
1575
+ if (options.min && options.max) {
1576
+ mediaQueryParts.push('and');
1577
+ }
1578
+ if (options.max) {
1579
+ if (typeof options.max === 'number') {
1580
+ mediaQueryParts.push(`(max-width: ${options.max}px)`);
1581
+ }
1582
+ else {
1583
+ mediaQueryParts.push(`(max-width: ${getBreakpointSize(options.max, 'max')}px)`);
1584
+ }
1585
+ }
1586
+ return mediaQueryParts.join(' ');
1587
+ };
1588
+ return {
1589
+ observeBreakpoint,
1590
+ isBreakpointMatched,
1591
+ getBreakpointSize,
1592
+ buildMediaQueryString,
1593
+ observeMediaQuery,
1594
+ isMediaQueryMatched,
1595
+ };
1596
+ }, { name: 'Breakpoint Observer' });
1597
+
1258
1598
  const BREAKPOINT_KEY_SET = new Set(BREAKPOINT_ORDER);
1259
1599
  const isBreakpointMap = (value) => {
1260
1600
  if (value === null || typeof value !== 'object' || Array.isArray(value))
@@ -2643,6 +2983,20 @@ const createLogger = (config) => {
2643
2983
  };
2644
2984
 
2645
2985
  const escapeHtml = (str) => str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2986
+ /** Escapes HTML-special characters but leaves existing entity references intact — for input that
2987
+ * may already contain entities (e.g. Markdown serialized from the editor's DOM, where `&` is
2988
+ * stored as `&amp;`), where plain escaping would double-escape them. */
2989
+ const escapeHtmlPreservingEntities = (str) => str
2990
+ .replace(/&(?![a-z]+;|#\d+;|#x[0-9a-f]+;)/gi, '&amp;')
2991
+ .replace(/</g, '&lt;')
2992
+ .replace(/>/g, '&gt;')
2993
+ .replace(/"/g, '&quot;');
2994
+ /** Rejects URL schemes that execute script when navigated. Whitespace/control characters are
2995
+ * stripped before checking — browsers ignore them inside URLs, so `java\tscript:` would
2996
+ * otherwise slip through. */
2997
+ const isSafeUrl = (url) =>
2998
+ // eslint-disable-next-line no-control-regex -- stripping control characters is the point: browsers ignore them inside URLs
2999
+ !/^(javascript|data|vbscript):/i.test(url.replace(/[\s\u0000-\u001f]/g, ''));
2646
3000
  const unescapeHtml = (str) => str
2647
3001
  .replace(/&amp;/g, '&')
2648
3002
  .replace(/&lt;/g, '<')
@@ -2656,30 +3010,183 @@ const parseTableRow = (line) => line
2656
3010
  .slice(1, -1)
2657
3011
  .map((cell) => cell.trim());
2658
3012
  const isTableSeparatorLine = (line) => /^\|?(\s*:?-+:?\s*\|)+\s*$/.test(line);
3013
+ /** Column alignments from a GFM separator line: `:---` left, `:---:` center, `---:` right. */
3014
+ const parseSeparatorAligns = (line) => parseTableRow(line).map((cell) => {
3015
+ const left = cell.startsWith(':');
3016
+ const right = cell.endsWith(':');
3017
+ return left && right ? 'center' : right ? 'right' : left ? 'left' : null;
3018
+ });
3019
+ /** The GFM separator token for a column alignment. */
3020
+ const separatorFor = (align) => align === 'center' ? ':---:' : align === 'right' ? '---:' : align === 'left' ? ':---' : '---';
3021
+ const alignStyle = (align) => (align ? ` style="text-align: ${align}"` : '');
3022
+ /** Wraps `content` in an emphasis `marker`, hoisting boundary whitespace outside the delimiters —
3023
+ * CommonMark emphasis must not face whitespace on the inside (`** fett**` doesn't parse), and
3024
+ * `&nbsp;` counts as whitespace there too. Whitespace-only content stays unwrapped. */
3025
+ const emphasize = (marker, content) => {
3026
+ const lead = /^(?:\s|&nbsp;)+/i.exec(content)?.[0] ?? '';
3027
+ const rest = content.slice(lead.length);
3028
+ const trail = /(?:\s|&nbsp;)+$/i.exec(rest)?.[0] ?? '';
3029
+ const core = rest.slice(0, rest.length - trail.length);
3030
+ return core ? `${lead}${marker}${core}${marker}${trail}` : content;
3031
+ };
2659
3032
  const makePlaceholder = (kind, idx) => `\u{E000}${kind}${idx}\u{E001}`;
2660
3033
  const placeholderRe = (kind) => new RegExp(`\u{E000}${kind}(\\d+)\u{E001}`, 'gu');
2661
3034
  const isPlaceholder = (kind, str) => new RegExp(`^\u{E000}${kind}\\d+\u{E001}$`, 'u').test(str);
3035
+ /** Tags allowed to survive inside an aligned block's raw-HTML passthrough. */
3036
+ const SAFE_INLINE_TAGS = new Set(['strong', 'em', 'b', 'i', 'del', 's', 'u', 'code', 'br']);
3037
+ /** Reduces raw inline HTML to the editor's own vocabulary: allowed tags lose all their attributes
3038
+ * (`<a>` keeps a safe `href`), everything else — including any event-handler attribute — is
3039
+ * dropped, and the remaining text is escaped. */
3040
+ const sanitizeInlineHtml = (html) => {
3041
+ const kept = [];
3042
+ const stashed = html.replace(/<\s*(\/?)\s*([a-z][a-z0-9]*)\b[^>]*>/gi, (full, closing, tag) => {
3043
+ const name = tag.toLowerCase();
3044
+ if (name === 'a') {
3045
+ const href = closing ? null : /\bhref\s*=\s*"([^"]*)"/i.exec(full)?.[1];
3046
+ const anchor = closing ? '</a>' : href && isSafeUrl(href) ? `<a href="${href}">` : '<a>';
3047
+ return makePlaceholder('TAG', kept.push(anchor) - 1);
3048
+ }
3049
+ if (!SAFE_INLINE_TAGS.has(name))
3050
+ return '';
3051
+ return makePlaceholder('TAG', kept.push(closing ? `</${name}>` : `<${name}>`) - 1);
3052
+ });
3053
+ return escapeHtmlPreservingEntities(stashed).replace(placeholderRe('TAG'), (_, i) => kept[+i] ?? '');
3054
+ };
2662
3055
  const processInline = (text) => {
2663
3056
  const inlineCodes = [];
2664
3057
  text = text.replace(/`([^`]+)`/g, (_, code) => {
2665
3058
  const idx = inlineCodes.push(`<code>${escapeHtml(code)}</code>`) - 1;
2666
3059
  return makePlaceholder('IC', idx);
2667
3060
  });
2668
- // Bold + italic — *** before ** before *
2669
- text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
2670
- text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
2671
- text = text.replace(/__(.+?)__/g, '<strong>$1</strong>');
2672
- text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
2673
- text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');
2674
- // Images before links (order matters)
2675
- text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
2676
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
2677
- return text.replace(placeholderRe('IC'), (_, i) => inlineCodes[+i] ?? '');
3061
+ // "Open in new tab" links round-trip as raw HTML because Markdown has no `target` syntax. Extract
3062
+ // them before the raw-HTML escape below, keeping only a safe href, `target="_blank"` and a forced
3063
+ // `rel="noopener noreferrer"` (any other attribute — including event handlers — is dropped); their
3064
+ // inner markup is processed recursively. Anything else that looks like raw HTML is still escaped.
3065
+ const newTabLinks = [];
3066
+ text = text.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (full, attrs, inner) => {
3067
+ const href = /\bhref\s*=\s*"([^"]*)"/i.exec(attrs)?.[1];
3068
+ const newTab = /\btarget\s*=\s*"_blank"/i.test(attrs);
3069
+ if (!newTab || !href || !isSafeUrl(href))
3070
+ return full;
3071
+ const anchor = `<a href="${href}" target="_blank" rel="noopener noreferrer">${processInline(inner)}</a>`;
3072
+ return makePlaceholder('NTLINK', newTabLinks.push(anchor) - 1);
3073
+ });
3074
+ // Raw HTML in Markdown text is escaped, not rendered — the editor writes this HTML straight
3075
+ // into the DOM via innerHTML, so anything else would let a crafted value inject markup. `<u>`
3076
+ // is the one deliberate exception: underline has no Markdown form and round-trips as raw <u>.
3077
+ text = escapeHtmlPreservingEntities(text).replace(/&lt;(\/?)u&gt;/gi, '<$1u>');
3078
+ // Bold + italic — *** before ** before *. A delimiter flanking whitespace doesn't open/close a
3079
+ // run (CommonMark's flanking rule), so literal asterisks as in `2 * 3 * 4` stay text.
3080
+ text = text.replace(/\*\*\*([^\s*](?:.*?[^\s*])?)\*\*\*/g, '<strong><em>$1</em></strong>');
3081
+ text = text.replace(/\*\*([^\s*](?:.*?[^\s*])?)\*\*/g, '<strong>$1</strong>');
3082
+ text = text.replace(/__([^\s_](?:.*?[^\s_])?)__/g, '<strong>$1</strong>');
3083
+ text = text.replace(/\*([^\s*](?:.*?[^\s*])?)\*/g, '<em>$1</em>');
3084
+ // Single-underscore emphasis must not fire inside a word (snake_case identifiers).
3085
+ text = text.replace(/(?<![\w*_])_([^\s_](?:.*?[^\s_])?)_(?![\w_])/g, '<em>$1</em>');
3086
+ text = text.replace(/~~([^\s~](?:.*?[^\s~])?)~~/g, '<del>$1</del>');
3087
+ // Images before links (order matters); URLs with script-running schemes stay literal text
3088
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => isSafeUrl(src) ? `<img src="${src}" alt="${alt}">` : match);
3089
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, href) => isSafeUrl(href) ? `<a href="${href}">${label}</a>` : match);
3090
+ return text
3091
+ .replace(placeholderRe('IC'), (_, i) => inlineCodes[+i] ?? '')
3092
+ .replace(placeholderRe('NTLINK'), (_, i) => newTabLinks[+i] ?? '');
3093
+ };
3094
+ // --- Nested list helpers (regex can't match balanced nesting, so these scan by tag depth) ---
3095
+ /** The first top-level `<ul>`/`<ol>` in `html`, matched with balanced nesting. */
3096
+ const findList = (html) => {
3097
+ const open = /<(ul|ol)\b[^>]*>/i.exec(html);
3098
+ if (!open)
3099
+ return null;
3100
+ const innerStart = open.index + open[0].length;
3101
+ const tag = /<(\/?)(?:ul|ol)\b[^>]*>/gi;
3102
+ tag.lastIndex = innerStart;
3103
+ let depth = 1;
3104
+ let match;
3105
+ while ((match = tag.exec(html))) {
3106
+ depth += match[1] ? -1 : 1;
3107
+ if (depth === 0) {
3108
+ return {
3109
+ start: open.index,
3110
+ end: tag.lastIndex,
3111
+ ordered: open[1].toLowerCase() === 'ol',
3112
+ inner: html.slice(innerStart, match.index),
3113
+ };
3114
+ }
3115
+ }
3116
+ return null;
3117
+ };
3118
+ /** The inner HTML of each direct `<li>` child of a list's inner HTML (balanced over nested lists). */
3119
+ const findListItems = (inner) => {
3120
+ const items = [];
3121
+ const tag = /<(\/?)li\b[^>]*>/gi;
3122
+ let depth = 0;
3123
+ let start = -1;
3124
+ let match;
3125
+ while ((match = tag.exec(inner))) {
3126
+ if (match[1]) {
3127
+ depth--;
3128
+ if (depth === 0 && start >= 0) {
3129
+ items.push(inner.slice(start, match.index));
3130
+ start = -1;
3131
+ }
3132
+ }
3133
+ else {
3134
+ if (depth === 0)
3135
+ start = match.index + match[0].length;
3136
+ depth++;
3137
+ }
3138
+ }
3139
+ return items;
3140
+ };
3141
+ /** Serializes a list's inner HTML to Markdown, indenting nested lists two spaces per level. */
3142
+ const listToMarkdown = (inner, ordered, depth) => {
3143
+ const indent = ' '.repeat(depth);
3144
+ let n = 1;
3145
+ return findListItems(inner)
3146
+ .map((itemInner) => {
3147
+ let content = itemInner;
3148
+ let nestedMarkdown = '';
3149
+ for (let nested = findList(content); nested; nested = findList(content)) {
3150
+ nestedMarkdown += `\n${listToMarkdown(nested.inner, nested.ordered, depth + 1)}`;
3151
+ content = content.slice(0, nested.start) + content.slice(nested.end);
3152
+ }
3153
+ const marker = ordered ? `${n++}. ` : '- ';
3154
+ // list items are single-line in this serializer, so a <br> degrades to a space
3155
+ return `${indent}${marker}${stripTags(content.replace(/<br\s*\/?>/gi, ' ')).trim()}${nestedMarkdown}`;
3156
+ })
3157
+ .join('\n');
3158
+ };
3159
+ /** Parses list lines into (indent-level, ordered, text) — two leading spaces per nesting level. */
3160
+ const parseListLines = (lines) => lines
3161
+ .map((line) => {
3162
+ const match = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line);
3163
+ return match
3164
+ ? { indent: Math.floor((match[1] ?? '').length / 2), ordered: /\d/.test(match[2] ?? ''), text: match[3] ?? '' }
3165
+ : null;
3166
+ })
3167
+ .filter((line) => line !== null);
3168
+ /** Builds nested `<ul>`/`<ol>` HTML from parsed list lines starting at `start`, for one indent level. */
3169
+ const buildListHtml = (lines, start, baseIndent) => {
3170
+ const ordered = lines[start]?.ordered ?? false;
3171
+ let items = '';
3172
+ let i = start;
3173
+ while (i < lines.length && (lines[i]?.indent ?? -1) === baseIndent) {
3174
+ let item = processInline(lines[i].text);
3175
+ i++;
3176
+ if (i < lines.length && (lines[i]?.indent ?? -1) > baseIndent) {
3177
+ const nested = buildListHtml(lines, i, lines[i].indent);
3178
+ item += nested.html;
3179
+ i = nested.next;
3180
+ }
3181
+ items += `<li>${item}</li>`;
3182
+ }
3183
+ const tag = ordered ? 'ol' : 'ul';
3184
+ return { html: `<${tag}>${items}</${tag}>`, next: i };
2678
3185
  };
2679
3186
  /**
2680
3187
  * Converts a markdown string to HTML.
2681
3188
  * Covers headings, bold, italic, strikethrough, inline code, fenced code blocks,
2682
- * links, images, block quotes, unordered/ordered lists, tables, horizontal rules, and paragraphs.
3189
+ * links, images, block quotes, unordered/ordered (and nested) lists, tables, horizontal rules, and paragraphs.
2683
3190
  */
2684
3191
  const markdownToHtml = (markdown) => {
2685
3192
  if (!markdown)
@@ -2704,6 +3211,16 @@ const markdownToHtml = (markdown) => {
2704
3211
  return '';
2705
3212
  if (isPlaceholder('CODE', trimmed))
2706
3213
  return trimmed;
3214
+ // Aligned block: text-align has no Markdown form, so it round-trips as raw native HTML.
3215
+ // Rebuild it instead of passing it through verbatim: only the alignment survives on the tag
3216
+ // (no other attributes, e.g. event handlers) and the inner markup is reduced to the editor's
3217
+ // own inline vocabulary.
3218
+ const aligned = /^<(p|h[1-6]|div)\b[^>]*\bstyle=["'][^"']*text-align:\s*([a-z]+)[^>]*>([\s\S]*)<\/\1>$/i.exec(trimmed);
3219
+ if (aligned) {
3220
+ const tag = (aligned[1] ?? 'p').toLowerCase();
3221
+ const align = (aligned[2] ?? 'left').toLowerCase();
3222
+ return `<${tag} style="text-align: ${align}">${sanitizeInlineHtml(aligned[3] ?? '')}</${tag}>`;
3223
+ }
2707
3224
  // Heading
2708
3225
  const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
2709
3226
  if (headingMatch?.[1] && headingMatch?.[2]) {
@@ -2713,46 +3230,39 @@ const markdownToHtml = (markdown) => {
2713
3230
  // Horizontal rule
2714
3231
  if (/^(---|\*\*\*|___)\s*$/.test(trimmed))
2715
3232
  return '<hr>';
2716
- // Table (GFM)
3233
+ // Table (GFM) — column alignment from the separator line lands as text-align on every cell
2717
3234
  const tableLines = trimmed.split('\n');
2718
3235
  if (tableLines.length >= 2 && /\|/.test(tableLines[0] ?? '') && isTableSeparatorLine(tableLines[1] ?? '')) {
3236
+ const aligns = parseSeparatorAligns(tableLines[1] ?? '');
2719
3237
  const headers = parseTableRow(tableLines[0] ?? '');
2720
- const thead = `<thead><tr>${headers.map((h) => `<th>${processInline(h)}</th>`).join('')}</tr></thead>`;
3238
+ const thead = `<thead><tr>${headers
3239
+ .map((h, i) => `<th${alignStyle(aligns[i] ?? null)}>${processInline(h)}</th>`)
3240
+ .join('')}</tr></thead>`;
2721
3241
  const bodyRows = tableLines.slice(2);
2722
3242
  const tbody = bodyRows.length > 0
2723
3243
  ? `<tbody>${bodyRows
2724
3244
  .map((row) => `<tr>${parseTableRow(row)
2725
- .map((cell) => `<td>${processInline(cell)}</td>`)
3245
+ .map((cell, i) => `<td${alignStyle(aligns[i] ?? null)}>${processInline(cell)}</td>`)
2726
3246
  .join('')}</tr>`)
2727
3247
  .join('')}</tbody>`
2728
3248
  : '';
2729
3249
  return `<table>${thead}${tbody}</table>`;
2730
3250
  }
2731
- // Blockquote
3251
+ // Blockquote — each line runs through the inline pass on its own (processInline escapes raw
3252
+ // HTML, so joining first would escape the <br> separators too)
2732
3253
  if (/^> /.test(trimmed)) {
2733
3254
  const content = trimmed
2734
3255
  .split('\n')
2735
- .map((l) => l.replace(/^>\s?/, ''))
3256
+ .map((l) => processInline(l.replace(/^>\s?/, '')))
2736
3257
  .join('<br>');
2737
- return `<blockquote>${processInline(content)}</blockquote>`;
3258
+ return `<blockquote>${content}</blockquote>`;
2738
3259
  }
2739
- // Unordered list
2740
- if (/^[-*+]\s/.test(trimmed)) {
2741
- const items = trimmed
2742
- .split('\n')
2743
- .filter((l) => /^[-*+]\s/.test(l))
2744
- .map((l) => `<li>${processInline(l.replace(/^[-*+]\s+/, '').trim())}</li>`)
2745
- .join('');
2746
- return `<ul>${items}</ul>`;
2747
- }
2748
- // Ordered list
2749
- if (/^\d+\.\s/.test(trimmed)) {
2750
- const items = trimmed
2751
- .split('\n')
2752
- .filter((l) => /^\d+\.\s/.test(l))
2753
- .map((l) => `<li>${processInline(l.replace(/^\d+\.\s+/, '').trim())}</li>`)
2754
- .join('');
2755
- return `<ol>${items}</ol>`;
3260
+ // List (unordered/ordered, with indentation-based nesting)
3261
+ if (/^([-*+]|\d+\.)\s/.test(trimmed)) {
3262
+ const lines = parseListLines(trimmed.split('\n'));
3263
+ if (lines.length > 0) {
3264
+ return buildListHtml(lines, 0, lines[0].indent).html;
3265
+ }
2756
3266
  }
2757
3267
  // Paragraph — single newlines within a block become <br>
2758
3268
  return `<p>${trimmed
@@ -2773,6 +3283,11 @@ const htmlToMarkdown = (html) => {
2773
3283
  if (!html)
2774
3284
  return '';
2775
3285
  let md = html;
3286
+ // Aligned blocks: text-align has no Markdown form, so preserve them verbatim as native HTML (their
3287
+ // inner markup stays HTML) and round-trip via a placeholder — extracted before the block passes
3288
+ // below rewrite them, restored after the final tag-strip.
3289
+ const alignedBlocks = [];
3290
+ md = md.replace(/<(p|h[1-6]|div)\b[^>]*\bstyle="[^"]*text-align[^"]*"[^>]*>[\s\S]*?<\/\1>/gi, (match) => makePlaceholder('ALIGN', alignedBlocks.push(match) - 1));
2776
3291
  // Code blocks — process before inline code
2777
3292
  md = md.replace(/<pre[^>]*><code[^>]*class="language-([^"]+)"[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, lang, code) => `\n\`\`\`${lang}\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
2778
3293
  md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, code) => `\n\`\`\`\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
@@ -2782,50 +3297,64 @@ const htmlToMarkdown = (html) => {
2782
3297
  for (let i = 6; i >= 1; i--) {
2783
3298
  md = md.replace(new RegExp(`<h${i}[^>]*>([\\s\\S]*?)<\\/h${i}>`, 'gi'), (_, content) => `\n${'#'.repeat(i)} ${content.trim()}\n`);
2784
3299
  }
2785
- // Bold + italic (combined before individual)
2786
- md = md.replace(/<strong[^>]*><em[^>]*>([\s\S]*?)<\/em><\/strong>/gi, '***$1***');
2787
- md = md.replace(/<em[^>]*><strong[^>]*>([\s\S]*?)<\/strong><\/em>/gi, '***$1***');
2788
- md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
2789
- md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
2790
- md = md.replace(/<(del|s|strike)[^>]*>([\s\S]*?)<\/\1>/gi, '~~$2~~');
3300
+ // Bold + italic (combined before individual). Boundary whitespace is hoisted out of the
3301
+ // delimiters (emphasize) — `<strong> x</strong>` must become ` **x**`, not the invalid `** x**`.
3302
+ md = md.replace(/<strong[^>]*><em[^>]*>([\s\S]*?)<\/em><\/strong>/gi, (_, c) => emphasize('***', c));
3303
+ md = md.replace(/<em[^>]*><strong[^>]*>([\s\S]*?)<\/strong><\/em>/gi, (_, c) => emphasize('***', c));
3304
+ md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _tag, c) => emphasize('**', c));
3305
+ md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _tag, c) => emphasize('*', c));
3306
+ md = md.replace(/<(del|s|strike)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _tag, c) => emphasize('~~', c));
2791
3307
  // Inline code
2792
3308
  md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, code) => `\`${unescapeHtml(code)}\``);
2793
- // Links and images
2794
- md = md.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
3309
+ // Links and images. A link with `target="_blank"` has no Markdown form, so it round-trips as raw
3310
+ // HTML (keeping only href + target + rel) via a placeholder — like underline below; ordinary links
3311
+ // become `[text](url)`.
3312
+ const newTabLinks = [];
3313
+ md = md.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (_, attrs, inner) => {
3314
+ const href = /\bhref\s*=\s*"([^"]*)"/i.exec(attrs)?.[1] ?? '';
3315
+ if (/\btarget\s*=\s*"_blank"/i.test(attrs) && href) {
3316
+ const anchor = `<a href="${href}" target="_blank" rel="noopener noreferrer">${inner}</a>`;
3317
+ return makePlaceholder('NTLINK', newTabLinks.push(anchor) - 1);
3318
+ }
3319
+ return href ? `[${inner}](${href})` : inner;
3320
+ });
2795
3321
  md = md.replace(/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '![$2]($1)');
2796
3322
  md = md.replace(/<img[^>]+alt="([^"]*)"[^>]*src="([^"]*)"[^>]*\/?>/gi, '![$1]($2)');
2797
3323
  md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, '![]($1)');
2798
- // Block quotes
3324
+ // Underline has no Markdown form — preserve it as raw <u> so it round-trips. Extract it now (its
3325
+ // inner markup is already converted above) so the block passes below don't strip the tag; the
3326
+ // placeholder is restored after the final tag-strip.
3327
+ const underlines = [];
3328
+ md = md.replace(/<u[^>]*>([\s\S]*?)<\/u>/gi, (_, inner) => makePlaceholder('U', underlines.push(inner) - 1));
3329
+ // Block quotes — <br> line breaks become one quoted line each (stripTags would swallow them)
2799
3330
  md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
2800
- return (stripTags(content)
3331
+ return (stripTags(content.replace(/<br\s*\/?>/gi, '\n'))
2801
3332
  .trim()
2802
3333
  .split('\n')
2803
3334
  .map((line) => `> ${line.trim()}`)
2804
3335
  .join('\n') + '\n');
2805
3336
  });
2806
- // Lists
2807
- md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, content) => {
2808
- return ('\n' +
2809
- content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m, item) => `- ${stripTags(item).trim()}\n`));
2810
- });
2811
- md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (_, content) => {
2812
- let n = 1;
2813
- return ('\n' +
2814
- content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m, item) => `${n++}. ${stripTags(item).trim()}\n`));
2815
- });
3337
+ // Lists — replace each top-level list with its recursively-serialized Markdown (handles nesting).
3338
+ for (let list = findList(md); list; list = findList(md)) {
3339
+ md = `${md.slice(0, list.start)}\n${listToMarkdown(list.inner, list.ordered, 0)}\n${md.slice(list.end)}`;
3340
+ }
2816
3341
  // Tables
2817
3342
  md = md.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, tableContent) => {
2818
- const extractCells = (row) => [...row.matchAll(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi)].map((m) => stripTags(m[1] ?? '').trim());
3343
+ // GFM cells are single-line, so a <br> inside one degrades to a space (not silently dropped)
3344
+ const extractCells = (row) => [...row.matchAll(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi)].map((m) => stripTags((m[1] ?? '').replace(/<br\s*\/?>/gi, ' ')).trim());
3345
+ // GFM alignment is per column, read from the header cells' text-align styles
3346
+ const extractAligns = (row) => [...row.matchAll(/<t[hd]([^>]*)>/gi)].map((m) => {
3347
+ const align = /text-align:\s*(left|center|right)/i.exec(m[1] ?? '')?.[1]?.toLowerCase();
3348
+ return align ?? null;
3349
+ });
2819
3350
  const theadMatch = tableContent.match(/<thead[^>]*>([\s\S]*?)<\/thead>/i);
2820
3351
  const firstTrMatch = tableContent.match(/<tr[^>]*>([\s\S]*?)<\/tr>/i);
2821
- const headerCells = theadMatch
2822
- ? extractCells(theadMatch[1] ?? '')
2823
- : firstTrMatch
2824
- ? extractCells(firstTrMatch[1] ?? '')
2825
- : [];
3352
+ const headerRow = theadMatch?.[1] ?? firstTrMatch?.[1] ?? '';
3353
+ const headerCells = headerRow ? extractCells(headerRow) : [];
2826
3354
  if (headerCells.length === 0)
2827
3355
  return '';
2828
- const separator = headerCells.map(() => '---').join(' | ');
3356
+ const aligns = extractAligns(headerRow);
3357
+ const separator = headerCells.map((_cell, i) => separatorFor(aligns[i] ?? null)).join(' | ');
2829
3358
  const bodySource = theadMatch
2830
3359
  ? tableContent.replace(theadMatch[0], '')
2831
3360
  : tableContent.replace(firstTrMatch?.[0] ?? '', '');
@@ -2833,13 +3362,21 @@ const htmlToMarkdown = (html) => {
2833
3362
  const lines = [`| ${headerCells.join(' | ')} |`, `| ${separator} |`, ...bodyRows.map((r) => `| ${r} |`)];
2834
3363
  return `\n${lines.join('\n')}\n`;
2835
3364
  });
2836
- // Block elements
2837
- md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, content) => `\n${stripTags(content).trim()}\n`);
3365
+ // Block elements — a <br> inside a paragraph is a soft line break; turn it into a newline before
3366
+ // the tag-strip removes it, so it round-trips (a single newline within a block renders as <br>)
3367
+ md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, content) => `\n${stripTags(content.replace(/<br\s*\/?>/gi, '\n')).trim()}\n`);
3368
+ // A <div> boundary acts as a paragraph boundary — clipboard HTML commonly uses divs as
3369
+ // paragraphs, and silently stripping them would merge adjacent blocks into one.
3370
+ md = md.replace(/<\/div\s*>/gi, '\n\n').replace(/<div[^>]*>/gi, '\n\n');
2838
3371
  md = md.replace(/<br\s*\/?>/gi, '\n');
2839
3372
  md = md.replace(/<hr\s*\/?>/gi, '\n---\n');
2840
- return unescapeHtml(stripTags(md))
2841
- .replace(/\n{3,}/g, '\n\n')
2842
- .trim();
3373
+ md = unescapeHtml(stripTags(md)).replace(placeholderRe('U'), (_, i) => `<u>${underlines[+i] ?? ''}</u>`);
3374
+ // new-tab links survive the tag-strip as raw HTML (restored after it, like underline)
3375
+ md = md.replace(placeholderRe('NTLINK'), (_, i) => newTabLinks[+i] ?? '');
3376
+ // aligned blocks are block-level, so pad with blank lines before collapsing so they survive as
3377
+ // their own Markdown block
3378
+ md = md.replace(placeholderRe('ALIGN'), (_, i) => `\n\n${alignedBlocks[+i] ?? ''}\n\n`);
3379
+ return md.replace(/\n{3,}/g, '\n\n').trim();
2843
3380
  };
2844
3381
 
2845
3382
  const clamp = (value, min = 0, max = 100) => {
@@ -3145,190 +3682,6 @@ const [provideLocale, injectLocale] = createRootProvider(() => {
3145
3682
  name: 'Locale',
3146
3683
  });
3147
3684
 
3148
- const [provideRenderer, injectRenderer] = createRootProvider(() => {
3149
- const rendererFactory = inject(RendererFactory2);
3150
- const renderer = rendererFactory.createRenderer(null, null);
3151
- const addClass = (element, ...classes) => {
3152
- classes.forEach((cls) => renderer.addClass(element, cls));
3153
- };
3154
- const removeClass = (element, ...classes) => {
3155
- classes.forEach((cls) => renderer.removeClass(element, cls));
3156
- };
3157
- const toggleClass = (element, className, force) => {
3158
- const shouldAdd = force ?? !element.classList.contains(className);
3159
- if (shouldAdd) {
3160
- renderer.addClass(element, className);
3161
- }
3162
- else {
3163
- renderer.removeClass(element, className);
3164
- }
3165
- return shouldAdd;
3166
- };
3167
- const setStyle = (element, style, flags) => {
3168
- Object.entries(style).forEach(([key, value]) => {
3169
- if (!Number.isNaN(Number(key)))
3170
- return;
3171
- if (value !== null && value !== undefined) {
3172
- renderer.setStyle(element, key, value, flags);
3173
- }
3174
- else {
3175
- renderer.removeStyle(element, key, flags);
3176
- }
3177
- });
3178
- };
3179
- const removeStyle = (element, style, flags) => {
3180
- if (!Number.isNaN(Number(style)))
3181
- return;
3182
- renderer.removeStyle(element, style, flags);
3183
- };
3184
- const removeStyles = (element, ...styles) => {
3185
- styles.forEach((style) => removeStyle(element, style));
3186
- };
3187
- const setCssProperty = (element, name, value) => {
3188
- if (value !== null && value !== undefined) {
3189
- renderer.setStyle(element, name, value, RendererStyleFlags2.DashCase);
3190
- }
3191
- else {
3192
- renderer.removeStyle(element, name, RendererStyleFlags2.DashCase);
3193
- }
3194
- };
3195
- const setCssProperties = (element, properties) => {
3196
- Object.entries(properties).forEach(([name, value]) => {
3197
- setCssProperty(element, name, value);
3198
- });
3199
- };
3200
- const setAttribute = (element, name, value, namespace) => {
3201
- if (value !== null && value !== undefined) {
3202
- renderer.setAttribute(element, name, value, namespace);
3203
- }
3204
- else {
3205
- renderer.removeAttribute(element, name, namespace);
3206
- }
3207
- };
3208
- const removeAttribute = (element, ...names) => {
3209
- names.forEach((name) => renderer.removeAttribute(element, name));
3210
- };
3211
- const setProperty = (element, name, value) => {
3212
- renderer.setProperty(element, name, value);
3213
- };
3214
- const setProperties = (element, properties) => {
3215
- Object.entries(properties).forEach(([name, value]) => {
3216
- renderer.setProperty(element, name, value);
3217
- });
3218
- };
3219
- const createElement = (tagName, namespace) => {
3220
- return renderer.createElement(tagName, namespace);
3221
- };
3222
- const createText = (value) => {
3223
- return renderer.createText(value);
3224
- };
3225
- const createComment = (value) => {
3226
- return renderer.createComment(value);
3227
- };
3228
- const appendChild = (parent, newChild) => {
3229
- renderer.appendChild(parent, newChild);
3230
- };
3231
- const insertBefore = (parent, newChild, refChild) => {
3232
- renderer.insertBefore(parent, newChild, refChild);
3233
- };
3234
- const removeChild = (parent, oldChild, isHostElement) => {
3235
- renderer.removeChild(parent, oldChild, isHostElement);
3236
- };
3237
- const selectRootElement = (selectorOrNode, preserveContent) => {
3238
- return renderer.selectRootElement(selectorOrNode, preserveContent);
3239
- };
3240
- const parentNode = (node) => {
3241
- return renderer.parentNode(node);
3242
- };
3243
- const nextSibling = (node) => {
3244
- return renderer.nextSibling(node);
3245
- };
3246
- const setValue = (node, value) => {
3247
- renderer.setValue(node, value);
3248
- };
3249
- const listen = (target, eventName, callback) => {
3250
- return renderer.listen(target, eventName, callback);
3251
- };
3252
- const destroyNode = (node) => {
3253
- if (renderer.destroyNode) {
3254
- renderer.destroyNode(node);
3255
- }
3256
- };
3257
- const replaceChild = (parent, newChild, oldChild) => {
3258
- renderer.insertBefore(parent, newChild, oldChild);
3259
- renderer.removeChild(parent, oldChild);
3260
- };
3261
- const empty = (element) => {
3262
- while (element.firstChild) {
3263
- renderer.removeChild(element, element.firstChild);
3264
- }
3265
- };
3266
- const setTextContent = (element, text) => {
3267
- renderer.setProperty(element, 'textContent', text);
3268
- };
3269
- const setInnerHTML = (element, html) => {
3270
- renderer.setProperty(element, 'innerHTML', html);
3271
- };
3272
- const setAttributes = (element, attributes, namespace) => {
3273
- Object.entries(attributes).forEach(([name, value]) => {
3274
- setAttribute(element, name, value, namespace);
3275
- });
3276
- };
3277
- const setDataAttributes = (element, data) => {
3278
- Object.entries(data).forEach(([key, value]) => {
3279
- setAttribute(element, `data-${key}`, value);
3280
- });
3281
- };
3282
- const moveBefore = (config) => {
3283
- const { newParent, child, before = null } = config;
3284
- if ('moveBefore' in newParent) {
3285
- try {
3286
- newParent.moveBefore(child, before);
3287
- }
3288
- catch {
3289
- newParent.insertBefore(child, before);
3290
- }
3291
- }
3292
- else {
3293
- newParent.insertBefore(child, before);
3294
- }
3295
- };
3296
- return {
3297
- addClass,
3298
- removeClass,
3299
- toggleClass,
3300
- setStyle,
3301
- removeStyle,
3302
- removeStyles,
3303
- setAttribute,
3304
- removeAttribute,
3305
- setProperty,
3306
- setProperties,
3307
- createElement,
3308
- createText,
3309
- createComment,
3310
- appendChild,
3311
- insertBefore,
3312
- removeChild,
3313
- selectRootElement,
3314
- parentNode,
3315
- nextSibling,
3316
- setValue,
3317
- listen,
3318
- destroyNode,
3319
- replaceChild,
3320
- empty,
3321
- setTextContent,
3322
- setInnerHTML,
3323
- setAttributes,
3324
- setDataAttributes,
3325
- setCssProperty,
3326
- setCssProperties,
3327
- moveBefore,
3328
- renderer,
3329
- };
3330
- }, { name: 'Angular Renderer' });
3331
-
3332
3685
  const [provideStyleManager, injectStyleManager] = createRootProvider(() => {
3333
3686
  const appRef = inject(ApplicationRef);
3334
3687
  const destroyRef = inject(DestroyRef);
@@ -4596,12 +4949,14 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
4596
4949
  middleware.push(flip({
4597
4950
  fallbackPlacements: strategy.fallbackPlacements ?? undefined,
4598
4951
  fallbackAxisSideDirection: 'start',
4952
+ boundary: strategy.boundary,
4599
4953
  }));
4600
4954
  if (strategy.shift !== false) {
4601
4955
  middleware.push(shift({
4602
4956
  crossAxis: typeof strategy.shift === 'object' ? (strategy.shift.crossAxis ?? false) : false,
4603
4957
  limiter: limitShift(),
4604
4958
  padding: strategy.viewportPadding ?? 8,
4959
+ boundary: strategy.boundary,
4605
4960
  }));
4606
4961
  }
4607
4962
  // size must run AFTER shift so the available space is measured from the shifted position —
@@ -7416,6 +7771,7 @@ class ProvideColorDirective {
7416
7771
  this.themes = injectColorThemes({ optional: true });
7417
7772
  this.prefix = injectColorThemesPrefix({ optional: true });
7418
7773
  this.injector = inject(Injector);
7774
+ this.parentProvider = inject(COLOR_PROVIDER, { optional: true, skipSelf: true });
7419
7775
  this.currentProviderSync = null;
7420
7776
  this.forcedColor = signal(FORCED_COLOR_UNSET, /* @ts-ignore */
7421
7777
  ...(ngDevMode ? [{ debugName: "forcedColor" }] : /* istanbul ignore next */ []));
@@ -7428,6 +7784,22 @@ class ProvideColorDirective {
7428
7784
  return this.color();
7429
7785
  }, /* @ts-ignore */
7430
7786
  ...(ngDevMode ? [{ debugName: "effectiveColor" }] : /* istanbul ignore next */ []));
7787
+ /**
7788
+ * The color that actually applies at this provider's location: its own effective color, or —
7789
+ * when this provider is passive (no input, nothing forced) — the nearest ancestor provider's.
7790
+ * Mirrors the CSS cascade, which falls through a provider's `-color--inherited` scope class;
7791
+ * consumers that re-apply context across detached DOM (overlay panes) must use this instead
7792
+ * of `effectiveColor` or a passive in-between provider (e.g. a form field's) erases the theme.
7793
+ */
7794
+ this.resolvedColor = computed(() => {
7795
+ const own = this.effectiveColor();
7796
+ const ownName = typeof own === 'object' && own !== null ? own.name : own;
7797
+ if (ownName) {
7798
+ return own;
7799
+ }
7800
+ return this.parentProvider?.resolvedColor();
7801
+ }, /* @ts-ignore */
7802
+ ...(ngDevMode ? [{ debugName: "resolvedColor" }] : /* istanbul ignore next */ []));
7431
7803
  this.colorName = computed(() => {
7432
7804
  const raw = this.effectiveColor();
7433
7805
  const value = typeof raw === 'object' && raw !== null ? raw.name : raw;
@@ -7452,7 +7824,7 @@ class ProvideColorDirective {
7452
7824
  this.currentProviderSync?.destroy();
7453
7825
  runInInjectionContext(this.injector, () => {
7454
7826
  this.currentProviderSync = effect(() => {
7455
- const provideColor = provider.effectiveColor();
7827
+ const provideColor = provider.resolvedColor();
7456
7828
  untracked(() => {
7457
7829
  if (provideColor === undefined) {
7458
7830
  this.clearForcedColor();
@@ -7563,5 +7935,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
7563
7935
  * Generated bundle index. Do not edit.
7564
7936
  */
7565
7937
 
7566
- export { ANIMATABLE_TOKEN, ANIMATED_IF_TOKEN, ANIMATED_LIFECYCLE_TOKEN, ANIMATION_DEBUG_STORAGE_KEY, AT_LEAST_ONE_REQUIRED, AnimatableDirective, AnimatedIfDirective, AnimatedLifecycleDirective, AnimatedOverlayDirective, AutoSurfaceDirective, BOUNDARY_ELEMENT_TOKEN, BREAKPOINT_INSTANCE_TOKEN, BREAKPOINT_ORDER, COLOR_PROVIDER, ClickOutsideDirective, ColorInteractiveContainerDirective, ColorInteractiveDirective, ColorInteractiveExcludeDirective, ColorInteractiveHasFocusDirective, DEFAULT_MULTI_INSTANCE_RELS, DEFAULT_MULTI_INSTANCE_TAGS, DEFAULT_VIEWPORT_CONFIG, DISABLE_LOGGER_PARAM, DragHandleDirective, ET_DISABLE_SCROLL_TOP, ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE, ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE, ET_PROPERTY_REMOVED, FOCUSABLE_SELECTOR, FOCUS_VISIBLE_TRACKER_TOKEN, HtmlToMarkdownPipe, IS_ARRAY_NOT_EMPTY, IS_EMAIL, InferMimeTypePipe, InitialsPipe, IsArrayNotEmpty, IsEmail, jsonLd as JsonLD, KeyPressManager, MUST_MATCH, MarkdownToHtmlPipe, MustMatch, NormalizeGameResultTypePipe, NormalizeMatchParticipantsPipe, NormalizeMatchScorePipe, NormalizeMatchStatePipe, NormalizeMatchTypePipe, PropsDirective, ProvideColorDirective, ProvideSurfaceDirective, RUNTIME_ERROR_NO_DATA, RepeatDirective, ResizeHandlesComponent, RuntimeError, SEO_DIRECTIVE_TOKEN, SURFACE_PROVIDER, SURFACE_TYPE, ScrollObserverDirective, ScrollObserverEndDirective, ScrollObserverStartDirective, SeoDirective, SlugifyPipe, StructuredDataComponent, SurfaceInteractiveDirective, ToArrayPipe, TypedQueryList, ValidateAtLeastOneRequired, Validators, animationDebugLog, applyAlternateBinding, applyAlternateLanguagesBindings, applyArticleBindings, applyAuthorBinding, applyCanonicalBinding, applyCenteredPosition, applyDescriptionBinding, applyGlobalPosition, applyHeadBinding, applyHeadTitleBinding, applyHostListener, applyHostListeners, applyInitialFocus, applyKeywordsBinding, applyLinkBinding, applyMetaBinding, applyNextBinding, applyOgBinding, applyOpenGraphBindings, applyPrevBinding, applyResourceHintsBindings, applyRobotsBinding, applySocialMediaBindings, applyStructuredDataBinding, applyTwitterCardBindings, areScrollStatesEqual, bindProps, boolBreakpointTransform, booleanBreakpointAttribute, boundingClientRectToElementRect, breakpointTransformBase, buildElementSignal, buildSignalEffects, canUseSessionMemory, clamp, clone, cloneFormGroup, computedTillFalsy, computedTillTruthy, controlValueSignal, controlValueSignalWithPrevious, createAnchoredPositionCleanup, createArrayPropertyBinding, createAutoSessionMemoryKey, createBulkPropertyBinding, createCanAnimateSignal, createComponentId, createCssColorThemeName, createCssSurfaceName, createDependencyStash, createDestroy, createDocumentElementSignal, createElementDictionary, createElementDimensions, createEmptyElementSignal, createFlipAnimation, createFlipAnimationGroup, createHostProps, createIsRenderedSignal, createLogger, createOverlayRuntimeRef, createPropHandlers, createPropertyBinding, createProps, createProvider, createRootProvider, createRootThemeCss, createRoute, createRouterState, createRxHostListener, createSessionMemory, createSetup, createStaticProvider, createStaticRootProvider, createSwatchCss, createSwipeTracker, createTailwindColorThemes, createTailwindCssVar, createTailwindRgbVar, createThemeStyle, createUserConsentProvider, deferredSignal, deleteCookie, easeElastic, easeIn, easeInOut, easeLinear, easeOut, easeOutBack, easeOutBackStrong, elementCanScroll, equal, firstElementSignal, focusElement, forceReflow, formatRuntimeError, fromNextFrame, getCookie, getDomain, getElementScrollCoordinates, getFocusableElements, getFormGroupValue, getGroupMatchPoints, getGroupMatchScore, getHeadingElement, getKnockoutMatchScore, getMatchScoreSubLine, getObjectProperty, getScrollContainerTarget, getScrollItemTarget, getScrollSnapTarget, hasCookie, htmlToMarkdown, inferMimeType, initials, injectAngularRootElement, injectBoundaryElement, injectBreakpointInput, injectBreakpointIsMatched, injectBreakpointObserver, injectCanHover, injectColorThemes, injectColorThemesPrefix, injectCurrentBreakpoint, injectDeviceInputType, injectDisplayOrientation, injectErrorTheme, injectFocusVisibleTracker, injectFragment, injectHasPrecisionInput, injectHasTouchInput, injectHostElement, injectIs2Xl, injectIsLandscape, injectIsLg, injectIsMd, injectIsPortrait, injectIsRouterInitialized, injectIsSm, injectIsXl, injectIsXs, injectLinkStore, injectLinkStoreConfig, injectLocale, injectMediaQueryIsMatched, injectMetaConfig, injectMetaStore, injectObserveBreakpoint, injectObserveMediaQuery, injectOverlayRuntime, injectPathParam, injectPathParamChanges, injectPathParams, injectPrefersReducedMotion, injectQueryParam, injectQueryParamChanges, injectQueryParams, injectRenderer, injectRoute, injectRouteData, injectRouteDataItem, injectRouteTitle, injectRouterEvent, injectRouterState, injectScrollbarDimensions, injectStructuredDataConfig, injectStructuredDataStore, injectStyleManager, injectSuccessTheme, injectSurfaceContextTracker, injectSurfaceThemes, injectSurfaceThemesPrefix, injectTemplateRef, injectTitleConfig, injectTitleStore, injectUrl, injectViewportConfig, injectViewportDimensions, injectViewportSize, injectWarningTheme, isAnimationDebugEnabled, isArray, isElementSignal, isElementVisible, isFocusable, isGroupMatch, isHTMLElement, isKnockoutMatch, isObject, markdownToHtml, maybeSignalValue, memoizeSignal, nextFrame, normalizeGameResultType, normalizeMatchParticipant, normalizeMatchParticipants, normalizeMatchScore, normalizeMatchState, normalizeMatchType, numberBreakpointAttribute, numberBreakpointTransform, previousSignalValue, provideBoundaryElement, provideBreakpointInstance, provideBreakpointObserver, provideColorThemes, provideColorThemesWithTailwind4, provideFocusVisibleTracker, provideLinkStore, provideLinkStoreConfig, provideLocale, provideMetaConfig, provideMetaStore, provideOverlayRuntime, provideRenderer, provideStructuredDataConfig, provideStructuredDataStore, provideStyleManager, provideSurfaceContextTracker, provideSurfaceThemesWithTailwind4, provideTitleConfig, provideTitleStore, provideViewportConfig, resetPositioningStyles, resolveSurfaceByElevation, round, routerDisableScrollTop, scrollToElement, setBackdropStyles, setBaseElementStyles, setCookie, setInputSignal, setupFocusTrap, setupPositioning, setupScrollRestoration, signalAnimatedNumber, signalAttributes, signalClasses, signalElementChildren, signalElementDimensions, signalElementIntersection, signalElementLastScrollDirection, signalElementMutations, signalElementScrollState, signalHostAttributes, signalHostClasses, signalHostElementDimensions, signalHostElementIntersection, signalHostElementLastScrollDirection, signalHostElementMutations, signalHostElementScrollState, signalHostStyles, signalIsRendered, signalStyles, slugify, switchQueryListChanges, syncSignal, templateComputed, toArray, toArrayTrackByFn, toStringBinding, transformOrReturn, typedBreakpointTransform, unbindProps, useCursorDragScroll, writeScrollbarSizeToCssVariables, writeViewportSizeToCssVariables, ɵProvideColorThemes, ɵProvideColorThemesPrefix, ɵProvideSurfaceThemes, ɵProvideSurfaceThemesPrefix };
7938
+ export { ANIMATABLE_TOKEN, ANIMATED_IF_TOKEN, ANIMATED_LIFECYCLE_TOKEN, ANIMATION_DEBUG_STORAGE_KEY, AT_LEAST_ONE_REQUIRED, AnimatableDirective, AnimatedIfDirective, AnimatedLifecycleDirective, AnimatedOverlayDirective, AutoSurfaceDirective, BOUNDARY_ELEMENT_TOKEN, BREAKPOINT_INSTANCE_TOKEN, BREAKPOINT_ORDER, COLOR_PROVIDER, ClickOutsideDirective, ColorInteractiveContainerDirective, ColorInteractiveDirective, ColorInteractiveExcludeDirective, ColorInteractiveHasFocusDirective, DEFAULT_MULTI_INSTANCE_RELS, DEFAULT_MULTI_INSTANCE_TAGS, DEFAULT_VIEWPORT_CONFIG, DISABLE_LOGGER_PARAM, DragHandleDirective, ET_DISABLE_SCROLL_TOP, ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE, ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE, ET_PROPERTY_REMOVED, FOCUSABLE_SELECTOR, FOCUS_VISIBLE_TRACKER_TOKEN, HtmlToMarkdownPipe, IS_ARRAY_NOT_EMPTY, IS_EMAIL, InferMimeTypePipe, InitialsPipe, IsArrayNotEmpty, IsEmail, jsonLd as JsonLD, KeyPressManager, MUST_MATCH, MarkdownToHtmlPipe, MustMatch, NormalizeGameResultTypePipe, NormalizeMatchParticipantsPipe, NormalizeMatchScorePipe, NormalizeMatchStatePipe, NormalizeMatchTypePipe, PropsDirective, ProvideColorDirective, ProvideSurfaceDirective, RUNTIME_ERROR_NO_DATA, RepeatDirective, ResizeHandlesComponent, RuntimeError, SEO_DIRECTIVE_TOKEN, SURFACE_PROVIDER, SURFACE_TYPE, ScrollObserverDirective, ScrollObserverEndDirective, ScrollObserverStartDirective, SeoDirective, SlugifyPipe, StructuredDataComponent, SurfaceInteractiveDirective, ToArrayPipe, TypedQueryList, ValidateAtLeastOneRequired, Validators, animationDebugLog, applyAlternateBinding, applyAlternateLanguagesBindings, applyArticleBindings, applyAuthorBinding, applyCanonicalBinding, applyCenteredPosition, applyDescriptionBinding, applyGlobalPosition, applyHeadBinding, applyHeadTitleBinding, applyHostListener, applyHostListeners, applyInitialFocus, applyKeywordsBinding, applyLinkBinding, applyMetaBinding, applyNextBinding, applyOgBinding, applyOpenGraphBindings, applyPrevBinding, applyResourceHintsBindings, applyRobotsBinding, applySocialMediaBindings, applyStructuredDataBinding, applyTwitterCardBindings, areScrollStatesEqual, bindProps, boolBreakpointTransform, booleanBreakpointAttribute, boundingClientRectToElementRect, breakpointTransformBase, buildElementSignal, buildSignalEffects, canUseSessionMemory, clamp, clone, cloneFormGroup, computedTillFalsy, computedTillTruthy, controlValueSignal, controlValueSignalWithPrevious, copyToClipboard, createAnchoredPositionCleanup, createArrayPropertyBinding, createAutoSessionMemoryKey, createBulkPropertyBinding, createCanAnimateSignal, createComponentId, createCssColorThemeName, createCssSurfaceName, createDependencyStash, createDestroy, createDocumentElementSignal, createElementDictionary, createElementDimensions, createEmptyElementSignal, createFlipAnimation, createFlipAnimationGroup, createHostProps, createIsRenderedSignal, createLogger, createOverlayRuntimeRef, createPropHandlers, createPropertyBinding, createProps, createProvider, createRootProvider, createRootThemeCss, createRoute, createRouterState, createRxHostListener, createSessionMemory, createSetup, createStaticProvider, createStaticRootProvider, createSwatchCss, createSwipeTracker, createTailwindColorThemes, createTailwindCssVar, createTailwindRgbVar, createThemeStyle, createUserConsentProvider, deferredSignal, deleteCookie, easeElastic, easeIn, easeInOut, easeLinear, easeOut, easeOutBack, easeOutBackStrong, elementCanScroll, equal, firstElementSignal, focusElement, forceReflow, formatRuntimeError, fromNextFrame, getCookie, getDomain, getElementScrollCoordinates, getFocusableElements, getFormGroupValue, getGroupMatchPoints, getGroupMatchScore, getHeadingElement, getKnockoutMatchScore, getMatchScoreSubLine, getObjectProperty, getScrollContainerTarget, getScrollItemTarget, getScrollSnapTarget, hasCookie, htmlToMarkdown, inferMimeType, initials, injectAngularRootElement, injectAnimatedBlockSize, injectBoundaryElement, injectBreakpointInput, injectBreakpointIsMatched, injectBreakpointObserver, injectCanHover, injectColorThemes, injectColorThemesPrefix, injectCurrentBreakpoint, injectDeviceInputType, injectDisplayOrientation, injectErrorTheme, injectFocusVisibleTracker, injectFragment, injectHasPrecisionInput, injectHasTouchInput, injectHostElement, injectIs2Xl, injectIsLandscape, injectIsLg, injectIsMd, injectIsPortrait, injectIsRouterInitialized, injectIsSm, injectIsXl, injectIsXs, injectLinkStore, injectLinkStoreConfig, injectLocale, injectMediaQueryIsMatched, injectMetaConfig, injectMetaStore, injectObserveBreakpoint, injectObserveMediaQuery, injectOverlayRuntime, injectPathParam, injectPathParamChanges, injectPathParams, injectPrefersReducedMotion, injectQueryParam, injectQueryParamChanges, injectQueryParams, injectRenderer, injectRoute, injectRouteData, injectRouteDataItem, injectRouteTitle, injectRouterEvent, injectRouterState, injectScrollbarDimensions, injectStructuredDataConfig, injectStructuredDataStore, injectStyleManager, injectSuccessTheme, injectSurfaceContextTracker, injectSurfaceThemes, injectSurfaceThemesPrefix, injectTemplateRef, injectTitleConfig, injectTitleStore, injectUrl, injectViewportConfig, injectViewportDimensions, injectViewportSize, injectWarningTheme, isAnimationDebugEnabled, isArray, isElementSignal, isElementVisible, isFocusable, isGroupMatch, isHTMLElement, isKnockoutMatch, isObject, markdownToHtml, maybeSignalValue, memoizeSignal, nextFrame, normalizeGameResultType, normalizeMatchParticipant, normalizeMatchParticipants, normalizeMatchScore, normalizeMatchState, normalizeMatchType, numberBreakpointAttribute, numberBreakpointTransform, previousSignalValue, provideBoundaryElement, provideBreakpointInstance, provideBreakpointObserver, provideColorThemes, provideColorThemesWithTailwind4, provideFocusVisibleTracker, provideLinkStore, provideLinkStoreConfig, provideLocale, provideMetaConfig, provideMetaStore, provideOverlayRuntime, provideRenderer, provideStructuredDataConfig, provideStructuredDataStore, provideStyleManager, provideSurfaceContextTracker, provideSurfaceThemesWithTailwind4, provideTitleConfig, provideTitleStore, provideViewportConfig, randomId, readFromClipboard, resetPositioningStyles, resolveSurfaceByElevation, round, routerDisableScrollTop, scrollToElement, setBackdropStyles, setBaseElementStyles, setCookie, setInputSignal, setupFocusTrap, setupPositioning, setupScrollRestoration, signalAnimatedNumber, signalAttributes, signalClasses, signalElementChildren, signalElementDimensions, signalElementIntersection, signalElementLastScrollDirection, signalElementMutations, signalElementScrollState, signalHostAttributes, signalHostClasses, signalHostElementDimensions, signalHostElementIntersection, signalHostElementLastScrollDirection, signalHostElementMutations, signalHostElementScrollState, signalHostStyles, signalIsRendered, signalStyles, slugify, switchQueryListChanges, syncSignal, templateComputed, toArray, toArrayTrackByFn, toStringBinding, transformOrReturn, typedBreakpointTransform, unbindProps, useCursorDragScroll, writeScrollbarSizeToCssVariables, writeViewportSizeToCssVariables, ɵProvideColorThemes, ɵProvideColorThemesPrefix, ɵProvideSurfaceThemes, ɵProvideSurfaceThemesPrefix };
7567
7939
  //# sourceMappingURL=ethlete-core.mjs.map