@ethlete/core 5.0.0-next.25 → 5.0.0-next.27

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
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';
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';
@@ -360,6 +360,58 @@ const Validators = {
360
360
  ValidateAtLeastOneRequired,
361
361
  };
362
362
 
363
+ const copyToClipboard = async (text) => {
364
+ if (typeof navigator === 'undefined' || typeof document === 'undefined') {
365
+ return false;
366
+ }
367
+ if (navigator.clipboard) {
368
+ try {
369
+ await navigator.clipboard.writeText(text);
370
+ return true;
371
+ }
372
+ catch {
373
+ // The async Clipboard API can be blocked (missing permission, insecure context) — try the legacy path.
374
+ }
375
+ }
376
+ return copyToClipboardViaExecCommand(text);
377
+ };
378
+ const readFromClipboard = async () => {
379
+ if (typeof navigator === 'undefined' || !navigator.clipboard) {
380
+ return null;
381
+ }
382
+ try {
383
+ return await navigator.clipboard.readText();
384
+ }
385
+ catch {
386
+ return null;
387
+ }
388
+ };
389
+ const copyToClipboardViaExecCommand = (text) => {
390
+ const previouslyFocusedElement = document.activeElement;
391
+ const textarea = document.createElement('textarea');
392
+ textarea.value = text;
393
+ textarea.setAttribute('readonly', '');
394
+ textarea.setAttribute('aria-hidden', 'true');
395
+ textarea.style.position = 'fixed';
396
+ textarea.style.top = '-9999px';
397
+ textarea.style.opacity = '0';
398
+ document.body.appendChild(textarea);
399
+ textarea.select();
400
+ textarea.setSelectionRange(0, text.length);
401
+ let didCopy = false;
402
+ try {
403
+ didCopy = document.execCommand('copy');
404
+ }
405
+ catch {
406
+ // execCommand throws in some environments instead of returning false.
407
+ }
408
+ textarea.remove();
409
+ if (previouslyFocusedElement instanceof HTMLElement) {
410
+ previouslyFocusedElement.focus();
411
+ }
412
+ return didCopy;
413
+ };
414
+
363
415
  /* eslint-disable @typescript-eslint/no-explicit-any */
364
416
  /* eslint-disable no-var */
365
417
  /**
@@ -641,199 +693,209 @@ class KeyPressManager {
641
693
  }
642
694
  }
643
695
 
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
- }
662
- };
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;
674
- }
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;
699
- };
700
- const reset = () => {
701
- cancelAnimation();
702
- currentAnimatedValue = initialValue;
703
- animatedValue.set(initialValue);
704
- return readonlySignal;
705
- };
706
- const stop = () => {
707
- cancelAnimation();
708
- return readonlySignal;
696
+ const [provideRenderer, injectRenderer] = createRootProvider(() => {
697
+ const rendererFactory = inject(RendererFactory2);
698
+ const renderer = rendererFactory.createRenderer(null, null);
699
+ const addClass = (element, ...classes) => {
700
+ classes.forEach((cls) => renderer.addClass(element, cls));
709
701
  };
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 });
702
+ const removeClass = (element, ...classes) => {
703
+ classes.forEach((cls) => renderer.removeClass(element, cls));
765
704
  };
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;
705
+ const toggleClass = (element, className, force) => {
706
+ const shouldAdd = force ?? !element.classList.contains(className);
707
+ if (shouldAdd) {
708
+ renderer.addClass(element, className);
773
709
  }
774
- if (option === 'min') {
775
- return size;
710
+ else {
711
+ renderer.removeClass(element, className);
776
712
  }
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;
713
+ return shouldAdd;
781
714
  };
782
- const buildMediaQueryString = (options) => {
783
- if (!options.min && !options.max) {
784
- throw new Error('At least one of min or max must be defined');
785
- }
786
- const mediaQueryParts = [];
787
- if (options.min) {
788
- if (typeof options.min === 'number') {
789
- mediaQueryParts.push(`(min-width: ${options.min}px)`);
715
+ const setStyle = (element, style, flags) => {
716
+ Object.entries(style).forEach(([key, value]) => {
717
+ if (!Number.isNaN(Number(key)))
718
+ return;
719
+ if (value !== null && value !== undefined) {
720
+ renderer.setStyle(element, key, value, flags);
790
721
  }
791
722
  else {
792
- mediaQueryParts.push(`(min-width: ${getBreakpointSize(options.min, 'min')}px)`);
723
+ renderer.removeStyle(element, key, flags);
793
724
  }
725
+ });
726
+ };
727
+ const removeStyle = (element, style, flags) => {
728
+ if (!Number.isNaN(Number(style)))
729
+ return;
730
+ renderer.removeStyle(element, style, flags);
731
+ };
732
+ const removeStyles = (element, ...styles) => {
733
+ styles.forEach((style) => removeStyle(element, style));
734
+ };
735
+ const setCssProperty = (element, name, value) => {
736
+ if (value !== null && value !== undefined) {
737
+ renderer.setStyle(element, name, value, RendererStyleFlags2.DashCase);
794
738
  }
795
- if (options.min && options.max) {
796
- mediaQueryParts.push('and');
739
+ else {
740
+ renderer.removeStyle(element, name, RendererStyleFlags2.DashCase);
797
741
  }
798
- if (options.max) {
799
- if (typeof options.max === 'number') {
800
- mediaQueryParts.push(`(max-width: ${options.max}px)`);
801
- }
802
- else {
803
- mediaQueryParts.push(`(max-width: ${getBreakpointSize(options.max, 'max')}px)`);
804
- }
742
+ };
743
+ const setCssProperties = (element, properties) => {
744
+ Object.entries(properties).forEach(([name, value]) => {
745
+ setCssProperty(element, name, value);
746
+ });
747
+ };
748
+ const setAttribute = (element, name, value, namespace) => {
749
+ if (value !== null && value !== undefined) {
750
+ renderer.setAttribute(element, name, value, namespace);
751
+ }
752
+ else {
753
+ renderer.removeAttribute(element, name, namespace);
805
754
  }
806
- return mediaQueryParts.join(' ');
807
755
  };
808
- return {
809
- observeBreakpoint,
810
- isBreakpointMatched,
811
- getBreakpointSize,
812
- buildMediaQueryString,
813
- observeMediaQuery,
814
- isMediaQueryMatched,
756
+ const removeAttribute = (element, ...names) => {
757
+ names.forEach((name) => renderer.removeAttribute(element, name));
815
758
  };
816
- }, { name: 'Breakpoint Observer' });
817
-
818
- const isElementSignal = (el) => {
819
- if (!isSignal(el))
820
- return false;
821
- const val = el();
822
- return typeof val === 'object' && val !== null && 'currentElements' in val && 'previousElements' in val;
823
- };
824
- const createDocumentElementSignal = () => {
825
- const documentElement = inject(DOCUMENT).documentElement;
826
- return signal({
827
- currentElements: [documentElement],
828
- previousElements: [],
829
- });
830
- };
831
- const createEmptyElementSignal = () => signal({
832
- currentElements: [],
833
- previousElements: [],
834
- });
835
- const areElementArraysEqual = (a, b) => {
836
- if (a === b)
759
+ const setProperty = (element, name, value) => {
760
+ renderer.setProperty(element, name, value);
761
+ };
762
+ const setProperties = (element, properties) => {
763
+ Object.entries(properties).forEach(([name, value]) => {
764
+ renderer.setProperty(element, name, value);
765
+ });
766
+ };
767
+ const createElement = (tagName, namespace) => {
768
+ return renderer.createElement(tagName, namespace);
769
+ };
770
+ const createText = (value) => {
771
+ return renderer.createText(value);
772
+ };
773
+ const createComment = (value) => {
774
+ return renderer.createComment(value);
775
+ };
776
+ const appendChild = (parent, newChild) => {
777
+ renderer.appendChild(parent, newChild);
778
+ };
779
+ const insertBefore = (parent, newChild, refChild) => {
780
+ renderer.insertBefore(parent, newChild, refChild);
781
+ };
782
+ const removeChild = (parent, oldChild, isHostElement) => {
783
+ renderer.removeChild(parent, oldChild, isHostElement);
784
+ };
785
+ const selectRootElement = (selectorOrNode, preserveContent) => {
786
+ return renderer.selectRootElement(selectorOrNode, preserveContent);
787
+ };
788
+ const parentNode = (node) => {
789
+ return renderer.parentNode(node);
790
+ };
791
+ const nextSibling = (node) => {
792
+ return renderer.nextSibling(node);
793
+ };
794
+ const setValue = (node, value) => {
795
+ renderer.setValue(node, value);
796
+ };
797
+ const listen = (target, eventName, callback) => {
798
+ return renderer.listen(target, eventName, callback);
799
+ };
800
+ const destroyNode = (node) => {
801
+ if (renderer.destroyNode) {
802
+ renderer.destroyNode(node);
803
+ }
804
+ };
805
+ const replaceChild = (parent, newChild, oldChild) => {
806
+ renderer.insertBefore(parent, newChild, oldChild);
807
+ renderer.removeChild(parent, oldChild);
808
+ };
809
+ const empty = (element) => {
810
+ while (element.firstChild) {
811
+ renderer.removeChild(element, element.firstChild);
812
+ }
813
+ };
814
+ const setTextContent = (element, text) => {
815
+ renderer.setProperty(element, 'textContent', text);
816
+ };
817
+ const setInnerHTML = (element, html) => {
818
+ renderer.setProperty(element, 'innerHTML', html);
819
+ };
820
+ const setAttributes = (element, attributes, namespace) => {
821
+ Object.entries(attributes).forEach(([name, value]) => {
822
+ setAttribute(element, name, value, namespace);
823
+ });
824
+ };
825
+ const setDataAttributes = (element, data) => {
826
+ Object.entries(data).forEach(([key, value]) => {
827
+ setAttribute(element, `data-${key}`, value);
828
+ });
829
+ };
830
+ const moveBefore = (config) => {
831
+ const { newParent, child, before = null } = config;
832
+ if ('moveBefore' in newParent) {
833
+ try {
834
+ newParent.moveBefore(child, before);
835
+ }
836
+ catch {
837
+ newParent.insertBefore(child, before);
838
+ }
839
+ }
840
+ else {
841
+ newParent.insertBefore(child, before);
842
+ }
843
+ };
844
+ return {
845
+ addClass,
846
+ removeClass,
847
+ toggleClass,
848
+ setStyle,
849
+ removeStyle,
850
+ removeStyles,
851
+ setAttribute,
852
+ removeAttribute,
853
+ setProperty,
854
+ setProperties,
855
+ createElement,
856
+ createText,
857
+ createComment,
858
+ appendChild,
859
+ insertBefore,
860
+ removeChild,
861
+ selectRootElement,
862
+ parentNode,
863
+ nextSibling,
864
+ setValue,
865
+ listen,
866
+ destroyNode,
867
+ replaceChild,
868
+ empty,
869
+ setTextContent,
870
+ setInnerHTML,
871
+ setAttributes,
872
+ setDataAttributes,
873
+ setCssProperty,
874
+ setCssProperties,
875
+ moveBefore,
876
+ renderer,
877
+ };
878
+ }, { name: 'Angular Renderer' });
879
+
880
+ const isElementSignal = (el) => {
881
+ if (!isSignal(el))
882
+ return false;
883
+ const val = el();
884
+ return typeof val === 'object' && val !== null && 'currentElements' in val && 'previousElements' in val;
885
+ };
886
+ const createDocumentElementSignal = () => {
887
+ const documentElement = inject(DOCUMENT).documentElement;
888
+ return signal({
889
+ currentElements: [documentElement],
890
+ previousElements: [],
891
+ });
892
+ };
893
+ const createEmptyElementSignal = () => signal({
894
+ currentElements: [],
895
+ previousElements: [],
896
+ });
897
+ const areElementArraysEqual = (a, b) => {
898
+ if (a === b)
837
899
  return true;
838
900
  if (a.length !== b.length)
839
901
  return false;
@@ -1255,6 +1317,251 @@ const injectScrollbarDimensions = memoizeSignal(() => {
1255
1317
  });
1256
1318
  });
1257
1319
 
1320
+ /**
1321
+ * Smoothly animates an element's `block-size` when its content changes size (e.g. a list filtering,
1322
+ * a loading state resolving). Set up once in an injection context; it observes the given content
1323
+ * element(s) and animates the host from its previous height to its new natural height.
1324
+ *
1325
+ * Robustness notes: the baseline height is captured on the first render (via `afterNextRender`) and
1326
+ * the effect only animates after that, so the initial layout settling never plays as a grow-from-0.
1327
+ * A change that interrupts an in-flight animation continues from the current animated height, and
1328
+ * zero/pre-layout measurements are ignored. Respects `prefers-reduced-motion`.
1329
+ */
1330
+ const injectAnimatedBlockSize = (config) => {
1331
+ const hostBinding = config.host ?? inject(ElementRef);
1332
+ const renderer = injectRenderer();
1333
+ const prefersReducedMotion = injectPrefersReducedMotion();
1334
+ const destroyRef = inject(DestroyRef);
1335
+ const hostSignal = firstElementSignal(buildElementSignal(hostBinding));
1336
+ const observeTargets = Array.isArray(config.observe) ? config.observe : [config.observe];
1337
+ const dimensionSignals = observeTargets.map((target) => signalElementDimensions(target));
1338
+ const duration = config.duration ?? 160;
1339
+ const easing = config.easing ?? 'ease';
1340
+ let lastBlockSize = null;
1341
+ let animation = null;
1342
+ let ready = false;
1343
+ const hostElement = () => hostSignal().currentElement;
1344
+ const run = () => {
1345
+ const host = hostElement();
1346
+ if (!host)
1347
+ return;
1348
+ // continue from the current animated height when interrupting a running animation
1349
+ const from = animation?.playState === 'running' ? host.getBoundingClientRect().height : lastBlockSize;
1350
+ animation?.cancel();
1351
+ animation = null;
1352
+ const to = host.getBoundingClientRect().height;
1353
+ // never record or animate a zero / pre-layout height
1354
+ if (to === 0)
1355
+ return;
1356
+ lastBlockSize = to;
1357
+ if (from === null || from < 1 || Math.abs(from - to) < 1 || prefersReducedMotion()) {
1358
+ return;
1359
+ }
1360
+ if (config.resizingClass && renderer) {
1361
+ renderer.addClass(host, config.resizingClass);
1362
+ }
1363
+ const nextAnimation = host.animate([{ blockSize: `${from}px` }, { blockSize: `${to}px` }], { duration, easing });
1364
+ const cleanup = () => {
1365
+ if (config.resizingClass && renderer) {
1366
+ renderer.removeClass(host, config.resizingClass);
1367
+ }
1368
+ if (animation === nextAnimation) {
1369
+ animation = null;
1370
+ }
1371
+ };
1372
+ nextAnimation.finished.then(cleanup).catch(cleanup);
1373
+ animation = nextAnimation;
1374
+ };
1375
+ // Capture the settled height of the first render as the baseline, then only animate later changes.
1376
+ afterNextRender(() => {
1377
+ const host = hostElement();
1378
+ lastBlockSize = host ? host.getBoundingClientRect().height || null : null;
1379
+ ready = true;
1380
+ });
1381
+ effect(() => {
1382
+ // track every observed element's size
1383
+ dimensionSignals.forEach((dimensions) => dimensions());
1384
+ if (!ready)
1385
+ return;
1386
+ untracked(() => run());
1387
+ });
1388
+ destroyRef.onDestroy(() => animation?.cancel());
1389
+ };
1390
+
1391
+ /**
1392
+ * Creates a signal that animates the transition of a numeric signal's value over time.
1393
+ */
1394
+ const signalAnimatedNumber = (source, options = {}) => {
1395
+ const ngZone = inject(NgZone);
1396
+ const duration = options.duration ?? 1000;
1397
+ const easing = options.easing ?? easeOut;
1398
+ const round = options.round ?? ((v) => Math.round(v));
1399
+ const initialValue = options.initialValue ?? 0;
1400
+ const animatedValue = signal(initialValue, /* @ts-ignore */
1401
+ ...(ngDevMode ? [{ debugName: "animatedValue" }] : /* istanbul ignore next */ []));
1402
+ let rafId = null;
1403
+ let currentAnimatedValue = initialValue;
1404
+ const cancelAnimation = () => {
1405
+ if (rafId !== null) {
1406
+ cancelAnimationFrame(rafId);
1407
+ rafId = null;
1408
+ }
1409
+ };
1410
+ const readonlySignal = animatedValue.asReadonly();
1411
+ const play = () => {
1412
+ const targetValue = isSignal(source) ? source() : source;
1413
+ cancelAnimation();
1414
+ const startValue = currentAnimatedValue;
1415
+ const startTime = performance.now();
1416
+ const delta = targetValue - startValue;
1417
+ // Skip animation if no change
1418
+ if (delta === 0) {
1419
+ options.onAnimationEnd?.();
1420
+ return readonlySignal;
1421
+ }
1422
+ options.onAnimationStart?.();
1423
+ ngZone.runOutsideAngular(() => {
1424
+ const animate = (timestamp) => {
1425
+ const elapsed = timestamp - startTime;
1426
+ const progress = Math.min(elapsed / duration, 1);
1427
+ const easedProgress = easing(progress);
1428
+ const value = startValue + delta * easedProgress;
1429
+ currentAnimatedValue = value;
1430
+ untracked(() => {
1431
+ animatedValue.set(round(value));
1432
+ });
1433
+ if (progress < 1) {
1434
+ rafId = requestAnimationFrame(animate);
1435
+ }
1436
+ else {
1437
+ rafId = null;
1438
+ ngZone.run(() => {
1439
+ options.onAnimationEnd?.();
1440
+ });
1441
+ }
1442
+ };
1443
+ rafId = requestAnimationFrame(animate);
1444
+ });
1445
+ return readonlySignal;
1446
+ };
1447
+ const reset = () => {
1448
+ cancelAnimation();
1449
+ currentAnimatedValue = initialValue;
1450
+ animatedValue.set(initialValue);
1451
+ return readonlySignal;
1452
+ };
1453
+ const stop = () => {
1454
+ cancelAnimation();
1455
+ return readonlySignal;
1456
+ };
1457
+ readonlySignal.play = play;
1458
+ readonlySignal.reset = reset;
1459
+ readonlySignal.stop = stop;
1460
+ return readonlySignal;
1461
+ };
1462
+ /* Linear easing function */
1463
+ const easeLinear = (t) => t;
1464
+ /* Ease in (slow start) */
1465
+ const easeIn = (t) => t * t;
1466
+ /* Ease out (slow end) */
1467
+ const easeOut = (t) => t * (2 - t);
1468
+ /* Ease in-out (slow start and end) */
1469
+ const easeInOut = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
1470
+ /** Elastic easing function (overshoots) */
1471
+ const easeElastic = (t) => {
1472
+ const c4 = (2 * Math.PI) / 3;
1473
+ return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
1474
+ };
1475
+ /** Back easing function (overshoots slightly then settles) */
1476
+ const easeOutBack = (t) => {
1477
+ const c1 = 1.70158;
1478
+ const c3 = c1 + 1;
1479
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
1480
+ };
1481
+ /** Back easing function with more overshoot */
1482
+ const easeOutBackStrong = (t) => {
1483
+ const c1 = 2.5;
1484
+ const c3 = c1 + 1;
1485
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
1486
+ };
1487
+
1488
+ const BREAKPOINT_ORDER = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
1489
+ /**
1490
+ * Default viewport config based on Tailwind CSS.
1491
+ * @see https://tailwindcss.com/docs/screens
1492
+ */
1493
+ const DEFAULT_VIEWPORT_CONFIG = {
1494
+ breakpoints: {
1495
+ xs: [0, 639],
1496
+ sm: [640, 767],
1497
+ md: [768, 1023],
1498
+ lg: [1024, 1279],
1499
+ xl: [1280, 1535],
1500
+ '2xl': [1536, Infinity],
1501
+ },
1502
+ };
1503
+ const [provideViewportConfig, injectViewportConfig] = createStaticRootProvider(DEFAULT_VIEWPORT_CONFIG, {
1504
+ name: 'Viewport Config',
1505
+ });
1506
+ const [provideBreakpointObserver, injectBreakpointObserver] = createRootProvider(() => {
1507
+ const breakpointObserver = inject(BreakpointObserver);
1508
+ const viewportConfig = injectViewportConfig();
1509
+ const isMediaQueryMatched = (mediaQuery) => breakpointObserver.isMatched(mediaQuery);
1510
+ const observeMediaQuery = (mediaQuery) => {
1511
+ return toSignal(breakpointObserver.observe(mediaQuery).pipe(map((x) => x.matches), startWith(isMediaQueryMatched(mediaQuery))), { requireSync: true });
1512
+ };
1513
+ const observeBreakpoint = (options) => observeMediaQuery(buildMediaQueryString(options));
1514
+ const isBreakpointMatched = (options) => isMediaQueryMatched(buildMediaQueryString(options));
1515
+ const getBreakpointSize = (type, option) => {
1516
+ const index = option === 'min' ? 0 : 1;
1517
+ const size = viewportConfig.breakpoints[type][index];
1518
+ if (size === Infinity || size === 0) {
1519
+ return size;
1520
+ }
1521
+ if (option === 'min') {
1522
+ return size;
1523
+ }
1524
+ // Due to scaling, the actual size of the viewport may be a decimal number.
1525
+ // Eg. on Windows 11 with 150% scaling, the viewport size may be 1535.33px
1526
+ // and thus not matching any of the default breakpoints.
1527
+ return size + 0.9;
1528
+ };
1529
+ const buildMediaQueryString = (options) => {
1530
+ if (!options.min && !options.max) {
1531
+ throw new Error('At least one of min or max must be defined');
1532
+ }
1533
+ const mediaQueryParts = [];
1534
+ if (options.min) {
1535
+ if (typeof options.min === 'number') {
1536
+ mediaQueryParts.push(`(min-width: ${options.min}px)`);
1537
+ }
1538
+ else {
1539
+ mediaQueryParts.push(`(min-width: ${getBreakpointSize(options.min, 'min')}px)`);
1540
+ }
1541
+ }
1542
+ if (options.min && options.max) {
1543
+ mediaQueryParts.push('and');
1544
+ }
1545
+ if (options.max) {
1546
+ if (typeof options.max === 'number') {
1547
+ mediaQueryParts.push(`(max-width: ${options.max}px)`);
1548
+ }
1549
+ else {
1550
+ mediaQueryParts.push(`(max-width: ${getBreakpointSize(options.max, 'max')}px)`);
1551
+ }
1552
+ }
1553
+ return mediaQueryParts.join(' ');
1554
+ };
1555
+ return {
1556
+ observeBreakpoint,
1557
+ isBreakpointMatched,
1558
+ getBreakpointSize,
1559
+ buildMediaQueryString,
1560
+ observeMediaQuery,
1561
+ isMediaQueryMatched,
1562
+ };
1563
+ }, { name: 'Breakpoint Observer' });
1564
+
1258
1565
  const BREAKPOINT_KEY_SET = new Set(BREAKPOINT_ORDER);
1259
1566
  const isBreakpointMap = (value) => {
1260
1567
  if (value === null || typeof value !== 'object' || Array.isArray(value))
@@ -2643,6 +2950,20 @@ const createLogger = (config) => {
2643
2950
  };
2644
2951
 
2645
2952
  const escapeHtml = (str) => str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2953
+ /** Escapes HTML-special characters but leaves existing entity references intact — for input that
2954
+ * may already contain entities (e.g. Markdown serialized from the editor's DOM, where `&` is
2955
+ * stored as `&amp;`), where plain escaping would double-escape them. */
2956
+ const escapeHtmlPreservingEntities = (str) => str
2957
+ .replace(/&(?![a-z]+;|#\d+;|#x[0-9a-f]+;)/gi, '&amp;')
2958
+ .replace(/</g, '&lt;')
2959
+ .replace(/>/g, '&gt;')
2960
+ .replace(/"/g, '&quot;');
2961
+ /** Rejects URL schemes that execute script when navigated. Whitespace/control characters are
2962
+ * stripped before checking — browsers ignore them inside URLs, so `java\tscript:` would
2963
+ * otherwise slip through. */
2964
+ const isSafeUrl = (url) =>
2965
+ // eslint-disable-next-line no-control-regex -- stripping control characters is the point: browsers ignore them inside URLs
2966
+ !/^(javascript|data|vbscript):/i.test(url.replace(/[\s\u0000-\u001f]/g, ''));
2646
2967
  const unescapeHtml = (str) => str
2647
2968
  .replace(/&amp;/g, '&')
2648
2969
  .replace(/&lt;/g, '<')
@@ -2656,30 +2977,158 @@ const parseTableRow = (line) => line
2656
2977
  .slice(1, -1)
2657
2978
  .map((cell) => cell.trim());
2658
2979
  const isTableSeparatorLine = (line) => /^\|?(\s*:?-+:?\s*\|)+\s*$/.test(line);
2980
+ /** Column alignments from a GFM separator line: `:---` left, `:---:` center, `---:` right. */
2981
+ const parseSeparatorAligns = (line) => parseTableRow(line).map((cell) => {
2982
+ const left = cell.startsWith(':');
2983
+ const right = cell.endsWith(':');
2984
+ return left && right ? 'center' : right ? 'right' : left ? 'left' : null;
2985
+ });
2986
+ /** The GFM separator token for a column alignment. */
2987
+ const separatorFor = (align) => align === 'center' ? ':---:' : align === 'right' ? '---:' : align === 'left' ? ':---' : '---';
2988
+ const alignStyle = (align) => (align ? ` style="text-align: ${align}"` : '');
2659
2989
  const makePlaceholder = (kind, idx) => `\u{E000}${kind}${idx}\u{E001}`;
2660
2990
  const placeholderRe = (kind) => new RegExp(`\u{E000}${kind}(\\d+)\u{E001}`, 'gu');
2661
2991
  const isPlaceholder = (kind, str) => new RegExp(`^\u{E000}${kind}\\d+\u{E001}$`, 'u').test(str);
2992
+ /** Tags allowed to survive inside an aligned block's raw-HTML passthrough. */
2993
+ const SAFE_INLINE_TAGS = new Set(['strong', 'em', 'b', 'i', 'del', 's', 'u', 'code', 'br']);
2994
+ /** Reduces raw inline HTML to the editor's own vocabulary: allowed tags lose all their attributes
2995
+ * (`<a>` keeps a safe `href`), everything else — including any event-handler attribute — is
2996
+ * dropped, and the remaining text is escaped. */
2997
+ const sanitizeInlineHtml = (html) => {
2998
+ const kept = [];
2999
+ const stashed = html.replace(/<\s*(\/?)\s*([a-z][a-z0-9]*)\b[^>]*>/gi, (full, closing, tag) => {
3000
+ const name = tag.toLowerCase();
3001
+ if (name === 'a') {
3002
+ const href = closing ? null : /\bhref\s*=\s*"([^"]*)"/i.exec(full)?.[1];
3003
+ const anchor = closing ? '</a>' : href && isSafeUrl(href) ? `<a href="${href}">` : '<a>';
3004
+ return makePlaceholder('TAG', kept.push(anchor) - 1);
3005
+ }
3006
+ if (!SAFE_INLINE_TAGS.has(name))
3007
+ return '';
3008
+ return makePlaceholder('TAG', kept.push(closing ? `</${name}>` : `<${name}>`) - 1);
3009
+ });
3010
+ return escapeHtmlPreservingEntities(stashed).replace(placeholderRe('TAG'), (_, i) => kept[+i] ?? '');
3011
+ };
2662
3012
  const processInline = (text) => {
2663
3013
  const inlineCodes = [];
2664
3014
  text = text.replace(/`([^`]+)`/g, (_, code) => {
2665
3015
  const idx = inlineCodes.push(`<code>${escapeHtml(code)}</code>`) - 1;
2666
3016
  return makePlaceholder('IC', idx);
2667
3017
  });
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>');
3018
+ // Raw HTML in Markdown text is escaped, not rendered — the editor writes this HTML straight
3019
+ // into the DOM via innerHTML, so anything else would let a crafted value inject markup. `<u>`
3020
+ // is the one deliberate exception: underline has no Markdown form and round-trips as raw <u>.
3021
+ text = escapeHtmlPreservingEntities(text).replace(/&lt;(\/?)u&gt;/gi, '<$1u>');
3022
+ // Bold + italic — *** before ** before *. A delimiter flanking whitespace doesn't open/close a
3023
+ // run (CommonMark's flanking rule), so literal asterisks as in `2 * 3 * 4` stay text.
3024
+ text = text.replace(/\*\*\*([^\s*](?:.*?[^\s*])?)\*\*\*/g, '<strong><em>$1</em></strong>');
3025
+ text = text.replace(/\*\*([^\s*](?:.*?[^\s*])?)\*\*/g, '<strong>$1</strong>');
3026
+ text = text.replace(/__([^\s_](?:.*?[^\s_])?)__/g, '<strong>$1</strong>');
3027
+ text = text.replace(/\*([^\s*](?:.*?[^\s*])?)\*/g, '<em>$1</em>');
3028
+ // Single-underscore emphasis must not fire inside a word (snake_case identifiers).
3029
+ text = text.replace(/(?<![\w*_])_([^\s_](?:.*?[^\s_])?)_(?![\w_])/g, '<em>$1</em>');
3030
+ text = text.replace(/~~([^\s~](?:.*?[^\s~])?)~~/g, '<del>$1</del>');
3031
+ // Images before links (order matters); URLs with script-running schemes stay literal text
3032
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => isSafeUrl(src) ? `<img src="${src}" alt="${alt}">` : match);
3033
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, href) => isSafeUrl(href) ? `<a href="${href}">${label}</a>` : match);
2677
3034
  return text.replace(placeholderRe('IC'), (_, i) => inlineCodes[+i] ?? '');
2678
3035
  };
3036
+ // --- Nested list helpers (regex can't match balanced nesting, so these scan by tag depth) ---
3037
+ /** The first top-level `<ul>`/`<ol>` in `html`, matched with balanced nesting. */
3038
+ const findList = (html) => {
3039
+ const open = /<(ul|ol)\b[^>]*>/i.exec(html);
3040
+ if (!open)
3041
+ return null;
3042
+ const innerStart = open.index + open[0].length;
3043
+ const tag = /<(\/?)(?:ul|ol)\b[^>]*>/gi;
3044
+ tag.lastIndex = innerStart;
3045
+ let depth = 1;
3046
+ let match;
3047
+ while ((match = tag.exec(html))) {
3048
+ depth += match[1] ? -1 : 1;
3049
+ if (depth === 0) {
3050
+ return {
3051
+ start: open.index,
3052
+ end: tag.lastIndex,
3053
+ ordered: open[1].toLowerCase() === 'ol',
3054
+ inner: html.slice(innerStart, match.index),
3055
+ };
3056
+ }
3057
+ }
3058
+ return null;
3059
+ };
3060
+ /** The inner HTML of each direct `<li>` child of a list's inner HTML (balanced over nested lists). */
3061
+ const findListItems = (inner) => {
3062
+ const items = [];
3063
+ const tag = /<(\/?)li\b[^>]*>/gi;
3064
+ let depth = 0;
3065
+ let start = -1;
3066
+ let match;
3067
+ while ((match = tag.exec(inner))) {
3068
+ if (match[1]) {
3069
+ depth--;
3070
+ if (depth === 0 && start >= 0) {
3071
+ items.push(inner.slice(start, match.index));
3072
+ start = -1;
3073
+ }
3074
+ }
3075
+ else {
3076
+ if (depth === 0)
3077
+ start = match.index + match[0].length;
3078
+ depth++;
3079
+ }
3080
+ }
3081
+ return items;
3082
+ };
3083
+ /** Serializes a list's inner HTML to Markdown, indenting nested lists two spaces per level. */
3084
+ const listToMarkdown = (inner, ordered, depth) => {
3085
+ const indent = ' '.repeat(depth);
3086
+ let n = 1;
3087
+ return findListItems(inner)
3088
+ .map((itemInner) => {
3089
+ let content = itemInner;
3090
+ let nestedMarkdown = '';
3091
+ for (let nested = findList(content); nested; nested = findList(content)) {
3092
+ nestedMarkdown += `\n${listToMarkdown(nested.inner, nested.ordered, depth + 1)}`;
3093
+ content = content.slice(0, nested.start) + content.slice(nested.end);
3094
+ }
3095
+ const marker = ordered ? `${n++}. ` : '- ';
3096
+ // list items are single-line in this serializer, so a <br> degrades to a space
3097
+ return `${indent}${marker}${stripTags(content.replace(/<br\s*\/?>/gi, ' ')).trim()}${nestedMarkdown}`;
3098
+ })
3099
+ .join('\n');
3100
+ };
3101
+ /** Parses list lines into (indent-level, ordered, text) — two leading spaces per nesting level. */
3102
+ const parseListLines = (lines) => lines
3103
+ .map((line) => {
3104
+ const match = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line);
3105
+ return match
3106
+ ? { indent: Math.floor((match[1] ?? '').length / 2), ordered: /\d/.test(match[2] ?? ''), text: match[3] ?? '' }
3107
+ : null;
3108
+ })
3109
+ .filter((line) => line !== null);
3110
+ /** Builds nested `<ul>`/`<ol>` HTML from parsed list lines starting at `start`, for one indent level. */
3111
+ const buildListHtml = (lines, start, baseIndent) => {
3112
+ const ordered = lines[start]?.ordered ?? false;
3113
+ let items = '';
3114
+ let i = start;
3115
+ while (i < lines.length && (lines[i]?.indent ?? -1) === baseIndent) {
3116
+ let item = processInline(lines[i].text);
3117
+ i++;
3118
+ if (i < lines.length && (lines[i]?.indent ?? -1) > baseIndent) {
3119
+ const nested = buildListHtml(lines, i, lines[i].indent);
3120
+ item += nested.html;
3121
+ i = nested.next;
3122
+ }
3123
+ items += `<li>${item}</li>`;
3124
+ }
3125
+ const tag = ordered ? 'ol' : 'ul';
3126
+ return { html: `<${tag}>${items}</${tag}>`, next: i };
3127
+ };
2679
3128
  /**
2680
3129
  * Converts a markdown string to HTML.
2681
3130
  * Covers headings, bold, italic, strikethrough, inline code, fenced code blocks,
2682
- * links, images, block quotes, unordered/ordered lists, tables, horizontal rules, and paragraphs.
3131
+ * links, images, block quotes, unordered/ordered (and nested) lists, tables, horizontal rules, and paragraphs.
2683
3132
  */
2684
3133
  const markdownToHtml = (markdown) => {
2685
3134
  if (!markdown)
@@ -2704,6 +3153,16 @@ const markdownToHtml = (markdown) => {
2704
3153
  return '';
2705
3154
  if (isPlaceholder('CODE', trimmed))
2706
3155
  return trimmed;
3156
+ // Aligned block: text-align has no Markdown form, so it round-trips as raw native HTML.
3157
+ // Rebuild it instead of passing it through verbatim: only the alignment survives on the tag
3158
+ // (no other attributes, e.g. event handlers) and the inner markup is reduced to the editor's
3159
+ // own inline vocabulary.
3160
+ const aligned = /^<(p|h[1-6]|div)\b[^>]*\bstyle=["'][^"']*text-align:\s*([a-z]+)[^>]*>([\s\S]*)<\/\1>$/i.exec(trimmed);
3161
+ if (aligned) {
3162
+ const tag = (aligned[1] ?? 'p').toLowerCase();
3163
+ const align = (aligned[2] ?? 'left').toLowerCase();
3164
+ return `<${tag} style="text-align: ${align}">${sanitizeInlineHtml(aligned[3] ?? '')}</${tag}>`;
3165
+ }
2707
3166
  // Heading
2708
3167
  const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
2709
3168
  if (headingMatch?.[1] && headingMatch?.[2]) {
@@ -2713,46 +3172,39 @@ const markdownToHtml = (markdown) => {
2713
3172
  // Horizontal rule
2714
3173
  if (/^(---|\*\*\*|___)\s*$/.test(trimmed))
2715
3174
  return '<hr>';
2716
- // Table (GFM)
3175
+ // Table (GFM) — column alignment from the separator line lands as text-align on every cell
2717
3176
  const tableLines = trimmed.split('\n');
2718
3177
  if (tableLines.length >= 2 && /\|/.test(tableLines[0] ?? '') && isTableSeparatorLine(tableLines[1] ?? '')) {
3178
+ const aligns = parseSeparatorAligns(tableLines[1] ?? '');
2719
3179
  const headers = parseTableRow(tableLines[0] ?? '');
2720
- const thead = `<thead><tr>${headers.map((h) => `<th>${processInline(h)}</th>`).join('')}</tr></thead>`;
3180
+ const thead = `<thead><tr>${headers
3181
+ .map((h, i) => `<th${alignStyle(aligns[i] ?? null)}>${processInline(h)}</th>`)
3182
+ .join('')}</tr></thead>`;
2721
3183
  const bodyRows = tableLines.slice(2);
2722
3184
  const tbody = bodyRows.length > 0
2723
3185
  ? `<tbody>${bodyRows
2724
3186
  .map((row) => `<tr>${parseTableRow(row)
2725
- .map((cell) => `<td>${processInline(cell)}</td>`)
3187
+ .map((cell, i) => `<td${alignStyle(aligns[i] ?? null)}>${processInline(cell)}</td>`)
2726
3188
  .join('')}</tr>`)
2727
3189
  .join('')}</tbody>`
2728
3190
  : '';
2729
3191
  return `<table>${thead}${tbody}</table>`;
2730
3192
  }
2731
- // Blockquote
3193
+ // Blockquote — each line runs through the inline pass on its own (processInline escapes raw
3194
+ // HTML, so joining first would escape the <br> separators too)
2732
3195
  if (/^> /.test(trimmed)) {
2733
3196
  const content = trimmed
2734
3197
  .split('\n')
2735
- .map((l) => l.replace(/^>\s?/, ''))
3198
+ .map((l) => processInline(l.replace(/^>\s?/, '')))
2736
3199
  .join('<br>');
2737
- return `<blockquote>${processInline(content)}</blockquote>`;
3200
+ return `<blockquote>${content}</blockquote>`;
2738
3201
  }
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>`;
3202
+ // List (unordered/ordered, with indentation-based nesting)
3203
+ if (/^([-*+]|\d+\.)\s/.test(trimmed)) {
3204
+ const lines = parseListLines(trimmed.split('\n'));
3205
+ if (lines.length > 0) {
3206
+ return buildListHtml(lines, 0, lines[0].indent).html;
3207
+ }
2756
3208
  }
2757
3209
  // Paragraph — single newlines within a block become <br>
2758
3210
  return `<p>${trimmed
@@ -2773,6 +3225,11 @@ const htmlToMarkdown = (html) => {
2773
3225
  if (!html)
2774
3226
  return '';
2775
3227
  let md = html;
3228
+ // Aligned blocks: text-align has no Markdown form, so preserve them verbatim as native HTML (their
3229
+ // inner markup stays HTML) and round-trip via a placeholder — extracted before the block passes
3230
+ // below rewrite them, restored after the final tag-strip.
3231
+ const alignedBlocks = [];
3232
+ md = md.replace(/<(p|h[1-6]|div)\b[^>]*\bstyle="[^"]*text-align[^"]*"[^>]*>[\s\S]*?<\/\1>/gi, (match) => makePlaceholder('ALIGN', alignedBlocks.push(match) - 1));
2776
3233
  // Code blocks — process before inline code
2777
3234
  md = md.replace(/<pre[^>]*><code[^>]*class="language-([^"]+)"[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, lang, code) => `\n\`\`\`${lang}\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
2778
3235
  md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, code) => `\n\`\`\`\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
@@ -2795,37 +3252,40 @@ const htmlToMarkdown = (html) => {
2795
3252
  md = md.replace(/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '![$2]($1)');
2796
3253
  md = md.replace(/<img[^>]+alt="([^"]*)"[^>]*src="([^"]*)"[^>]*\/?>/gi, '![$1]($2)');
2797
3254
  md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, '![]($1)');
2798
- // Block quotes
3255
+ // Underline has no Markdown form — preserve it as raw <u> so it round-trips. Extract it now (its
3256
+ // inner markup is already converted above) so the block passes below don't strip the tag; the
3257
+ // placeholder is restored after the final tag-strip.
3258
+ const underlines = [];
3259
+ md = md.replace(/<u[^>]*>([\s\S]*?)<\/u>/gi, (_, inner) => makePlaceholder('U', underlines.push(inner) - 1));
3260
+ // Block quotes — <br> line breaks become one quoted line each (stripTags would swallow them)
2799
3261
  md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
2800
- return (stripTags(content)
3262
+ return (stripTags(content.replace(/<br\s*\/?>/gi, '\n'))
2801
3263
  .trim()
2802
3264
  .split('\n')
2803
3265
  .map((line) => `> ${line.trim()}`)
2804
3266
  .join('\n') + '\n');
2805
3267
  });
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
- });
3268
+ // Lists — replace each top-level list with its recursively-serialized Markdown (handles nesting).
3269
+ for (let list = findList(md); list; list = findList(md)) {
3270
+ md = `${md.slice(0, list.start)}\n${listToMarkdown(list.inner, list.ordered, 0)}\n${md.slice(list.end)}`;
3271
+ }
2816
3272
  // Tables
2817
3273
  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());
3274
+ // GFM cells are single-line, so a <br> inside one degrades to a space (not silently dropped)
3275
+ const extractCells = (row) => [...row.matchAll(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi)].map((m) => stripTags((m[1] ?? '').replace(/<br\s*\/?>/gi, ' ')).trim());
3276
+ // GFM alignment is per column, read from the header cells' text-align styles
3277
+ const extractAligns = (row) => [...row.matchAll(/<t[hd]([^>]*)>/gi)].map((m) => {
3278
+ const align = /text-align:\s*(left|center|right)/i.exec(m[1] ?? '')?.[1]?.toLowerCase();
3279
+ return align ?? null;
3280
+ });
2819
3281
  const theadMatch = tableContent.match(/<thead[^>]*>([\s\S]*?)<\/thead>/i);
2820
3282
  const firstTrMatch = tableContent.match(/<tr[^>]*>([\s\S]*?)<\/tr>/i);
2821
- const headerCells = theadMatch
2822
- ? extractCells(theadMatch[1] ?? '')
2823
- : firstTrMatch
2824
- ? extractCells(firstTrMatch[1] ?? '')
2825
- : [];
3283
+ const headerRow = theadMatch?.[1] ?? firstTrMatch?.[1] ?? '';
3284
+ const headerCells = headerRow ? extractCells(headerRow) : [];
2826
3285
  if (headerCells.length === 0)
2827
3286
  return '';
2828
- const separator = headerCells.map(() => '---').join(' | ');
3287
+ const aligns = extractAligns(headerRow);
3288
+ const separator = headerCells.map((_cell, i) => separatorFor(aligns[i] ?? null)).join(' | ');
2829
3289
  const bodySource = theadMatch
2830
3290
  ? tableContent.replace(theadMatch[0], '')
2831
3291
  : tableContent.replace(firstTrMatch?.[0] ?? '', '');
@@ -2833,13 +3293,19 @@ const htmlToMarkdown = (html) => {
2833
3293
  const lines = [`| ${headerCells.join(' | ')} |`, `| ${separator} |`, ...bodyRows.map((r) => `| ${r} |`)];
2834
3294
  return `\n${lines.join('\n')}\n`;
2835
3295
  });
2836
- // Block elements
2837
- md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, content) => `\n${stripTags(content).trim()}\n`);
3296
+ // Block elements — a <br> inside a paragraph is a soft line break; turn it into a newline before
3297
+ // the tag-strip removes it, so it round-trips (a single newline within a block renders as <br>)
3298
+ md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, content) => `\n${stripTags(content.replace(/<br\s*\/?>/gi, '\n')).trim()}\n`);
3299
+ // A <div> boundary acts as a paragraph boundary — clipboard HTML commonly uses divs as
3300
+ // paragraphs, and silently stripping them would merge adjacent blocks into one.
3301
+ md = md.replace(/<\/div\s*>/gi, '\n\n').replace(/<div[^>]*>/gi, '\n\n');
2838
3302
  md = md.replace(/<br\s*\/?>/gi, '\n');
2839
3303
  md = md.replace(/<hr\s*\/?>/gi, '\n---\n');
2840
- return unescapeHtml(stripTags(md))
2841
- .replace(/\n{3,}/g, '\n\n')
2842
- .trim();
3304
+ md = unescapeHtml(stripTags(md)).replace(placeholderRe('U'), (_, i) => `<u>${underlines[+i] ?? ''}</u>`);
3305
+ // aligned blocks are block-level, so pad with blank lines before collapsing so they survive as
3306
+ // their own Markdown block
3307
+ md = md.replace(placeholderRe('ALIGN'), (_, i) => `\n\n${alignedBlocks[+i] ?? ''}\n\n`);
3308
+ return md.replace(/\n{3,}/g, '\n\n').trim();
2843
3309
  };
2844
3310
 
2845
3311
  const clamp = (value, min = 0, max = 100) => {
@@ -3145,190 +3611,6 @@ const [provideLocale, injectLocale] = createRootProvider(() => {
3145
3611
  name: 'Locale',
3146
3612
  });
3147
3613
 
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
3614
  const [provideStyleManager, injectStyleManager] = createRootProvider(() => {
3333
3615
  const appRef = inject(ApplicationRef);
3334
3616
  const destroyRef = inject(DestroyRef);
@@ -4596,12 +4878,14 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
4596
4878
  middleware.push(flip({
4597
4879
  fallbackPlacements: strategy.fallbackPlacements ?? undefined,
4598
4880
  fallbackAxisSideDirection: 'start',
4881
+ boundary: strategy.boundary,
4599
4882
  }));
4600
4883
  if (strategy.shift !== false) {
4601
4884
  middleware.push(shift({
4602
4885
  crossAxis: typeof strategy.shift === 'object' ? (strategy.shift.crossAxis ?? false) : false,
4603
4886
  limiter: limitShift(),
4604
4887
  padding: strategy.viewportPadding ?? 8,
4888
+ boundary: strategy.boundary,
4605
4889
  }));
4606
4890
  }
4607
4891
  // size must run AFTER shift so the available space is measured from the shifted position —
@@ -5569,6 +5853,43 @@ const inferMimeType = (srcset) => {
5569
5853
  return null;
5570
5854
  };
5571
5855
 
5856
+ class InitialsPipe {
5857
+ constructor() {
5858
+ this.transform = initials;
5859
+ }
5860
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: InitialsPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
5861
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.5", ngImport: i0, type: InitialsPipe, isStandalone: true, name: "initials" }); }
5862
+ }
5863
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: InitialsPipe, decorators: [{
5864
+ type: Pipe,
5865
+ args: [{ name: 'initials' }]
5866
+ }] });
5867
+ /**
5868
+ * Extracts the initials from a name (or any whitespace separated string).
5869
+ *
5870
+ * Words are split on whitespace, and the first character of each word is taken.
5871
+ * At most `maxLength` initials are returned (defaults to 2), always uppercased.
5872
+ *
5873
+ * @example
5874
+ * initials('John Doe') // 'JD'
5875
+ * initials('john doe smith') // 'JD'
5876
+ * initials('john doe smith', 3) // 'JDS'
5877
+ * initials('Madonna') // 'M'
5878
+ * initials(' ') // ''
5879
+ * initials(null) // ''
5880
+ */
5881
+ const initials = (value, maxLength = 2) => {
5882
+ if (!value)
5883
+ return '';
5884
+ const words = value.trim().split(/\s+/).filter(Boolean);
5885
+ if (!words.length)
5886
+ return '';
5887
+ return words
5888
+ .slice(0, maxLength)
5889
+ .map((word) => word[0]?.toUpperCase() ?? '')
5890
+ .join('');
5891
+ };
5892
+
5572
5893
  class MarkdownToHtmlPipe {
5573
5894
  constructor() {
5574
5895
  this.transform = markdownToHtml;
@@ -5581,6 +5902,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
5581
5902
  args: [{ name: 'markdownToHtml' }]
5582
5903
  }] });
5583
5904
 
5905
+ class SlugifyPipe {
5906
+ constructor() {
5907
+ this.transform = slugify;
5908
+ }
5909
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: SlugifyPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
5910
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.5", ngImport: i0, type: SlugifyPipe, isStandalone: true, name: "slugify" }); }
5911
+ }
5912
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: SlugifyPipe, decorators: [{
5913
+ type: Pipe,
5914
+ args: [{ name: 'slugify' }]
5915
+ }] });
5916
+ /**
5917
+ * Converts a string into a URL-friendly slug.
5918
+ *
5919
+ * Diacritics are stripped, the string is lowercased, and any run of
5920
+ * non-alphanumeric characters becomes a single hyphen. Leading/trailing
5921
+ * hyphens are trimmed.
5922
+ *
5923
+ * @example
5924
+ * slugify('Hello World') // 'hello-world'
5925
+ * slugify(' Crème brûlée! ') // 'creme-brulee'
5926
+ * slugify('FC Bayern München') // 'fc-bayern-munchen'
5927
+ * slugify('foo_bar / baz') // 'foo-bar-baz'
5928
+ * slugify(null) // ''
5929
+ */
5930
+ const slugify = (value) => {
5931
+ if (!value)
5932
+ return '';
5933
+ return value
5934
+ .normalize('NFKD')
5935
+ .replace(/[̀-ͯ]/g, '')
5936
+ .toLowerCase()
5937
+ .trim()
5938
+ .replace(/[^a-z0-9]+/g, '-')
5939
+ .replace(/^-+|-+$/g, '');
5940
+ };
5941
+
5584
5942
  class ToArrayPipe {
5585
5943
  constructor() {
5586
5944
  this.transform = toArray;
@@ -7489,5 +7847,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
7489
7847
  * Generated bundle index. Do not edit.
7490
7848
  */
7491
7849
 
7492
- 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, 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, 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, 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, switchQueryListChanges, syncSignal, templateComputed, toArray, toArrayTrackByFn, toStringBinding, transformOrReturn, typedBreakpointTransform, unbindProps, useCursorDragScroll, writeScrollbarSizeToCssVariables, writeViewportSizeToCssVariables, ɵProvideColorThemes, ɵProvideColorThemesPrefix, ɵProvideSurfaceThemes, ɵProvideSurfaceThemesPrefix };
7850
+ 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, 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 };
7493
7851
  //# sourceMappingURL=ethlete-core.mjs.map