@ethlete/core 5.0.0-next.35 → 5.0.0-next.37

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.
@@ -4,13 +4,9 @@ import { takeUntilDestroyed, toSignal, toObservable, outputFromObservable } from
4
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, isObservable, firstValueFrom, defaultIfEmpty } from 'rxjs';
5
5
  import { FormGroup, FormArray, FormControl } from '@angular/forms';
6
6
  import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals';
7
- import { coerceElement } from '@angular/cdk/coercion';
8
7
  import { DOCUMENT as DOCUMENT$1, isPlatformBrowser } from '@angular/common';
9
- import { BreakpointObserver } from '@angular/cdk/layout';
10
8
  import { Router, NavigationEnd, NavigationSkipped, NavigationStart } from '@angular/router';
11
- import { Overlay } from '@angular/cdk/overlay';
12
- import { ComponentPortal } from '@angular/cdk/portal';
13
- import { autoUpdate, computePosition, offset, flip, size, shift, limitShift, arrow, hide } from '@floating-ui/dom';
9
+ import { autoUpdate, offset, flip, shift, limitShift, computePosition, hide, arrow, size } from '@floating-ui/dom';
14
10
  import { Title, Meta, DomSanitizer } from '@angular/platform-browser';
15
11
 
16
12
  const ANIMATION_DEBUG_STORAGE_KEY = 'et-overlay-debug';
@@ -86,7 +82,7 @@ class AnimatableDirective {
86
82
  }
87
83
  }
88
84
  else {
89
- animationDebugLog(`animatable ${el.tagName.toLowerCase()}`, `${eventType} ignored — count already 0 (start ${didEmitStart ? '' : 'not '}emitted)`);
85
+ animationDebugLog(`animatable ${el.tagName.toLowerCase()}`, `${eventType} ignored - count already 0 (start ${didEmitStart ? '' : 'not '}emitted)`);
90
86
  console.warn(`${el.tagName} received animation end/cancel event but activeAnimationCount is already 0. Start was ${didEmitStart ? '' : 'not '}emitted.`);
91
87
  }
92
88
  break;
@@ -126,7 +122,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
126
122
  }]
127
123
  }], ctorParameters: () => [] });
128
124
 
129
- const componentIds = new Map();
125
+ const componentIds = /* @__PURE__ */ new Map();
130
126
  const createComponentId = (prefix) => {
131
127
  const id = componentIds.get(prefix) ?? 0;
132
128
  componentIds.set(prefix, id + 1);
@@ -135,7 +131,7 @@ const createComponentId = (prefix) => {
135
131
  let randomIdFallbackCounter = 0;
136
132
  /**
137
133
  * 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`),
134
+ * not - crucially in **insecure contexts** (a plain-HTTP page served from a LAN IP, not `localhost`),
139
135
  * where `crypto.randomUUID` is `undefined`. Calling it there throws, so anything using it during e.g.
140
136
  * an overlay/menu open would break on such hosts. Prefer this over `crypto.randomUUID()` directly.
141
137
  */
@@ -144,7 +140,7 @@ const randomId = () => {
144
140
  if (typeof cryptoObj?.randomUUID === 'function') {
145
141
  return cryptoObj.randomUUID();
146
142
  }
147
- // `getRandomValues` is available even in insecure contexts — build an RFC-4122-ish v4 id from it.
143
+ // `getRandomValues` is available even in insecure contexts - build an RFC-4122-ish v4 id from it.
148
144
  if (typeof cryptoObj?.getRandomValues === 'function') {
149
145
  const bytes = Array.from(cryptoObj.getRandomValues(new Uint8Array(16)));
150
146
  bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
@@ -152,7 +148,7 @@ const randomId = () => {
152
148
  const hex = bytes.map((byte) => (byte ?? 0).toString(16).padStart(2, '0'));
153
149
  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
150
  }
155
- // last resort: no Web Crypto at all — a session-unique (not cryptographic) id is enough for DOM ids
151
+ // last resort: no Web Crypto at all - a session-unique (not cryptographic) id is enough for DOM ids
156
152
  return `et-id-${(randomIdFallbackCounter++).toString(36)}-${Date.now().toString(36)}`;
157
153
  };
158
154
 
@@ -179,42 +175,67 @@ const createProviders = (token, factory, extraToken) => [
179
175
  { provide: token, useFactory: factory },
180
176
  ...(extraToken ? [{ provide: extraToken, useExisting: token }] : []),
181
177
  ];
182
- const createProvider = (factory, options) => {
178
+
179
+ const defineProvider = (factory, options) => {
183
180
  const token = new InjectionToken(options?.name ?? createComponentId('provider'));
184
- const provide = () => createProviders(token, factory, options?.extraInjectionToken);
185
- const injectFn = createInjectFunction(token);
186
- return [provide, injectFn, token];
181
+ return {
182
+ provide: () => createProviders(token, factory, options?.extraInjectionToken),
183
+ inject: createInjectFunction(token),
184
+ token,
185
+ };
187
186
  };
188
- const createRootProvider = (factory, options) => {
187
+ /**
188
+ * Like {@link defineProvider}, but the factory also runs in the root injector, so `injectX()` works
189
+ * without anyone calling `provideX()`. `provideX()` still exists, to give a subtree its own instance.
190
+ */
191
+ const defineRootProvider = (factory, options) => {
189
192
  const token = new InjectionToken(options?.name ?? createComponentId('provider'), {
190
193
  providedIn: 'root',
191
194
  factory,
192
195
  });
193
- const provide = () => createProviders(token, factory, options?.extraInjectionToken);
194
- const injectFn = createInjectFunction(token);
195
- return [provide, injectFn, token];
196
+ return {
197
+ provide: () => createProviders(token, factory, options?.extraInjectionToken),
198
+ inject: createInjectFunction(token),
199
+ token,
200
+ };
196
201
  };
197
- const createStaticProvider = (defaultValue, options) => {
202
+ /**
203
+ * Like {@link defineProvider} for a plain value rather than a factory: `provideX(override)` merges the
204
+ * partial override over `defaultValue`, which is how every config object in the library is shaped.
205
+ */
206
+ const defineStaticProvider = (defaultValue, options) => {
198
207
  const token = new InjectionToken(options?.name ?? createComponentId('static-provider'));
199
- const provide = (valueOverride) => [
200
- { provide: token, useValue: maybeMergeValues(defaultValue, valueOverride) },
201
- ...(options?.extraInjectionToken ? [{ provide: options.extraInjectionToken, useExisting: token }] : []),
202
- ];
203
- const injectFn = createInjectFunction(token);
204
- return [provide, injectFn, token];
208
+ return {
209
+ provide: (valueOverride) => createValueProviders(token, defaultValue, valueOverride, options),
210
+ inject: createInjectFunction(token),
211
+ token,
212
+ };
205
213
  };
206
- const createStaticRootProvider = (defaultValue, options) => {
214
+ /**
215
+ * {@link defineStaticProvider} whose default is also available in the root injector, so `injectX()`
216
+ * resolves to `defaultValue` in an app that never provides it.
217
+ */
218
+ const defineStaticRootProvider = (defaultValue, options) => {
207
219
  const token = new InjectionToken(options?.name ?? createComponentId('static-provider'), {
208
220
  providedIn: 'root',
209
221
  factory: () => defaultValue,
210
222
  });
211
- const provide = (valueOverride) => [
212
- { provide: token, useValue: maybeMergeValues(defaultValue, valueOverride) },
213
- ...(options?.extraInjectionToken ? [{ provide: options.extraInjectionToken, useExisting: token }] : []),
214
- ];
215
- const injectFn = createInjectFunction(token);
216
- return [provide, injectFn, token];
223
+ return {
224
+ provide: (valueOverride) => createValueProviders(token, defaultValue, valueOverride, options),
225
+ inject: createInjectFunction(token),
226
+ token,
227
+ };
217
228
  };
229
+
230
+ const toProvideFn = (definition) => definition.provide;
231
+ /** The `injectX` half of a provider definition. */
232
+ const toInjectFn = (definition) => definition.inject;
233
+ /** The token behind a provider definition. */
234
+ const toToken = (definition) => definition.token;
235
+ const createValueProviders = (token, defaultValue, valueOverride, options) => [
236
+ { provide: token, useValue: maybeMergeValues(defaultValue, valueOverride) },
237
+ ...(options?.extraInjectionToken ? [{ provide: options.extraInjectionToken, useExisting: token }] : []),
238
+ ];
218
239
  const maybeMergeValues = (defaultValue, valueOverride) => {
219
240
  if (valueOverride && defaultValue && typeof defaultValue === 'object') {
220
241
  return { ...defaultValue, ...valueOverride };
@@ -222,7 +243,6 @@ const maybeMergeValues = (defaultValue, valueOverride) => {
222
243
  return valueOverride ?? defaultValue;
223
244
  };
224
245
 
225
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
226
246
  const cloneFormGroup = (formGroup) => {
227
247
  const cloneLevel = (group, cloneGroup) => {
228
248
  Object.keys(group.controls).forEach((key) => {
@@ -233,7 +253,6 @@ const cloneFormGroup = (formGroup) => {
233
253
  cloneLevel(control, cloneControl);
234
254
  }
235
255
  else if (control instanceof FormArray) {
236
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
237
256
  const cloneArrayControl = new FormArray([], control.validator, control.asyncValidator);
238
257
  control.controls.forEach((control) => {
239
258
  if (control instanceof FormGroup) {
@@ -296,7 +315,6 @@ const applyHostListeners = (listeners, options) => {
296
315
  for (const eventName in listeners) {
297
316
  const handler = listeners[eventName];
298
317
  if (handler) {
299
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
300
318
  applyHostListener(eventName, handler, options);
301
319
  }
302
320
  }
@@ -389,7 +407,7 @@ const copyToClipboard = (text) => defer(() => {
389
407
  }
390
408
  if (navigator.clipboard) {
391
409
  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.
410
+ // The async Clipboard API can be blocked (missing permission, insecure context) - try the legacy path.
393
411
  catchError(() => of(copyToClipboardViaExecCommand(text))));
394
412
  }
395
413
  return of(copyToClipboardViaExecCommand(text));
@@ -426,7 +444,6 @@ const copyToClipboardViaExecCommand = (text) => {
426
444
  return didCopy;
427
445
  };
428
446
 
429
- /* eslint-disable @typescript-eslint/no-explicit-any */
430
447
  /* eslint-disable no-var */
431
448
  /**
432
449
  * Stolen from klona to avoid adding a dependency
@@ -455,7 +472,11 @@ const clone = (original) => {
455
472
  if (typeof original !== 'object')
456
473
  return original;
457
474
  var _og = original;
458
- var i = 0, k, list, tmp, str = Object.prototype.toString.call(_og);
475
+ var i = 0;
476
+ var k;
477
+ var list;
478
+ var tmp;
479
+ var str = Object.prototype.toString.call(_og);
459
480
  if (str === '[object Object]') {
460
481
  tmp = Object.create(_og.__proto__ || null);
461
482
  }
@@ -487,8 +508,6 @@ const clone = (original) => {
487
508
  tmp = _og.slice(0);
488
509
  }
489
510
  else if (str.slice(-6) === 'Array]') {
490
- // ArrayBuffer.isView(x)
491
- // ~> `new` bcuz `Buffer.slice` => ref
492
511
  tmp = new _og.constructor(_og);
493
512
  }
494
513
  if (tmp) {
@@ -537,7 +556,9 @@ function find(iter, tar, key) {
537
556
  }
538
557
  }
539
558
  const equal = (foo, bar) => {
540
- var ctor, len, tmp;
559
+ var ctor;
560
+ var len;
561
+ var tmp;
541
562
  if (foo === bar)
542
563
  return true;
543
564
  if (foo && bar && (ctor = foo.constructor) === bar.constructor) {
@@ -707,7 +728,7 @@ class KeyPressManager {
707
728
  }
708
729
  }
709
730
 
710
- const [provideRenderer, injectRenderer] = createRootProvider(() => {
731
+ const RENDERER_DEF = /* @__PURE__ */ defineRootProvider(() => {
711
732
  const rendererFactory = inject(RendererFactory2);
712
733
  const renderer = rendererFactory.createRenderer(null, null);
713
734
  const addClass = (element, ...classes) => {
@@ -890,7 +911,10 @@ const [provideRenderer, injectRenderer] = createRootProvider(() => {
890
911
  renderer,
891
912
  };
892
913
  }, { name: 'Angular Renderer' });
914
+ const provideRenderer = /* @__PURE__ */ toProvideFn(RENDERER_DEF);
915
+ const injectRenderer = /* @__PURE__ */ toInjectFn(RENDERER_DEF);
893
916
 
917
+ const coerceElement = (el) => (el instanceof ElementRef ? el.nativeElement : el);
894
918
  const isElementSignal = (el) => {
895
919
  if (!isSignal(el))
896
920
  return false;
@@ -1229,17 +1253,17 @@ const memoizeSignal = (factory) => {
1229
1253
  };
1230
1254
 
1231
1255
  /** Inject a signal containing a boolean value indicating if the viewport is xs */
1232
- const injectIsXs = memoizeSignal(() => injectObserveBreakpoint({ max: 'xs' }));
1256
+ const injectIsXs = /* @__PURE__ */ memoizeSignal(() => injectObserveBreakpoint({ max: 'xs' }));
1233
1257
  /** Inject a signal containing a boolean value indicating if the viewport is sm */
1234
- const injectIsSm = memoizeSignal(() => injectObserveBreakpoint({ min: 'sm', max: 'sm' }));
1258
+ const injectIsSm = /* @__PURE__ */ memoizeSignal(() => injectObserveBreakpoint({ min: 'sm', max: 'sm' }));
1235
1259
  /** Inject a signal containing a boolean value indicating if the viewport is md */
1236
- const injectIsMd = memoizeSignal(() => injectObserveBreakpoint({ min: 'md', max: 'md' }));
1260
+ const injectIsMd = /* @__PURE__ */ memoizeSignal(() => injectObserveBreakpoint({ min: 'md', max: 'md' }));
1237
1261
  /** Inject a signal containing a boolean value indicating if the viewport is lg */
1238
- const injectIsLg = memoizeSignal(() => injectObserveBreakpoint({ min: 'lg', max: 'lg' }));
1262
+ const injectIsLg = /* @__PURE__ */ memoizeSignal(() => injectObserveBreakpoint({ min: 'lg', max: 'lg' }));
1239
1263
  /** Inject a signal containing a boolean value indicating if the viewport is xl */
1240
- const injectIsXl = memoizeSignal(() => injectObserveBreakpoint({ min: 'xl', max: 'xl' }));
1264
+ const injectIsXl = /* @__PURE__ */ memoizeSignal(() => injectObserveBreakpoint({ min: 'xl', max: 'xl' }));
1241
1265
  /** Inject a signal containing a boolean value indicating if the viewport is 2xl */
1242
- const injectIs2Xl = memoizeSignal(() => injectObserveBreakpoint({ min: '2xl' }));
1266
+ const injectIs2Xl = /* @__PURE__ */ memoizeSignal(() => injectObserveBreakpoint({ min: '2xl' }));
1243
1267
  /**
1244
1268
  * Inject a boolean value indicating if the viewport is matching the provided options.
1245
1269
  * This value is not reactive. If you want to react to changes, use the {@link injectObserveBreakpoint} function instead.
@@ -1259,7 +1283,7 @@ const injectObserveBreakpoint = (options) => injectBreakpointObserver().observeB
1259
1283
  */
1260
1284
  const injectObserveMediaQuery = (mediaQuery) => injectBreakpointObserver().observeMediaQuery(mediaQuery);
1261
1285
  /** Inject a signal containing the current breakpoint. */
1262
- const injectCurrentBreakpoint = memoizeSignal(() => {
1286
+ const injectCurrentBreakpoint = /* @__PURE__ */ memoizeSignal(() => {
1263
1287
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1264
1288
  const first = BREAKPOINT_ORDER[0];
1265
1289
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -1276,11 +1300,11 @@ const injectCurrentBreakpoint = memoizeSignal(() => {
1276
1300
  });
1277
1301
  });
1278
1302
  /** Inject a signal that indicates if the user is using a portrait display */
1279
- const injectIsPortrait = memoizeSignal(() => injectObserveMediaQuery('(orientation: portrait)'));
1303
+ const injectIsPortrait = /* @__PURE__ */ memoizeSignal(() => injectObserveMediaQuery('(orientation: portrait)'));
1280
1304
  /** Inject a signal that indicates if the user is using a landscape display */
1281
- const injectIsLandscape = memoizeSignal(() => injectObserveMediaQuery('(orientation: landscape)'));
1305
+ const injectIsLandscape = /* @__PURE__ */ memoizeSignal(() => injectObserveMediaQuery('(orientation: landscape)'));
1282
1306
  /** Inject a signal containing the current display orientation */
1283
- const injectDisplayOrientation = memoizeSignal(() => {
1307
+ const injectDisplayOrientation = /* @__PURE__ */ memoizeSignal(() => {
1284
1308
  const isPortrait = injectIsPortrait();
1285
1309
  return computed(() => {
1286
1310
  if (isPortrait())
@@ -1289,11 +1313,11 @@ const injectDisplayOrientation = memoizeSignal(() => {
1289
1313
  });
1290
1314
  });
1291
1315
  /** Inject a signal that indicates if the device has a touch input */
1292
- const injectHasTouchInput = memoizeSignal(() => injectObserveMediaQuery('(pointer: coarse)'));
1316
+ const injectHasTouchInput = /* @__PURE__ */ memoizeSignal(() => injectObserveMediaQuery('(pointer: coarse)'));
1293
1317
  /** Inject a signal that indicates if the device has a fine input (mouse or stylus) */
1294
- const injectHasPrecisionInput = memoizeSignal(() => injectObserveMediaQuery('(pointer: fine)'));
1318
+ const injectHasPrecisionInput = /* @__PURE__ */ memoizeSignal(() => injectObserveMediaQuery('(pointer: fine)'));
1295
1319
  /** Inject a signal containing the current device input type */
1296
- const injectDeviceInputType = memoizeSignal(() => {
1320
+ const injectDeviceInputType = /* @__PURE__ */ memoizeSignal(() => {
1297
1321
  const isTouch = injectHasTouchInput();
1298
1322
  return computed(() => {
1299
1323
  if (isTouch())
@@ -1302,13 +1326,13 @@ const injectDeviceInputType = memoizeSignal(() => {
1302
1326
  });
1303
1327
  });
1304
1328
  /** Inject a signal that indicates if the user prefers reduced motion */
1305
- const injectPrefersReducedMotion = memoizeSignal(() => injectObserveMediaQuery('(prefers-reduced-motion: reduce)'));
1329
+ const injectPrefersReducedMotion = /* @__PURE__ */ memoizeSignal(() => injectObserveMediaQuery('(prefers-reduced-motion: reduce)'));
1306
1330
  /** Inject a signal containing a boolean value indicating if the user can hover (eg. using a mouse) */
1307
- const injectCanHover = memoizeSignal(() => injectObserveMediaQuery('(hover: hover)'));
1331
+ const injectCanHover = /* @__PURE__ */ memoizeSignal(() => injectObserveMediaQuery('(hover: hover)'));
1308
1332
  /** Inject a signal containing the viewport dimensions */
1309
- const injectViewportDimensions = memoizeSignal(() => signalElementDimensions(createDocumentElementSignal()));
1333
+ const injectViewportDimensions = /* @__PURE__ */ memoizeSignal(() => signalElementDimensions(createDocumentElementSignal()));
1310
1334
  /** Inject a signal containing the scrollbar dimensions. Dimensions will be 0 if scrollbars overlap the page contents (like on mobile). */
1311
- const injectScrollbarDimensions = memoizeSignal(() => {
1335
+ const injectScrollbarDimensions = /* @__PURE__ */ memoizeSignal(() => {
1312
1336
  const document = inject(DOCUMENT);
1313
1337
  const renderer = injectRenderer();
1314
1338
  const scrollbarRuler = renderer.createElement('div');
@@ -1342,7 +1366,7 @@ const injectScrollbarDimensions = memoizeSignal(() => {
1342
1366
  * A change that interrupts an in-flight animation continues from the current animated size, and
1343
1367
  * zero/pre-layout measurements are ignored. Respects `prefers-reduced-motion`.
1344
1368
  *
1345
- * The animation starts synchronously inside the `ResizeObserver` callback — it runs after layout
1369
+ * The animation starts synchronously inside the `ResizeObserver` callback - it runs after layout
1346
1370
  * but **before paint**, so the host never paints a frame at its new natural size. Routing the
1347
1371
  * resize through a signal + effect instead would apply the animation one change-detection cycle
1348
1372
  * (= one painted frame) late: a growing panel would flash at its final size, snap back, and only
@@ -1409,7 +1433,7 @@ const injectAnimatedBlockSize = (config) => {
1409
1433
  const cleanup = () => {
1410
1434
  // Cancelling an animation to start the next one rejects its `finished` promise, firing this
1411
1435
  // cleanup after a newer animation has already taken over. That newer animation owns the
1412
- // resizing class, so only strip it when no other animation is mid-flight — otherwise an
1436
+ // resizing class, so only strip it when no other animation is mid-flight - otherwise an
1413
1437
  // interrupted resize (rapid content changes, e.g. async search typing) would un-clip the
1414
1438
  // scroller while it is still animating and flash a scrollbar.
1415
1439
  if (animation !== null && animation !== nextAnimation) {
@@ -1425,7 +1449,7 @@ const injectAnimatedBlockSize = (config) => {
1425
1449
  nextAnimation.finished.then(cleanup).catch(cleanup);
1426
1450
  animation = nextAnimation;
1427
1451
  };
1428
- // no zone/signal indirection — see the pre-paint timing note in the doc comment above
1452
+ // no zone/signal indirection - see the pre-paint timing note in the doc comment above
1429
1453
  const observer = new ResizeObserver(() => {
1430
1454
  if (!ready)
1431
1455
  return;
@@ -1568,15 +1592,41 @@ const DEFAULT_VIEWPORT_CONFIG = {
1568
1592
  '2xl': [1536, Infinity],
1569
1593
  },
1570
1594
  };
1571
- const [provideViewportConfig, injectViewportConfig] = createStaticRootProvider(DEFAULT_VIEWPORT_CONFIG, {
1595
+ const VIEWPORT_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider(DEFAULT_VIEWPORT_CONFIG, {
1572
1596
  name: 'Viewport Config',
1573
1597
  });
1574
- const [provideBreakpointObserver, injectBreakpointObserver] = createRootProvider(() => {
1575
- const breakpointObserver = inject(BreakpointObserver);
1598
+ const provideViewportConfig = /* @__PURE__ */ toProvideFn(VIEWPORT_CONFIG_DEF);
1599
+ const injectViewportConfig = /* @__PURE__ */ toInjectFn(VIEWPORT_CONFIG_DEF);
1600
+ const BREAKPOINT_OBSERVER_DEF = /* @__PURE__ */ defineRootProvider(() => {
1576
1601
  const viewportConfig = injectViewportConfig();
1577
- const isMediaQueryMatched = (mediaQuery) => breakpointObserver.isMatched(mediaQuery);
1602
+ const defaultView = inject(DOCUMENT).defaultView;
1603
+ const matchMedia = defaultView?.matchMedia?.bind(defaultView) ?? null;
1604
+ const queryLists = new Map();
1605
+ const querySignals = new Map();
1606
+ const getQueryList = (mediaQuery) => {
1607
+ if (!matchMedia)
1608
+ return null;
1609
+ let queryList = queryLists.get(mediaQuery);
1610
+ if (!queryList) {
1611
+ queryList = matchMedia(mediaQuery);
1612
+ queryLists.set(mediaQuery, queryList);
1613
+ }
1614
+ return queryList;
1615
+ };
1616
+ const isMediaQueryMatched = (mediaQuery) => getQueryList(mediaQuery)?.matches ?? false;
1578
1617
  const observeMediaQuery = (mediaQuery) => {
1579
- return toSignal(breakpointObserver.observe(mediaQuery).pipe(map((x) => x.matches), startWith(isMediaQueryMatched(mediaQuery))), { requireSync: true });
1618
+ const existing = querySignals.get(mediaQuery);
1619
+ if (existing)
1620
+ return existing;
1621
+ const queryList = getQueryList(mediaQuery);
1622
+ const matches = signal(queryList?.matches ?? false, /* @ts-ignore */
1623
+ ...(ngDevMode ? [{ debugName: "matches" }] : /* istanbul ignore next */ []));
1624
+ // the root provider outlives every consumer, so the listener is intentionally never removed -
1625
+ // the same query list is shared by all of them
1626
+ queryList?.addEventListener('change', (event) => matches.set(event.matches));
1627
+ const result = matches.asReadonly();
1628
+ querySignals.set(mediaQuery, result);
1629
+ return result;
1580
1630
  };
1581
1631
  const observeBreakpoint = (options) => observeMediaQuery(buildMediaQueryString(options));
1582
1632
  const isBreakpointMatched = (options) => isMediaQueryMatched(buildMediaQueryString(options));
@@ -1629,13 +1679,27 @@ const [provideBreakpointObserver, injectBreakpointObserver] = createRootProvider
1629
1679
  isMediaQueryMatched,
1630
1680
  };
1631
1681
  }, { name: 'Breakpoint Observer' });
1682
+ const provideBreakpointObserver = /* @__PURE__ */ toProvideFn(BREAKPOINT_OBSERVER_DEF);
1683
+ const injectBreakpointObserver = /* @__PURE__ */ toInjectFn(BREAKPOINT_OBSERVER_DEF);
1632
1684
 
1633
- const BREAKPOINT_KEY_SET = new Set(BREAKPOINT_ORDER);
1685
+ const BREAKPOINT_KEY_SET = /* @__PURE__ */ new Set(BREAKPOINT_ORDER);
1634
1686
  const isBreakpointMap = (value) => {
1635
1687
  if (value === null || typeof value !== 'object' || Array.isArray(value))
1636
1688
  return false;
1637
1689
  const keys = Object.keys(value);
1638
- return keys.length > 0 && keys.every((k) => BREAKPOINT_KEY_SET.has(k));
1690
+ if (keys.length === 0)
1691
+ return false;
1692
+ const invalidKeys = keys.filter((key) => !BREAKPOINT_KEY_SET.has(key));
1693
+ if (!invalidKeys.length)
1694
+ return true;
1695
+ // One bad key makes the whole map inert - it stops being a breakpoint map and becomes a plain value, which
1696
+ // for an attribute binding means `[object Object]` and no effect at all. Silent, and expensive to find.
1697
+ if (ngDevMode && invalidKeys.length < keys.length) {
1698
+ console.warn(`[ethlete] A breakpoint map used the unknown key(s) ${invalidKeys.map((key) => `"${key}"`).join(', ')}, so ` +
1699
+ `the whole map was ignored. Valid keys are ${BREAKPOINT_ORDER.join(', ')} - there is no "default"; the ` +
1700
+ 'smallest breakpoint is the fallback.');
1701
+ }
1702
+ return false;
1639
1703
  };
1640
1704
  const resolveFromMap = (map, bp, defaultValue) => {
1641
1705
  const idx = BREAKPOINT_ORDER.indexOf(bp);
@@ -1654,7 +1718,6 @@ const breakpointTransformBase = (coerce) => {
1654
1718
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1655
1719
  let capturedDefault = undefined;
1656
1720
  let initialized = false;
1657
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1658
1721
  let cachedSig = null;
1659
1722
  const transformFn = (value) => {
1660
1723
  const coerced = isBreakpointMap(value) ? value : coerce(value);
@@ -1671,13 +1734,10 @@ const breakpointTransformBase = (coerce) => {
1671
1734
  const bp = currentBp();
1672
1735
  const r = raw();
1673
1736
  if (!cachedSig) {
1674
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1675
1737
  const instance = injector.get(BREAKPOINT_INSTANCE_TOKEN);
1676
1738
  for (const key of Object.keys(instance)) {
1677
1739
  const val = instance[key];
1678
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1679
1740
  if (val && typeof val === 'function' && val[SIGNAL]?.transformFn === transformFn) {
1680
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1681
1741
  cachedSig = val;
1682
1742
  break;
1683
1743
  }
@@ -1686,7 +1746,6 @@ const breakpointTransformBase = (coerce) => {
1686
1746
  if (!cachedSig || r === undefined || !isBreakpointMap(r))
1687
1747
  return;
1688
1748
  const resolved = resolveFromMap(r, bp, capturedDefault);
1689
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1690
1749
  untracked(() => setInputSignal(cachedSig, resolved));
1691
1750
  });
1692
1751
  return transformFn;
@@ -1812,11 +1871,12 @@ const signalElementMutations = (el, options) => {
1812
1871
  };
1813
1872
  const signalHostElementMutations = (options) => signalElementMutations(inject(ElementRef), options);
1814
1873
 
1815
- const signalElementChildren = (el) => {
1874
+ const DEFAULT_CHILDREN_MUTATIONS = { childList: true, subtree: true, attributes: true };
1875
+ const signalElementChildren = (el, options) => {
1816
1876
  const elements = buildElementSignal(el);
1817
1877
  const firstEl = firstElementSignal(elements);
1818
1878
  const isRendered = signalIsRendered();
1819
- const elementMutations = signalElementMutations(elements, { childList: true, subtree: true, attributes: true });
1879
+ const elementMutations = signalElementMutations(elements, options?.mutations ?? DEFAULT_CHILDREN_MUTATIONS);
1820
1880
  return computed(() => {
1821
1881
  if (!isRendered())
1822
1882
  return [];
@@ -1972,7 +2032,12 @@ const signalStyles = (el, styleMap) => {
1972
2032
  const signalHostStyles = (styleMap) => signalStyles(inject(ElementRef), styleMap);
1973
2033
 
1974
2034
  const elementCanScroll = (element, direction) => {
1975
- const el = element || document.documentElement;
2035
+ // Omitting the element means "the document scroller". Resolved lazily and guarded so a call on the
2036
+ // server answers "cannot scroll" instead of throwing on a missing `document`.
2037
+ const el = element || (typeof document === 'undefined' ? null : document.documentElement);
2038
+ if (!el) {
2039
+ return false;
2040
+ }
1976
2041
  const { scrollHeight, clientHeight, scrollWidth, clientWidth } = el;
1977
2042
  if (direction === 'x') {
1978
2043
  return scrollWidth > clientWidth;
@@ -1982,17 +2047,25 @@ const elementCanScroll = (element, direction) => {
1982
2047
  }
1983
2048
  return scrollHeight > clientHeight || scrollWidth > clientWidth;
1984
2049
  };
1985
- const createViewportRect = () => ({
1986
- left: 0,
1987
- top: 0,
1988
- right: window.innerWidth,
1989
- bottom: window.innerHeight,
1990
- width: window.innerWidth,
1991
- height: window.innerHeight,
1992
- x: 0,
1993
- y: 0,
1994
- toJSON: () => ({}),
1995
- });
2050
+ const createViewportRect = () => {
2051
+ // Collapses to a zero rect where there is no window (server). `isElementVisible` never reaches the
2052
+ // intersection math there anyway - `elementCanScroll` already reports the document as unscrollable
2053
+ // - so the rect only has to be a valid shape.
2054
+ const hasWindow = typeof window !== 'undefined';
2055
+ const width = hasWindow ? window.innerWidth : 0;
2056
+ const height = hasWindow ? window.innerHeight : 0;
2057
+ return {
2058
+ left: 0,
2059
+ top: 0,
2060
+ right: width,
2061
+ bottom: height,
2062
+ width,
2063
+ height,
2064
+ x: 0,
2065
+ y: 0,
2066
+ toJSON: () => ({}),
2067
+ };
2068
+ };
1996
2069
  const isElementVisible = (options) => {
1997
2070
  const { container, element } = options;
1998
2071
  if (!element) {
@@ -2206,7 +2279,7 @@ const getScrollItemTarget = (entries, container, direction, scrollOrigin, axisDi
2206
2279
  const firstIndex = entries.indexOf(firstVisible);
2207
2280
  const lastIndex = entries.indexOf(lastVisible);
2208
2281
  const containerRect = container.getBoundingClientRect();
2209
- // Only a single item is visible — it must be oversized (wider/taller than the container).
2282
+ // Only a single item is visible - it must be oversized (wider/taller than the container).
2210
2283
  if (firstVisible === lastVisible) {
2211
2284
  const itemRect = firstVisible.target.getBoundingClientRect();
2212
2285
  const isStartEdgeVisible = axisDirection === 'horizontal'
@@ -2445,20 +2518,21 @@ const signalElementLastScrollDirection = (el) => {
2445
2518
  const signalHostElementLastScrollDirection = () => signalElementLastScrollDirection(inject(ElementRef));
2446
2519
 
2447
2520
  /**
2448
- * A signal for whether this page is the one the reader is actually looking at — `false` for a backgrounded
2521
+ * A signal for whether this page is the one the reader is actually looking at - `false` for a backgrounded
2449
2522
  * tab or a minimised window.
2450
2523
  *
2451
2524
  * Not the same question as whether an element is on screen: an IntersectionObserver reports a fully visible
2452
2525
  * element in a background tab, because the tab's own visibility is no part of what it measures. Anything
2453
- * that moves on its own needs to ask both, and a timer is not a substitute — browsers throttle timers in a
2526
+ * that moves on its own needs to ask both, and a timer is not a substitute - browsers throttle timers in a
2454
2527
  * hidden tab rather than stopping them, so work keeps piling up out of sight.
2455
2528
  */
2456
- const injectIsDocumentVisible = memoizeSignal(() => {
2529
+ const injectIsDocumentVisible = /* @__PURE__ */ memoizeSignal(() => {
2457
2530
  const document = inject(DOCUMENT);
2458
2531
  const isVisible = () => document.visibilityState === 'visible';
2459
2532
  return toSignal(fromEvent(document, 'visibilitychange').pipe(map(isVisible)), { initialValue: isVisible() });
2460
2533
  });
2461
2534
 
2535
+ const DEFAULT_SCROLL_STATE_MUTATIONS = { childList: true, subtree: true, attributes: true };
2462
2536
  const areScrollStatesEqual = (a, b) => {
2463
2537
  return (a.canScroll === b.canScroll &&
2464
2538
  a.canScrollHorizontally === b.canScrollHorizontally &&
@@ -2469,7 +2543,7 @@ const signalElementScrollState = (el, options) => {
2469
2543
  const elements = buildElementSignal(el);
2470
2544
  const observedEl = firstElementSignal(elements);
2471
2545
  const elementDimensions = signalElementDimensions(elements);
2472
- const elementMutations = signalElementMutations(elements, { childList: true, subtree: true, attributes: true });
2546
+ const elementMutations = signalElementMutations(elements, options?.mutations ?? DEFAULT_SCROLL_STATE_MUTATIONS);
2473
2547
  const isRendered = signalIsRendered();
2474
2548
  const initialScrollPosition = options?.initialScrollPosition;
2475
2549
  if (initialScrollPosition) {
@@ -2583,7 +2657,6 @@ const useCursorDragScroll = (el, options) => {
2583
2657
  const element = firstElementSignal(elements);
2584
2658
  const destroyRef = inject(DestroyRef);
2585
2659
  const { enabled = signal(true), allowedDirection = 'both' } = options ?? {};
2586
- const scrollState = signalElementScrollState(elements);
2587
2660
  const renderer = injectRenderer();
2588
2661
  const isDragging = signal(false, /* @ts-ignore */
2589
2662
  ...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
@@ -2596,8 +2669,13 @@ const useCursorDragScroll = (el, options) => {
2596
2669
  const dragAmount = signal({ x: 0, y: 0 }, /* @ts-ignore */
2597
2670
  ...(ngDevMode ? [{ debugName: "dragAmount" }] : /* istanbul ignore next */ []));
2598
2671
  const document = inject(DOCUMENT);
2599
- const canScroll = computed(() => {
2600
- const currentScrollState = scrollState();
2672
+ // Only measured when the caller hasn't already: this costs a MutationObserver and a ResizeObserver.
2673
+ const suppliedCanScroll = options?.canScroll;
2674
+ const measuredScrollState = suppliedCanScroll ? null : signalElementScrollState(elements);
2675
+ const measuredCanScroll = computed(() => {
2676
+ const currentScrollState = measuredScrollState?.();
2677
+ if (!currentScrollState)
2678
+ return false;
2601
2679
  const direction = maybeSignalValue(allowedDirection);
2602
2680
  switch (direction) {
2603
2681
  case 'both':
@@ -2608,7 +2686,8 @@ const useCursorDragScroll = (el, options) => {
2608
2686
  return currentScrollState.canScrollVertically;
2609
2687
  }
2610
2688
  }, /* @ts-ignore */
2611
- ...(ngDevMode ? [{ debugName: "canScroll" }] : /* istanbul ignore next */ []));
2689
+ ...(ngDevMode ? [{ debugName: "measuredCanScroll" }] : /* istanbul ignore next */ []));
2690
+ const canScroll = suppliedCanScroll ?? measuredCanScroll;
2612
2691
  // Cleanup if the element the cursor drag scroll is bound to gets changed
2613
2692
  effect(() => {
2614
2693
  const { previousElement } = element();
@@ -2709,7 +2788,7 @@ const useCursorDragScroll = (el, options) => {
2709
2788
  dragAmount.set({ x: 0, y: 0 });
2710
2789
  };
2711
2790
  const setupDragging = (e) => {
2712
- // A context menu takes the pointer away without ever delivering a `mouseup` — so without it in here, a
2791
+ // A context menu takes the pointer away without ever delivering a `mouseup` - so without it in here, a
2713
2792
  // right click (or a ctrl-click on a Mac, which is the primary button) leaves the drag latched on and
2714
2793
  // every later mouse move scrolls the container.
2715
2794
  const dragEnd = merge(fromEvent(document, 'mouseup'), fromEvent(document, 'contextmenu'));
@@ -2736,7 +2815,7 @@ const useCursorDragScroll = (el, options) => {
2736
2815
  };
2737
2816
  };
2738
2817
 
2739
- const ET_PROPERTY_REMOVED = Symbol('ET_PROPERTY_REMOVED');
2818
+ const ET_PROPERTY_REMOVED = /* @__PURE__ */ Symbol('ET_PROPERTY_REMOVED');
2740
2819
  const transformOrReturn = (src, config) => {
2741
2820
  const transformer = config?.transform;
2742
2821
  if (transformer) {
@@ -2753,7 +2832,7 @@ const createInitialRoute = () => {
2753
2832
  return window.location.pathname + window.location.search + window.location.hash;
2754
2833
  };
2755
2834
  /** Inject the current router event */
2756
- const injectRouterEvent = memoizeSignal(() => {
2835
+ const injectRouterEvent = /* @__PURE__ */ memoizeSignal(() => {
2757
2836
  const router = inject(Router);
2758
2837
  const initialRoute = createInitialRoute();
2759
2838
  return toSignal(router.events, {
@@ -2764,7 +2843,7 @@ const injectRouterEvent = memoizeSignal(() => {
2764
2843
  * Signal that indicates whether the router has been initialized.
2765
2844
  * The router is considered initialized once the first NavigationEnd event with an id different than -1 has been emitted.
2766
2845
  */
2767
- const injectIsRouterInitialized = memoizeSignal(() => {
2846
+ const injectIsRouterInitialized = /* @__PURE__ */ memoizeSignal(() => {
2768
2847
  const event = injectRouterEvent();
2769
2848
  return computed(() => {
2770
2849
  const e = event();
@@ -2776,7 +2855,7 @@ const injectIsRouterInitialized = memoizeSignal(() => {
2776
2855
  * The url includes query params as well as the fragment. Use `injectRoute` instead if you are not intrusted in those.
2777
2856
  * @example "/my-page?query=1&param=true#fragment"
2778
2857
  */
2779
- const injectUrl = memoizeSignal(() => {
2858
+ const injectUrl = /* @__PURE__ */ memoizeSignal(() => {
2780
2859
  const event = injectRouterEvent();
2781
2860
  const router = inject(Router);
2782
2861
  // Captured once for reads that happen before the router has committed any
@@ -2786,7 +2865,7 @@ const injectUrl = memoizeSignal(() => {
2786
2865
  // We depend on `event` and `currentNavigation` only to get re-evaluated
2787
2866
  // around each navigation, but we read the *committed* url straight from the
2788
2867
  // router. The router commits `currentUrlTree` before it activates components,
2789
- // so `router.url` is already correct inside a child component's constructor —
2868
+ // so `router.url` is already correct inside a child component's constructor -
2790
2869
  // unlike the NavigationEnd event, which only reaches this signal *after*
2791
2870
  // activation.
2792
2871
  event();
@@ -2806,7 +2885,7 @@ const injectUrl = memoizeSignal(() => {
2806
2885
  * Inject the current route
2807
2886
  * @example "/my-page"
2808
2887
  */
2809
- const injectRoute = memoizeSignal(() => {
2888
+ const injectRoute = /* @__PURE__ */ memoizeSignal(() => {
2810
2889
  const url = injectUrl();
2811
2890
  return computed(() => {
2812
2891
  const fullUrl = url();
@@ -2841,14 +2920,9 @@ const createRoute = (router) => {
2841
2920
  /**
2842
2921
  * Inject the complete router state. This includes the current route data, path params, query params, title and fragment.
2843
2922
  */
2844
- const injectRouterState = memoizeSignal(() => {
2923
+ const injectRouterState = /* @__PURE__ */ memoizeSignal(() => {
2845
2924
  const event = injectRouterEvent();
2846
2925
  const router = inject(Router);
2847
- // We only depend on `event` to get re-evaluated on every router event, but we
2848
- // read the *committed* snapshot straight from the router. The router commits
2849
- // `routerState` before it activates components (and emits ActivationStart
2850
- // before NavigationEnd), so this is already correct inside a child component's
2851
- // constructor — unlike an effect, which only flushes after construction.
2852
2926
  return computed(() => {
2853
2927
  event();
2854
2928
  return createRouterState(router);
@@ -2917,7 +2991,7 @@ const injectPathParam = (key, config) => {
2917
2991
  * Inject query params that changed during navigation. Unchanged query params will be ignored.
2918
2992
  * Removed query params will be represented by the symbol `ET_PROPERTY_REMOVED`.
2919
2993
  */
2920
- const injectQueryParamChanges = memoizeSignal(() => {
2994
+ const injectQueryParamChanges = /* @__PURE__ */ memoizeSignal(() => {
2921
2995
  const queryParams = injectQueryParams();
2922
2996
  const prevQueryParams = previousSignalValue(queryParams);
2923
2997
  return computed(() => {
@@ -2941,7 +3015,7 @@ const injectQueryParamChanges = memoizeSignal(() => {
2941
3015
  * Inject path params that changed during navigation. Unchanged path params will be ignored.
2942
3016
  * Removed path params will be represented by the symbol `ET_PROPERTY_REMOVED`.
2943
3017
  */
2944
- const injectPathParamChanges = memoizeSignal(() => {
3018
+ const injectPathParamChanges = /* @__PURE__ */ memoizeSignal(() => {
2945
3019
  const pathParams = injectPathParams();
2946
3020
  const prevPathParams = previousSignalValue(pathParams);
2947
3021
  return computed(() => {
@@ -2961,22 +3035,42 @@ const injectPathParamChanges = memoizeSignal(() => {
2961
3035
  return changes;
2962
3036
  });
2963
3037
  });
3038
+ /**
3039
+ * The state handed to the navigation that is currently in progress, i.e. what
3040
+ * `router.navigate(…, { state })` passed - typed to whatever you expect.
3041
+ *
3042
+ * Read **synchronously**, in a constructor or a resolver, and deliberately not a signal: navigation state exists
3043
+ * only for the duration of the navigation that carries it. By the time an effect flushed, the navigation would be
3044
+ * over and the answer always `null`. Returns `null` when there is no navigation in flight or it carried no state -
3045
+ * which is the normal case for a page arrived at by typing its URL, so always handle it.
3046
+ *
3047
+ * Use it for the things a URL should not carry: a "you were redirected because…" reason, a pre-fetched object the
3048
+ * previous page already had, a scroll intent. Anything the page needs to survive a reload belongs in the URL.
3049
+ *
3050
+ * @example
3051
+ * type CheckoutNavState = { fromCart: boolean };
3052
+ *
3053
+ * export class CheckoutComponent {
3054
+ * private navState = injectRouterNavigationState<CheckoutNavState>();
3055
+ *
3056
+ * protected cameFromCart = this.navState?.fromCart ?? false;
3057
+ * }
3058
+ */
3059
+ const injectRouterNavigationState = () => {
3060
+ const router = inject(Router);
3061
+ return router.getCurrentNavigation()?.extras.state ?? null;
3062
+ };
2964
3063
 
2965
- const ET_DISABLE_SCROLL_TOP = Symbol('ET_DISABLE_SCROLL_TOP');
2966
- const ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE = Symbol('ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE');
2967
- const ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE = Symbol('ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE');
3064
+ const ET_DISABLE_SCROLL_TOP = /* @__PURE__ */ Symbol('ET_DISABLE_SCROLL_TOP');
3065
+ const ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE = /* @__PURE__ */ Symbol('ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE');
3066
+ const ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE = /* @__PURE__ */ Symbol('ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE');
2968
3067
  const routerDisableScrollTop = (config = {}) => {
2969
3068
  return {
2970
3069
  ...(!config.asReturnRoute ? { [ET_DISABLE_SCROLL_TOP]: true } : { [ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE]: true }),
2971
3070
  ...(config.onPathParamChange ? { [ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE]: true } : {}),
2972
3071
  };
2973
3072
  };
2974
- /**
2975
- * Registry of "not settled yet" reporters. Lives in the root injector so a route component can
2976
- * reach the registration created by the app level `setupScrollRestoration` call.
2977
- * @internal
2978
- */
2979
- const [, injectScrollRestorationHolds] = createRootProvider(() => {
3073
+ const SCROLL_RESTORATION_HOLDS_DEF = /* @__PURE__ */ defineRootProvider(() => {
2980
3074
  const holds = new Set();
2981
3075
  return {
2982
3076
  add: (isPending) => {
@@ -2992,11 +3086,17 @@ const [, injectScrollRestorationHolds] = createRootProvider(() => {
2992
3086
  },
2993
3087
  };
2994
3088
  }, { name: 'ScrollRestorationHolds' });
3089
+ /**
3090
+ * Registry of "not settled yet" reporters. Lives in the root injector so a route component can
3091
+ * reach the registration created by the app level `setupScrollRestoration` call.
3092
+ * @internal
3093
+ */
3094
+ const injectScrollRestorationHolds = /* @__PURE__ */ toInjectFn(SCROLL_RESTORATION_HOLDS_DEF);
2995
3095
  /**
2996
3096
  * Suspend scroll restoration while `isPending` reads `true`.
2997
3097
  *
2998
3098
  * Restoration already waits for the scroll container to grow tall enough to reach the saved offset,
2999
- * so this is only needed when the content may take longer than `restore.timeout` to get there —
3099
+ * so this is only needed when the content may take longer than `restore.timeout` to get there -
3000
3100
  * a slow list request, for instance. Registers for the lifetime of the current injection context.
3001
3101
  *
3002
3102
  * @example
@@ -3030,7 +3130,7 @@ const setupScrollRestoration = (config = {}) => {
3030
3130
  return el ?? document.documentElement;
3031
3131
  };
3032
3132
  // The browser restores the offset against the still-loading (much shorter) document, which is the
3033
- // exact problem we are solving — take it over completely.
3133
+ // exact problem we are solving - take it over completely.
3034
3134
  if (restoreEnabled && 'scrollRestoration' in history) {
3035
3135
  history.scrollRestoration = 'manual';
3036
3136
  }
@@ -3069,7 +3169,7 @@ const setupScrollRestoration = (config = {}) => {
3069
3169
  }
3070
3170
  const now = Date.now();
3071
3171
  // A registered hold means the page knows more data is coming, so the content is expected to
3072
- // grow — suspend the clock instead of giving up on a still loading page.
3172
+ // grow - suspend the clock instead of giving up on a still loading page.
3073
3173
  if (holds.isHeld() && now < hardDeadline) {
3074
3174
  deadline = now + restoreTimeout;
3075
3175
  }
@@ -3082,7 +3182,7 @@ const setupScrollRestoration = (config = {}) => {
3082
3182
  }
3083
3183
  cancelPendingRestore();
3084
3184
  };
3085
- // Wait for the new route to actually render before reading any geometry — on `NavigationEnd` the
3185
+ // Wait for the new route to actually render before reading any geometry - on `NavigationEnd` the
3086
3186
  // components exist but the DOM still shows the previous page, whose height would be measured
3087
3187
  // instead. The timeout is a safety net in case no render pass follows.
3088
3188
  const start = () => {
@@ -3206,7 +3306,7 @@ const createLogger = (config) => {
3206
3306
  };
3207
3307
 
3208
3308
  const escapeHtml = (str) => str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3209
- /** Escapes HTML-special characters but leaves existing entity references intact — for input that
3309
+ /** Escapes HTML-special characters but leaves existing entity references intact - for input that
3210
3310
  * may already contain entities (e.g. Markdown serialized from the editor's DOM, where `&` is
3211
3311
  * stored as `&amp;`), where plain escaping would double-escape them. */
3212
3312
  const escapeHtmlPreservingEntities = (str) => str
@@ -3215,7 +3315,7 @@ const escapeHtmlPreservingEntities = (str) => str
3215
3315
  .replace(/>/g, '&gt;')
3216
3316
  .replace(/"/g, '&quot;');
3217
3317
  /** Rejects URL schemes that execute script when navigated. Whitespace/control characters are
3218
- * stripped before checking — browsers ignore them inside URLs, so `java\tscript:` would
3318
+ * stripped before checking - browsers ignore them inside URLs, so `java\tscript:` would
3219
3319
  * otherwise slip through. */
3220
3320
  const isSafeUrl = (url) =>
3221
3321
  // eslint-disable-next-line no-control-regex -- stripping control characters is the point: browsers ignore them inside URLs
@@ -3242,7 +3342,7 @@ const parseSeparatorAligns = (line) => parseTableRow(line).map((cell) => {
3242
3342
  /** The GFM separator token for a column alignment. */
3243
3343
  const separatorFor = (align) => align === 'center' ? ':---:' : align === 'right' ? '---:' : align === 'left' ? ':---' : '---';
3244
3344
  const alignStyle = (align) => (align ? ` style="text-align: ${align}"` : '');
3245
- /** Wraps `content` in an emphasis `marker`, hoisting boundary whitespace outside the delimiters —
3345
+ /** Wraps `content` in an emphasis `marker`, hoisting boundary whitespace outside the delimiters -
3246
3346
  * CommonMark emphasis must not face whitespace on the inside (`** fett**` doesn't parse), and
3247
3347
  * `&nbsp;` counts as whitespace there too. Whitespace-only content stays unwrapped. */
3248
3348
  const emphasize = (marker, content) => {
@@ -3256,9 +3356,9 @@ const makePlaceholder = (kind, idx) => `\u{E000}${kind}${idx}\u{E001}`;
3256
3356
  const placeholderRe = (kind) => new RegExp(`\u{E000}${kind}(\\d+)\u{E001}`, 'gu');
3257
3357
  const isPlaceholder = (kind, str) => new RegExp(`^\u{E000}${kind}\\d+\u{E001}$`, 'u').test(str);
3258
3358
  /** Tags allowed to survive inside an aligned block's raw-HTML passthrough. */
3259
- const SAFE_INLINE_TAGS = new Set(['strong', 'em', 'b', 'i', 'del', 's', 'u', 'code', 'br']);
3359
+ const SAFE_INLINE_TAGS = /* @__PURE__ */ new Set(['strong', 'em', 'b', 'i', 'del', 's', 'u', 'code', 'br']);
3260
3360
  /** Reduces raw inline HTML to the editor's own vocabulary: allowed tags lose all their attributes
3261
- * (`<a>` keeps a safe `href`), everything else — including any event-handler attribute — is
3361
+ * (`<a>` keeps a safe `href`), everything else - including any event-handler attribute - is
3262
3362
  * dropped, and the remaining text is escaped. */
3263
3363
  const sanitizeInlineHtml = (html) => {
3264
3364
  const kept = [];
@@ -3283,7 +3383,7 @@ const processInline = (text) => {
3283
3383
  });
3284
3384
  // "Open in new tab" links round-trip as raw HTML because Markdown has no `target` syntax. Extract
3285
3385
  // them before the raw-HTML escape below, keeping only a safe href, `target="_blank"` and a forced
3286
- // `rel="noopener noreferrer"` (any other attribute — including event handlers — is dropped); their
3386
+ // `rel="noopener noreferrer"` (any other attribute - including event handlers - is dropped); their
3287
3387
  // inner markup is processed recursively. Anything else that looks like raw HTML is still escaped.
3288
3388
  const newTabLinks = [];
3289
3389
  text = text.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (full, attrs, inner) => {
@@ -3294,11 +3394,11 @@ const processInline = (text) => {
3294
3394
  const anchor = `<a href="${href}" target="_blank" rel="noopener noreferrer">${processInline(inner)}</a>`;
3295
3395
  return makePlaceholder('NTLINK', newTabLinks.push(anchor) - 1);
3296
3396
  });
3297
- // Raw HTML in Markdown text is escaped, not rendered — the editor writes this HTML straight
3397
+ // Raw HTML in Markdown text is escaped, not rendered - the editor writes this HTML straight
3298
3398
  // into the DOM via innerHTML, so anything else would let a crafted value inject markup. `<u>`
3299
3399
  // is the one deliberate exception: underline has no Markdown form and round-trips as raw <u>.
3300
3400
  text = escapeHtmlPreservingEntities(text).replace(/&lt;(\/?)u&gt;/gi, '<$1u>');
3301
- // Bold + italic — *** before ** before *. A delimiter flanking whitespace doesn't open/close a
3401
+ // Bold + italic - *** before ** before *. A delimiter flanking whitespace doesn't open/close a
3302
3402
  // run (CommonMark's flanking rule), so literal asterisks as in `2 * 3 * 4` stay text.
3303
3403
  text = text.replace(/\*\*\*([^\s*](?:.*?[^\s*])?)\*\*\*/g, '<strong><em>$1</em></strong>');
3304
3404
  text = text.replace(/\*\*([^\s*](?:.*?[^\s*])?)\*\*/g, '<strong>$1</strong>');
@@ -3361,6 +3461,75 @@ const findListItems = (inner) => {
3361
3461
  }
3362
3462
  return items;
3363
3463
  };
3464
+ /** The first top-level `<blockquote>` in `html`, matched with balanced nesting. */
3465
+ const findBlockquote = (html) => {
3466
+ const open = /<blockquote\b[^>]*>/i.exec(html);
3467
+ if (!open)
3468
+ return null;
3469
+ const innerStart = open.index + open[0].length;
3470
+ const tag = /<(\/?)blockquote\b[^>]*>/gi;
3471
+ tag.lastIndex = innerStart;
3472
+ let depth = 1;
3473
+ let match;
3474
+ while ((match = tag.exec(html))) {
3475
+ depth += match[1] ? -1 : 1;
3476
+ if (depth === 0) {
3477
+ return { start: open.index, end: tag.lastIndex, inner: html.slice(innerStart, match.index) };
3478
+ }
3479
+ }
3480
+ return null;
3481
+ };
3482
+ /** The lines of quoted text in a blockquote's own (non-nested) HTML, each with its `> ` prefix.
3483
+ * `<br>` and paragraph boundaries are the line breaks - the tag-strip would swallow them. */
3484
+ const quotedLines = (html, prefix) => {
3485
+ const text = stripTags(html.replace(/<br\s*\/?>/gi, '\n').replace(/<\/p\s*>/gi, '\n')).trim();
3486
+ return text ? text.split('\n').map((line) => `${prefix}${line.trim()}`) : [];
3487
+ };
3488
+ /** Serializes a blockquote's inner HTML to Markdown, one `>` per nesting level. */
3489
+ const blockquoteToMarkdown = (inner, depth) => {
3490
+ const prefix = '> '.repeat(depth + 1);
3491
+ const lines = [];
3492
+ let rest = inner;
3493
+ for (let nested = findBlockquote(rest); nested; nested = findBlockquote(rest)) {
3494
+ lines.push(...quotedLines(rest.slice(0, nested.start), prefix));
3495
+ lines.push(blockquoteToMarkdown(nested.inner, depth + 1));
3496
+ rest = rest.slice(nested.end);
3497
+ }
3498
+ lines.push(...quotedLines(rest, prefix));
3499
+ // An empty quote still needs its marker, or it would vanish from the value entirely.
3500
+ return lines.length > 0 ? lines.join('\n') : prefix.trim();
3501
+ };
3502
+ /** Builds a `<blockquote>` from `> `-prefixed lines, recursing into runs of deeper (`>>`) lines. */
3503
+ const buildBlockquoteHtml = (lines) => {
3504
+ let html = '';
3505
+ let run = [];
3506
+ const flushRun = () => {
3507
+ if (run.length > 0)
3508
+ html += run.map(processInline).join('<br>');
3509
+ run = [];
3510
+ };
3511
+ for (let i = 0; i < lines.length;) {
3512
+ const stripped = (lines[i] ?? '').replace(/^>[ \t]?/, '');
3513
+ if (!stripped.startsWith('>')) {
3514
+ run.push(stripped);
3515
+ i++;
3516
+ continue;
3517
+ }
3518
+ flushRun();
3519
+ const nested = [];
3520
+ while (i < lines.length) {
3521
+ const line = (lines[i] ?? '').replace(/^>[ \t]?/, '');
3522
+ if (!line.startsWith('>'))
3523
+ break;
3524
+ nested.push(line);
3525
+ i++;
3526
+ }
3527
+ html += buildBlockquoteHtml(nested);
3528
+ }
3529
+ flushRun();
3530
+ // `<br>` so an empty quote has a line box the caret can sit in (the editor writes this HTML live)
3531
+ return `<blockquote>${html || '<br>'}</blockquote>`;
3532
+ };
3364
3533
  /** Serializes a list's inner HTML to Markdown, indenting nested lists two spaces per level. */
3365
3534
  const listToMarkdown = (inner, ordered, depth) => {
3366
3535
  const indent = ' '.repeat(depth);
@@ -3379,7 +3548,7 @@ const listToMarkdown = (inner, ordered, depth) => {
3379
3548
  })
3380
3549
  .join('\n');
3381
3550
  };
3382
- /** Parses list lines into (indent-level, ordered, text) — two leading spaces per nesting level. */
3551
+ /** Parses list lines into (indent-level, ordered, text) - two leading spaces per nesting level. */
3383
3552
  const parseListLines = (lines) => lines
3384
3553
  .map((line) => {
3385
3554
  const match = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line);
@@ -3453,7 +3622,7 @@ const markdownToHtml = (markdown) => {
3453
3622
  // Horizontal rule
3454
3623
  if (/^(---|\*\*\*|___)\s*$/.test(trimmed))
3455
3624
  return '<hr>';
3456
- // Table (GFM) — column alignment from the separator line lands as text-align on every cell
3625
+ // Table (GFM) - column alignment from the separator line lands as text-align on every cell
3457
3626
  const tableLines = trimmed.split('\n');
3458
3627
  if (tableLines.length >= 2 && /\|/.test(tableLines[0] ?? '') && isTableSeparatorLine(tableLines[1] ?? '')) {
3459
3628
  const aligns = parseSeparatorAligns(tableLines[1] ?? '');
@@ -3471,15 +3640,10 @@ const markdownToHtml = (markdown) => {
3471
3640
  : '';
3472
3641
  return `<table>${thead}${tbody}</table>`;
3473
3642
  }
3474
- // Blockquote — each line runs through the inline pass on its own (processInline escapes raw
3475
- // HTML, so joining first would escape the <br> separators too)
3476
- if (/^> /.test(trimmed)) {
3477
- const content = trimmed
3478
- .split('\n')
3479
- .map((l) => processInline(l.replace(/^>\s?/, '')))
3480
- .join('<br>');
3481
- return `<blockquote>${content}</blockquote>`;
3482
- }
3643
+ // Blockquote - each line runs through the inline pass on its own (processInline escapes raw
3644
+ // HTML, so joining first would escape the <br> separators too), and a `>>` run nests
3645
+ if (/^>/.test(trimmed))
3646
+ return buildBlockquoteHtml(trimmed.split('\n'));
3483
3647
  // List (unordered/ordered, with indentation-based nesting)
3484
3648
  if (/^([-*+]|\d+\.)\s/.test(trimmed)) {
3485
3649
  const lines = parseListLines(trimmed.split('\n'));
@@ -3487,7 +3651,7 @@ const markdownToHtml = (markdown) => {
3487
3651
  return buildListHtml(lines, 0, lines[0].indent).html;
3488
3652
  }
3489
3653
  }
3490
- // Paragraph — single newlines within a block become <br>
3654
+ // Paragraph - single newlines within a block become <br>
3491
3655
  return `<p>${trimmed
3492
3656
  .split('\n')
3493
3657
  .map((l) => processInline(l))
@@ -3507,21 +3671,21 @@ const htmlToMarkdown = (html) => {
3507
3671
  return '';
3508
3672
  let md = html;
3509
3673
  // Aligned blocks: text-align has no Markdown form, so preserve them verbatim as native HTML (their
3510
- // inner markup stays HTML) and round-trip via a placeholder — extracted before the block passes
3674
+ // inner markup stays HTML) and round-trip via a placeholder - extracted before the block passes
3511
3675
  // below rewrite them, restored after the final tag-strip.
3512
3676
  const alignedBlocks = [];
3513
3677
  md = md.replace(/<(p|h[1-6]|div)\b[^>]*\bstyle="[^"]*text-align[^"]*"[^>]*>[\s\S]*?<\/\1>/gi, (match) => makePlaceholder('ALIGN', alignedBlocks.push(match) - 1));
3514
- // Code blocks — process before inline code
3678
+ // Code blocks - process before inline code
3515
3679
  md = md.replace(/<pre[^>]*><code[^>]*class="language-([^"]+)"[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, lang, code) => `\n\`\`\`${lang}\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
3516
3680
  md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, code) => `\n\`\`\`\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
3517
3681
  md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => `\n\`\`\`\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
3518
- // Headings — keep the inner markup so the inline passes below turn any nested
3682
+ // Headings - keep the inner markup so the inline passes below turn any nested
3519
3683
  // bold/italic/link/code into markdown; leftover tags are stripped at the end.
3520
3684
  for (let i = 6; i >= 1; i--) {
3521
3685
  md = md.replace(new RegExp(`<h${i}[^>]*>([\\s\\S]*?)<\\/h${i}>`, 'gi'), (_, content) => `\n${'#'.repeat(i)} ${content.trim()}\n`);
3522
3686
  }
3523
3687
  // Bold + italic (combined before individual). Boundary whitespace is hoisted out of the
3524
- // delimiters (emphasize) — `<strong> x</strong>` must become ` **x**`, not the invalid `** x**`.
3688
+ // delimiters (emphasize) - `<strong> x</strong>` must become ` **x**`, not the invalid `** x**`.
3525
3689
  md = md.replace(/<strong[^>]*><em[^>]*>([\s\S]*?)<\/em><\/strong>/gi, (_, c) => emphasize('***', c));
3526
3690
  md = md.replace(/<em[^>]*><strong[^>]*>([\s\S]*?)<\/strong><\/em>/gi, (_, c) => emphasize('***', c));
3527
3691
  md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _tag, c) => emphasize('**', c));
@@ -3530,7 +3694,7 @@ const htmlToMarkdown = (html) => {
3530
3694
  // Inline code
3531
3695
  md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, code) => `\`${unescapeHtml(code)}\``);
3532
3696
  // Links and images. A link with `target="_blank"` has no Markdown form, so it round-trips as raw
3533
- // HTML (keeping only href + target + rel) via a placeholder — like underline below; ordinary links
3697
+ // HTML (keeping only href + target + rel) via a placeholder - like underline below; ordinary links
3534
3698
  // become `[text](url)`.
3535
3699
  const newTabLinks = [];
3536
3700
  md = md.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (_, attrs, inner) => {
@@ -3544,20 +3708,17 @@ const htmlToMarkdown = (html) => {
3544
3708
  md = md.replace(/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '![$2]($1)');
3545
3709
  md = md.replace(/<img[^>]+alt="([^"]*)"[^>]*src="([^"]*)"[^>]*\/?>/gi, '![$1]($2)');
3546
3710
  md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, '![]($1)');
3547
- // Underline has no Markdown form — preserve it as raw <u> so it round-trips. Extract it now (its
3711
+ // Underline has no Markdown form - preserve it as raw <u> so it round-trips. Extract it now (its
3548
3712
  // inner markup is already converted above) so the block passes below don't strip the tag; the
3549
3713
  // placeholder is restored after the final tag-strip.
3550
3714
  const underlines = [];
3551
3715
  md = md.replace(/<u[^>]*>([\s\S]*?)<\/u>/gi, (_, inner) => makePlaceholder('U', underlines.push(inner) - 1));
3552
- // Block quotes — <br> line breaks become one quoted line each (stripTags would swallow them)
3553
- md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
3554
- return (stripTags(content.replace(/<br\s*\/?>/gi, '\n'))
3555
- .trim()
3556
- .split('\n')
3557
- .map((line) => `> ${line.trim()}`)
3558
- .join('\n') + '\n');
3559
- });
3560
- // Lists — replace each top-level list with its recursively-serialized Markdown (handles nesting).
3716
+ // Block quotes - replace each top-level <blockquote> with its recursively-serialized Markdown, so
3717
+ // a quote inside a quote becomes `>>` (regex alone can't match balanced nesting).
3718
+ for (let quote = findBlockquote(md); quote; quote = findBlockquote(md)) {
3719
+ md = `${md.slice(0, quote.start)}\n${blockquoteToMarkdown(quote.inner, 0)}\n${md.slice(quote.end)}`;
3720
+ }
3721
+ // Lists - replace each top-level list with its recursively-serialized Markdown (handles nesting).
3561
3722
  for (let list = findList(md); list; list = findList(md)) {
3562
3723
  md = `${md.slice(0, list.start)}\n${listToMarkdown(list.inner, list.ordered, 0)}\n${md.slice(list.end)}`;
3563
3724
  }
@@ -3585,10 +3746,10 @@ const htmlToMarkdown = (html) => {
3585
3746
  const lines = [`| ${headerCells.join(' | ')} |`, `| ${separator} |`, ...bodyRows.map((r) => `| ${r} |`)];
3586
3747
  return `\n${lines.join('\n')}\n`;
3587
3748
  });
3588
- // Block elements — a <br> inside a paragraph is a soft line break; turn it into a newline before
3749
+ // Block elements - a <br> inside a paragraph is a soft line break; turn it into a newline before
3589
3750
  // the tag-strip removes it, so it round-trips (a single newline within a block renders as <br>)
3590
3751
  md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, content) => `\n${stripTags(content.replace(/<br\s*\/?>/gi, '\n')).trim()}\n`);
3591
- // A <div> boundary acts as a paragraph boundary — clipboard HTML commonly uses divs as
3752
+ // A <div> boundary acts as a paragraph boundary - clipboard HTML commonly uses divs as
3592
3753
  // paragraphs, and silently stripping them would merge adjacent blocks into one.
3593
3754
  md = md.replace(/<\/div\s*>/gi, '\n\n').replace(/<div[^>]*>/gi, '\n\n');
3594
3755
  md = md.replace(/<br\s*\/?>/gi, '\n');
@@ -3647,17 +3808,11 @@ class RuntimeError extends Error {
3647
3808
  this.code = code;
3648
3809
  this.data = data;
3649
3810
  if (data !== RUNTIME_ERROR_NO_DATA) {
3650
- try {
3651
- const _data = clone(data);
3652
- setTimeout(() => {
3653
- console.error(_data);
3654
- }, 1);
3655
- }
3656
- catch {
3657
- setTimeout(() => {
3658
- console.error(data);
3659
- }, 1);
3660
- }
3811
+ // deferred so the error itself is logged first - consoles render the live object, so a
3812
+ // mutation after the throw is visible in the log
3813
+ setTimeout(() => {
3814
+ console.error(data);
3815
+ }, 1);
3661
3816
  }
3662
3817
  }
3663
3818
  }
@@ -3694,7 +3849,7 @@ const canUseSessionMemory = () => {
3694
3849
  if (typeof globalThis.document === 'undefined') {
3695
3850
  return false;
3696
3851
  }
3697
- return globalThis.sessionStorage != null;
3852
+ return globalThis.sessionStorage !== null && globalThis.sessionStorage !== undefined;
3698
3853
  }
3699
3854
  catch {
3700
3855
  return false;
@@ -3763,6 +3918,8 @@ const getClientXY = (event) => {
3763
3918
  }
3764
3919
  return { clientX: event.clientX, clientY: event.clientY };
3765
3920
  };
3921
+ /** Trailing window over which {@link SwipeTracker.end} measures the release velocity. */
3922
+ const SWIPE_VELOCITY_WINDOW_MS = 100;
3766
3923
  const createSwipeTracker = (startEvent) => {
3767
3924
  const { clientX: originClientX, clientY: originClientY } = getClientXY(startEvent);
3768
3925
  const startTime = Date.now();
@@ -3770,10 +3927,25 @@ const createSwipeTracker = (startEvent) => {
3770
3927
  let movementY = 0;
3771
3928
  let isSwiping = false;
3772
3929
  let isScrolling = false;
3930
+ /**
3931
+ * Recent positions, oldest first, used to derive the release velocity. Trimmed to the trailing
3932
+ * window plus the one sample just before it, which serves as that window's baseline.
3933
+ */
3934
+ let samples = [{ t: startTime, movementX: 0, movementY: 0 }];
3773
3935
  const update = (event) => {
3774
3936
  const { clientX, clientY } = getClientXY(event);
3775
3937
  movementX = clientX - originClientX;
3776
3938
  movementY = clientY - originClientY;
3939
+ const now = Date.now();
3940
+ samples.push({ t: now, movementX, movementY });
3941
+ // Trim against the newest sample rather than `now + window`: `end()` measures from its own
3942
+ // timestamp, which is never earlier, so this always keeps a superset of what it needs.
3943
+ const cutoff = now - SWIPE_VELOCITY_WINDOW_MS;
3944
+ let dropCount = 0;
3945
+ while (dropCount + 1 < samples.length && samples[dropCount + 1].t < cutoff)
3946
+ dropCount++;
3947
+ if (dropCount)
3948
+ samples = samples.slice(dropCount);
3777
3949
  const positiveMovementX = Math.abs(movementX);
3778
3950
  const positiveMovementY = Math.abs(movementY);
3779
3951
  if (!isSwiping && !isScrolling) {
@@ -3796,12 +3968,31 @@ const createSwipeTracker = (startEvent) => {
3796
3968
  isSwiping,
3797
3969
  };
3798
3970
  };
3971
+ const measureReleaseVelocity = (endTime) => {
3972
+ const windowStart = endTime - SWIPE_VELOCITY_WINDOW_MS;
3973
+ const firstInWindow = samples.findIndex((sample) => sample.t >= windowStart);
3974
+ // Nothing moved during the window: the pointer was held still before release, so the gesture
3975
+ // carries no momentum no matter how fast it started out.
3976
+ if (firstInWindow === -1)
3977
+ return { x: 0, y: 0 };
3978
+ const newest = samples[samples.length - 1];
3979
+ // Measure across the window when it holds a span of its own. With a single sample in it - a
3980
+ // pointer emitting moves slower than the window - reach back one sample for a baseline instead
3981
+ // of reporting nothing.
3982
+ const hasSpanInWindow = samples.length - firstInWindow >= 2;
3983
+ const baseline = samples[hasSpanInWindow ? firstInWindow : Math.max(0, firstInWindow - 1)];
3984
+ const elapsed = newest.t - baseline.t;
3985
+ if (elapsed <= 0)
3986
+ return { x: 0, y: 0 };
3987
+ return {
3988
+ x: ((newest.movementX - baseline.movementX) / elapsed) * 1000,
3989
+ y: ((newest.movementY - baseline.movementY) / elapsed) * 1000,
3990
+ };
3991
+ };
3799
3992
  const end = () => {
3800
- const swipeTime = Math.max(1, Date.now() - startTime);
3801
3993
  const positiveMovementX = Math.abs(movementX);
3802
3994
  const positiveMovementY = Math.abs(movementY);
3803
- const pixelPerSecondX = (movementX / swipeTime) * 1000;
3804
- const pixelPerSecondY = (movementY / swipeTime) * 1000;
3995
+ const { x: pixelPerSecondX, y: pixelPerSecondY } = measureReleaseVelocity(Date.now());
3805
3996
  return {
3806
3997
  movementX,
3807
3998
  movementY,
@@ -3818,11 +4009,12 @@ const createSwipeTracker = (startEvent) => {
3818
4009
  const cancel = () => {
3819
4010
  movementX = 0;
3820
4011
  movementY = 0;
4012
+ samples = [{ t: Date.now(), movementX: 0, movementY: 0 }];
3821
4013
  };
3822
4014
  return { update, end, cancel };
3823
4015
  };
3824
4016
 
3825
- const [, injectAngularRootElement] = createRootProvider(() => {
4017
+ const ANGULAR_ROOT_ELEMENT_DEF = /* @__PURE__ */ defineRootProvider(() => {
3826
4018
  const appRef = inject(ApplicationRef);
3827
4019
  const rootElement = signal(null, /* @ts-ignore */
3828
4020
  ...(ngDevMode ? [{ debugName: "rootElement" }] : /* istanbul ignore next */ []));
@@ -3838,8 +4030,9 @@ const [, injectAngularRootElement] = createRootProvider(() => {
3838
4030
  setTimeout(poll);
3839
4031
  return rootElement;
3840
4032
  });
4033
+ const injectAngularRootElement = /* @__PURE__ */ toInjectFn(ANGULAR_ROOT_ELEMENT_DEF);
3841
4034
 
3842
- const [provideBoundaryElement, injectBoundaryElement, BOUNDARY_ELEMENT_TOKEN] = createProvider(() => {
4035
+ const BOUNDARY_ELEMENT_DEF = /* @__PURE__ */ defineProvider(() => {
3843
4036
  const hostElement = inject(ElementRef, { optional: true });
3844
4037
  const document = inject(DOCUMENT);
3845
4038
  const override = signal(null, /* @ts-ignore */
@@ -3851,8 +4044,11 @@ const [provideBoundaryElement, injectBoundaryElement, BOUNDARY_ELEMENT_TOKEN] =
3851
4044
  override,
3852
4045
  };
3853
4046
  }, { name: 'Boundary Element' });
4047
+ const provideBoundaryElement = /* @__PURE__ */ toProvideFn(BOUNDARY_ELEMENT_DEF);
4048
+ const injectBoundaryElement = /* @__PURE__ */ toInjectFn(BOUNDARY_ELEMENT_DEF);
4049
+ const BOUNDARY_ELEMENT_TOKEN = /* @__PURE__ */ toToken(BOUNDARY_ELEMENT_DEF);
3854
4050
 
3855
- const [provideFocusVisibleTracker, injectFocusVisibleTracker, FOCUS_VISIBLE_TRACKER_TOKEN] = createRootProvider(() => {
4051
+ const FOCUS_VISIBLE_TRACKER_DEF = /* @__PURE__ */ defineRootProvider(() => {
3856
4052
  const document = inject(DOCUMENT);
3857
4053
  const destroyRef = inject(DestroyRef);
3858
4054
  const isFocusVisible = signal(false, /* @ts-ignore */
@@ -3894,8 +4090,11 @@ const [provideFocusVisibleTracker, injectFocusVisibleTracker, FOCUS_VISIBLE_TRAC
3894
4090
  isFocusVisible: isFocusVisible.asReadonly(),
3895
4091
  };
3896
4092
  }, { name: 'Focus Visible Tracker' });
4093
+ const provideFocusVisibleTracker = /* @__PURE__ */ toProvideFn(FOCUS_VISIBLE_TRACKER_DEF);
4094
+ const injectFocusVisibleTracker = /* @__PURE__ */ toInjectFn(FOCUS_VISIBLE_TRACKER_DEF);
4095
+ const FOCUS_VISIBLE_TRACKER_TOKEN = /* @__PURE__ */ toToken(FOCUS_VISIBLE_TRACKER_DEF);
3897
4096
 
3898
- const [provideLocale, injectLocale] = createRootProvider(() => {
4097
+ const LOCALE_DEF = /* @__PURE__ */ defineRootProvider(() => {
3899
4098
  const currentLocale = signal('en', /* @ts-ignore */
3900
4099
  ...(ngDevMode ? [{ debugName: "currentLocale" }] : /* istanbul ignore next */ []));
3901
4100
  return {
@@ -3904,8 +4103,65 @@ const [provideLocale, injectLocale] = createRootProvider(() => {
3904
4103
  }, {
3905
4104
  name: 'Locale',
3906
4105
  });
4106
+ const provideLocale = /* @__PURE__ */ toProvideFn(LOCALE_DEF);
4107
+ const injectLocale = /* @__PURE__ */ toInjectFn(LOCALE_DEF);
4108
+
4109
+ const resolveSource = (source, locale) => typeof source === 'function' ? source(locale) : source;
4110
+ /**
4111
+ * The one mechanism for the strings a component renders or announces itself. Every domain in the UI
4112
+ * library that has such strings exposes exactly this pair - `provide<Domain>Labels` to localize a
4113
+ * subtree, `inject<Domain>Labels()` to read the result - so localizing an app is the same move
4114
+ * repeated per domain rather than a different one each time.
4115
+ *
4116
+ * `defaults` may itself be locale-derived, for a domain that ships more than one language (the
4117
+ * `@ethlete/query` error tables, say). Overrides are partial and layer on top of whatever the locale
4118
+ * resolved to, so translating into a third language while keeping a shipped one as the base is the
4119
+ * same call.
4120
+ *
4121
+ * The injector returns a **signal**, because both halves can change at runtime - read it in a
4122
+ * template or computed, never destructure it once.
4123
+ *
4124
+ * @example
4125
+ * export type BreadcrumbLabels = { navigation: string; overflow: string };
4126
+ *
4127
+ * export const DEFAULT_BREADCRUMB_LABELS: BreadcrumbLabels = {
4128
+ * navigation: 'Breadcrumb',
4129
+ * overflow: 'Show hidden levels',
4130
+ * };
4131
+ *
4132
+ * const BREADCRUMB_LABELS_DEF = defineLabels<BreadcrumbLabels>('BREADCRUMB_LABELS', DEFAULT_BREADCRUMB_LABELS);
4133
+ *
4134
+ * export const provideBreadcrumbLabels = toProvideFn(BREADCRUMB_LABELS_DEF);
4135
+ * export const injectBreadcrumbLabels = toInjectFn(BREADCRUMB_LABELS_DEF);
4136
+ * export const BREADCRUMB_LABELS = toToken(BREADCRUMB_LABELS_DEF);
4137
+ *
4138
+ * @example
4139
+ * // consumer, fixed wording
4140
+ * provideBreadcrumbLabels({ navigation: 'Brotkrumen' });
4141
+ *
4142
+ * // consumer, driven by the app's i18n and re-resolved on a locale change
4143
+ * provideBreadcrumbLabels((locale) => ({ navigation: translate('breadcrumb.nav', locale) }));
4144
+ */
4145
+ const defineLabels = (name, defaults) => {
4146
+ const token = new InjectionToken(name, {
4147
+ providedIn: 'root',
4148
+ factory: () => ({}),
4149
+ });
4150
+ return {
4151
+ provide: (labels) => ({ provide: token, useValue: labels }),
4152
+ inject: () => {
4153
+ const source = inject(token);
4154
+ const { currentLocale } = injectLocale();
4155
+ return computed(() => {
4156
+ const locale = currentLocale();
4157
+ return { ...resolveSource(defaults, locale), ...resolveSource(source, locale) };
4158
+ });
4159
+ },
4160
+ token,
4161
+ };
4162
+ };
3907
4163
 
3908
- const [provideStyleManager, injectStyleManager] = createRootProvider(() => {
4164
+ const STYLE_MANAGER_DEF = /* @__PURE__ */ defineRootProvider(() => {
3909
4165
  const appRef = inject(ApplicationRef);
3910
4166
  const destroyRef = inject(DestroyRef);
3911
4167
  const document = inject(DOCUMENT);
@@ -3964,6 +4220,8 @@ const [provideStyleManager, injectStyleManager] = createRootProvider(() => {
3964
4220
  });
3965
4221
  return { mount };
3966
4222
  }, { name: 'StyleManager' });
4223
+ const provideStyleManager = /* @__PURE__ */ toProvideFn(STYLE_MANAGER_DEF);
4224
+ const injectStyleManager = /* @__PURE__ */ toInjectFn(STYLE_MANAGER_DEF);
3967
4225
 
3968
4226
  const createUserConsentProvider = (options) => ({
3969
4227
  provide: options.for,
@@ -3987,8 +4245,23 @@ const fromNextFrame = () => {
3987
4245
  });
3988
4246
  });
3989
4247
  };
3990
- const forceReflow = (element = document.body) => {
3991
- return element.offsetHeight;
4248
+ const forceReflow = (element) => {
4249
+ // The `document.body` fallback is resolved lazily and guarded: there is nothing to reflow on the
4250
+ // server, so this becomes a no-op instead of throwing on a missing `document`.
4251
+ const el = element ?? (typeof document === 'undefined' ? null : document.body);
4252
+ return el?.offsetHeight ?? 0;
4253
+ };
4254
+ /**
4255
+ * Whether the user asked for reduced motion, read straight from `matchMedia`.
4256
+ *
4257
+ * Inside a component, prefer the reactive `injectPrefersReducedMotion()`. This is for the animation
4258
+ * helpers that run from effects, event handlers and imperative APIs, where there is no injection
4259
+ * context. Pass the element being animated so the query resolves against its own window - otherwise
4260
+ * an animation inside an iframe would read the top-level document's preference.
4261
+ */
4262
+ const matchesReducedMotion = (element) => {
4263
+ const view = element?.ownerDocument.defaultView ?? (typeof window === 'undefined' ? null : window);
4264
+ return view?.matchMedia('(prefers-reduced-motion: reduce)').matches ?? false;
3992
4265
  };
3993
4266
 
3994
4267
  const ANIMATED_LIFECYCLE_TOKEN = new InjectionToken('ANIMATED_LIFECYCLE_DIRECTIVE_TOKEN');
@@ -4214,7 +4487,7 @@ class AnimatedLifecycleDirective {
4214
4487
  if (!e.cancelled && e.transitionId === transitionId) {
4215
4488
  return of(true);
4216
4489
  }
4217
- // The batch ended cancelled or under a different id — e.g. the browser cancelled and
4490
+ // The batch ended cancelled or under a different id - e.g. the browser cancelled and
4218
4491
  // restarted a running transition mid-flight (an anchored overlay whose ancestor pane got
4219
4492
  // destroyed) or a foreign transition on the same element finished. If nothing is
4220
4493
  // animating anymore the expected end event can never arrive, so treat the transition as
@@ -4361,327 +4634,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
4361
4634
  }]
4362
4635
  }], ctorParameters: () => [], propDecorators: { ifValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "etAnimatedIf", required: false }] }] } });
4363
4636
 
4364
- class AnimatedOverlayDirective {
4365
- constructor() {
4366
- this.overlayService = inject(Overlay);
4367
- this.injector = inject(Injector);
4368
- this.viewContainerRef = inject(ViewContainerRef);
4369
- this.zone = inject(NgZone);
4370
- this.elementRef = inject(ElementRef);
4371
- this.rootBoundary = injectBoundaryElement({ optional: true });
4372
- this.destroyRef = inject(DestroyRef);
4373
- this.floatingElCleanupFn = null;
4374
- this.unmountSubscription = null;
4375
- this.placement = input('bottom', /* @ts-ignore */
4376
- ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
4377
- this.fallbackPlacements = input(/* @ts-ignore */
4378
- ...(ngDevMode ? [undefined, { debugName: "fallbackPlacements" }] : /* istanbul ignore next */ []));
4379
- this.offset = input(null, /* @ts-ignore */
4380
- ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
4381
- this.arrowPadding = input(4, /* @ts-ignore */
4382
- ...(ngDevMode ? [{ debugName: "arrowPadding" }] : /* istanbul ignore next */ []));
4383
- this.viewportPadding = input(8, /* @ts-ignore */
4384
- ...(ngDevMode ? [{ debugName: "viewportPadding" }] : /* istanbul ignore next */ []));
4385
- this.autoResize = input(false, { ...(ngDevMode ? { debugName: "autoResize" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4386
- this.shift = input(true, { ...(ngDevMode ? { debugName: "shift" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4387
- this.autoHide = input(false, { ...(ngDevMode ? { debugName: "autoHide" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4388
- this.autoCloseIfReferenceHidden = input(false, { ...(ngDevMode ? { debugName: "autoCloseIfReferenceHidden" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4389
- this.referenceElement = input(this.elementRef.nativeElement, /* @ts-ignore */
4390
- ...(ngDevMode ? [{ debugName: "referenceElement" }] : /* istanbul ignore next */ []));
4391
- this.mirrorWidth = input(false, { ...(ngDevMode ? { debugName: "mirrorWidth" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4392
- this.portal = null;
4393
- this.overlayRef = null;
4394
- this.componentRef = null;
4395
- this.beforeOpened$ = new Subject();
4396
- this.afterOpened$ = new Subject();
4397
- this.beforeClosed$ = new Subject();
4398
- this.afterClosed$ = new Subject();
4399
- this.state = signal('init', /* @ts-ignore */
4400
- ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
4401
- this.isMounted = computed(() => this.state() === 'mounted', /* @ts-ignore */
4402
- ...(ngDevMode ? [{ debugName: "isMounted" }] : /* istanbul ignore next */ []));
4403
- this.isMounting = computed(() => this.state() === 'mounting', /* @ts-ignore */
4404
- ...(ngDevMode ? [{ debugName: "isMounting" }] : /* istanbul ignore next */ []));
4405
- this.isUnmounted = computed(() => this.state() === 'unmounted', /* @ts-ignore */
4406
- ...(ngDevMode ? [{ debugName: "isUnmounted" }] : /* istanbul ignore next */ []));
4407
- this.isUnmounting = computed(() => this.state() === 'unmounting', /* @ts-ignore */
4408
- ...(ngDevMode ? [{ debugName: "isUnmounting" }] : /* istanbul ignore next */ []));
4409
- this.canMount = computed(() => !this.isMounted() && !this.isMounting(), /* @ts-ignore */
4410
- ...(ngDevMode ? [{ debugName: "canMount" }] : /* istanbul ignore next */ []));
4411
- this.canUnmount = computed(() => (this.isMounted() || this.isMounting()) && !this.isUnmounting(), /* @ts-ignore */
4412
- ...(ngDevMode ? [{ debugName: "canUnmount" }] : /* istanbul ignore next */ []));
4413
- this.isHidden = signal(false, /* @ts-ignore */
4414
- ...(ngDevMode ? [{ debugName: "isHidden" }] : /* istanbul ignore next */ []));
4415
- this.isMounted$ = toObservable(this.isMounted);
4416
- this.isMounting$ = toObservable(this.isMounting);
4417
- this.isUnmounted$ = toObservable(this.isUnmounted);
4418
- this.isUnmounting$ = toObservable(this.isUnmounting);
4419
- this.canMount$ = toObservable(this.canMount);
4420
- this.canUnmount$ = toObservable(this.canUnmount);
4421
- this.isHidden$ = toObservable(this.isHidden);
4422
- this.referenceElementDimensions = signalElementDimensions(computed(() => (this.mirrorWidth() ? this.referenceElement() : null)));
4423
- effect(() => {
4424
- const dimensions = this.referenceElementDimensions();
4425
- if (!dimensions || !this.mirrorWidth())
4426
- return;
4427
- this.overlayRef?.updateSize({
4428
- width: this.elementRef.nativeElement.offsetWidth,
4429
- });
4430
- });
4431
- this.destroyRef.onDestroy(() => this.destroy());
4432
- }
4433
- mount(config) {
4434
- if (this.isUnmounting()) {
4435
- if (!this.componentRef) {
4436
- if (isDevMode()) {
4437
- console.warn('AnimatedOverlayDirective: Cannot remount. Component ref is not available.');
4438
- }
4439
- return;
4440
- }
4441
- this.unmountSubscription?.unsubscribe();
4442
- this.unmountSubscription = null;
4443
- this.applyComponentData(config.data);
4444
- this.applyColorProvider(config.colorProvider);
4445
- const lifecycle = this.componentRef.instance.animatedLifecycle();
4446
- if (lifecycle) {
4447
- this.state.set('mounting');
4448
- this.beforeOpened$.next();
4449
- lifecycle.enter();
4450
- lifecycle.state$
4451
- .pipe(filter((s) => s === 'entered'), tap(() => {
4452
- if (this.state() === 'mounting') {
4453
- this.state.set('mounted');
4454
- this.afterOpened$.next();
4455
- }
4456
- }), take(1), takeUntilDestroyed(this.destroyRef))
4457
- .subscribe();
4458
- }
4459
- return this.componentRef.instance;
4460
- }
4461
- if (!this.canMount()) {
4462
- if (isDevMode()) {
4463
- console.warn('AnimatedOverlayDirective: Cannot mount. Component is already mounted or currently mounting.');
4464
- }
4465
- return;
4466
- }
4467
- const { component, providers, data, colorProvider } = config;
4468
- this.state.set('mounting');
4469
- this.beforeOpened$.next();
4470
- const injector = Injector.create({
4471
- parent: this.injector,
4472
- providers: providers ?? [],
4473
- });
4474
- this.overlayRef = this.overlayService.create();
4475
- this.portal = new ComponentPortal(component, this.viewContainerRef, injector);
4476
- this.componentRef = this.overlayRef.attach(this.portal);
4477
- this.applyComponentData(data);
4478
- this.applyColorProvider(colorProvider);
4479
- this.updateOverlaySize();
4480
- this.setupFloatingUI();
4481
- return this.componentRef.instance;
4482
- }
4483
- unmount() {
4484
- if (!this.isMounted() && !this.isMounting() && !this.isUnmounting()) {
4485
- if (isDevMode()) {
4486
- console.warn(`AnimatedOverlayDirective: Cannot unmount. Component is currently ${this.state()}`);
4487
- }
4488
- return;
4489
- }
4490
- if (this.isUnmounting()) {
4491
- return;
4492
- }
4493
- if (!this.componentRef) {
4494
- if (isDevMode()) {
4495
- console.warn('AnimatedOverlayDirective: Cannot unmount. Component ref is not available.');
4496
- }
4497
- return;
4498
- }
4499
- this.state.set('unmounting');
4500
- this.beforeClosed$.next();
4501
- if (this.isHidden()) {
4502
- this.destroy();
4503
- return;
4504
- }
4505
- const lifecycle = this.componentRef.instance.animatedLifecycle();
4506
- if (!lifecycle) {
4507
- this.destroy();
4508
- return;
4509
- }
4510
- lifecycle.leave();
4511
- this.unmountSubscription = lifecycle.state$
4512
- .pipe(filter((s) => s === 'left'), take(1), takeUntilDestroyed(this.destroyRef))
4513
- .subscribe(() => {
4514
- this.unmountSubscription = null;
4515
- this.destroy();
4516
- });
4517
- }
4518
- applyComponentData(data) {
4519
- if (!this.componentRef || !data)
4520
- return;
4521
- Object.assign(this.componentRef.instance, data);
4522
- this.componentRef.changeDetectorRef.markForCheck();
4523
- }
4524
- applyColorProvider(colorProvider) {
4525
- if (!this.componentRef || !colorProvider)
4526
- return;
4527
- this.componentRef.instance.setColorFromProvider(colorProvider);
4528
- }
4529
- updateOverlaySize() {
4530
- if (!this.overlayRef || !this.mirrorWidth())
4531
- return;
4532
- this.overlayRef.updateSize({
4533
- width: this.elementRef.nativeElement.offsetWidth,
4534
- });
4535
- }
4536
- setupFloatingUI() {
4537
- this.zone.runOutsideAngular(() => {
4538
- if (!this.componentRef)
4539
- return;
4540
- const floatingEl = this.componentRef.location.nativeElement;
4541
- const floatingElArrow = this.getFloatingArrow();
4542
- floatingEl.classList.add('et-floating-element');
4543
- const refEl = this.referenceElement();
4544
- const boundary = this.rootBoundary?.value();
4545
- this.floatingElCleanupFn = autoUpdate(refEl, floatingEl, () => {
4546
- this.updateFloatingPosition(floatingEl, refEl, boundary, floatingElArrow);
4547
- });
4548
- this.waitForRenderAndAnimate();
4549
- });
4550
- }
4551
- getFloatingArrow() {
4552
- if (!this.componentRef)
4553
- return null;
4554
- return this.componentRef.location.nativeElement.querySelector('[et-floating-arrow]');
4555
- }
4556
- updateFloatingPosition(floatingEl, refEl, boundary, floatingElArrow) {
4557
- if (!this.componentRef)
4558
- return;
4559
- const middleware = this.buildFloatingMiddleware(floatingEl, boundary, floatingElArrow);
4560
- computePosition(refEl, floatingEl, {
4561
- placement: this.placement(),
4562
- middleware,
4563
- }).then(({ x, y, placement, middlewareData }) => {
4564
- this.applyFloatingStyles(floatingEl, x, y, placement);
4565
- this.applyArrowStyles(floatingEl, middlewareData, floatingElArrow);
4566
- this.handleReferenceHidden(floatingEl, middlewareData);
4567
- });
4568
- }
4569
- buildFloatingMiddleware(floatingEl, boundary, floatingElArrow) {
4570
- const middleware = [];
4571
- const offsetValue = this.offset();
4572
- if (offsetValue !== null) {
4573
- middleware.push(offset(offsetValue));
4574
- }
4575
- middleware.push(flip({
4576
- fallbackPlacements: this.fallbackPlacements() ?? undefined,
4577
- fallbackAxisSideDirection: 'start',
4578
- boundary,
4579
- }));
4580
- if (this.autoResize()) {
4581
- middleware.push(size({
4582
- padding: this.viewportPadding() ?? undefined,
4583
- apply({ availableHeight, availableWidth }) {
4584
- floatingEl.style.setProperty('--et-floating-max-width', `${availableWidth}px`);
4585
- floatingEl.style.setProperty('--et-floating-max-height', `${availableHeight}px`);
4586
- },
4587
- }));
4588
- }
4589
- if (this.shift()) {
4590
- middleware.push(shift({
4591
- limiter: limitShift(),
4592
- padding: this.viewportPadding() ?? undefined,
4593
- boundary,
4594
- }));
4595
- }
4596
- if (floatingElArrow) {
4597
- middleware.push(arrow({ element: floatingElArrow, padding: this.arrowPadding() ?? undefined }));
4598
- }
4599
- if (this.autoHide() || this.autoCloseIfReferenceHidden()) {
4600
- middleware.push(hide({ strategy: 'referenceHidden', boundary }));
4601
- }
4602
- return middleware;
4603
- }
4604
- applyFloatingStyles(floatingEl, x, y, placement) {
4605
- floatingEl.style.setProperty('--et-floating-translate', `translate3d(${x}px, ${y}px, 0)`);
4606
- floatingEl.setAttribute('et-floating-placement', placement);
4607
- }
4608
- applyArrowStyles(floatingEl, middlewareData, floatingElArrow) {
4609
- if (!middlewareData.arrow || !floatingElArrow)
4610
- return;
4611
- const { x: arrowX, y: arrowY } = middlewareData.arrow;
4612
- floatingEl.style.setProperty('--et-floating-arrow-translate', `translate3d(${arrowX ?? 0}px, ${arrowY ?? 0}px, 0)`);
4613
- }
4614
- handleReferenceHidden(floatingEl, middlewareData) {
4615
- if (middlewareData.hide?.referenceHidden) {
4616
- if (this.autoCloseIfReferenceHidden()) {
4617
- this.unmount();
4618
- }
4619
- else {
4620
- floatingEl.classList.add('et-floating-element--hidden');
4621
- this.isHidden.set(true);
4622
- }
4623
- }
4624
- else if (this.autoHide()) {
4625
- floatingEl.classList.remove('et-floating-element--hidden');
4626
- this.isHidden.set(false);
4627
- }
4628
- }
4629
- waitForRenderAndAnimate() {
4630
- nextFrame(() => {
4631
- if (!this.componentRef || this.state() !== 'mounting') {
4632
- return;
4633
- }
4634
- const lifecycle = this.componentRef.instance.animatedLifecycle?.();
4635
- if (!lifecycle) {
4636
- console.error('AnimatedOverlayDirective: The component does not have an AnimatedLifecycleDirective. Please add one to the component.');
4637
- return;
4638
- }
4639
- lifecycle.enter();
4640
- lifecycle.state$
4641
- .pipe(filter((s) => s === 'entered'), tap(() => {
4642
- if (this.state() === 'mounting') {
4643
- this.state.set('mounted');
4644
- this.afterOpened$.next();
4645
- }
4646
- }), take(1), takeUntilDestroyed(this.destroyRef))
4647
- .subscribe();
4648
- });
4649
- }
4650
- destroy() {
4651
- this.zone.runOutsideAngular(() => {
4652
- this.floatingElCleanupFn?.();
4653
- });
4654
- if (this.overlayRef) {
4655
- this.overlayRef.dispose();
4656
- this.overlayRef = null;
4657
- }
4658
- if (this.componentRef) {
4659
- this.componentRef.destroy();
4660
- this.componentRef = null;
4661
- }
4662
- this.state.set('unmounted');
4663
- this.afterClosed$.next();
4664
- }
4665
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: AnimatedOverlayDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
4666
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: AnimatedOverlayDirective, isStandalone: true, selector: "[etAnimatedOverlay]", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, fallbackPlacements: { classPropertyName: "fallbackPlacements", publicName: "fallbackPlacements", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, arrowPadding: { classPropertyName: "arrowPadding", publicName: "arrowPadding", isSignal: true, isRequired: false, transformFunction: null }, viewportPadding: { classPropertyName: "viewportPadding", publicName: "viewportPadding", isSignal: true, isRequired: false, transformFunction: null }, autoResize: { classPropertyName: "autoResize", publicName: "autoResize", isSignal: true, isRequired: false, transformFunction: null }, shift: { classPropertyName: "shift", publicName: "shift", isSignal: true, isRequired: false, transformFunction: null }, autoHide: { classPropertyName: "autoHide", publicName: "autoHide", isSignal: true, isRequired: false, transformFunction: null }, autoCloseIfReferenceHidden: { classPropertyName: "autoCloseIfReferenceHidden", publicName: "autoCloseIfReferenceHidden", isSignal: true, isRequired: false, transformFunction: null }, referenceElement: { classPropertyName: "referenceElement", publicName: "referenceElement", isSignal: true, isRequired: false, transformFunction: null }, mirrorWidth: { classPropertyName: "mirrorWidth", publicName: "mirrorWidth", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "et-animated-overlay" }, exportAs: ["etAnimatedOverlay"], ngImport: i0 }); }
4667
- }
4668
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: AnimatedOverlayDirective, decorators: [{
4669
- type: Directive,
4670
- args: [{
4671
- selector: '[etAnimatedOverlay]',
4672
- exportAs: 'etAnimatedOverlay',
4673
- host: {
4674
- class: 'et-animated-overlay',
4675
- },
4676
- }]
4677
- }], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], fallbackPlacements: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackPlacements", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], arrowPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrowPadding", required: false }] }], viewportPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewportPadding", required: false }] }], autoResize: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoResize", required: false }] }], shift: [{ type: i0.Input, args: [{ isSignal: true, alias: "shift", required: false }] }], autoHide: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoHide", required: false }] }], autoCloseIfReferenceHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoCloseIfReferenceHidden", required: false }] }], referenceElement: [{ type: i0.Input, args: [{ isSignal: true, alias: "referenceElement", required: false }] }], mirrorWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "mirrorWidth", required: false }] }] } });
4678
-
4679
4637
  const createFlipAnimationGroup = (config) => {
4680
- const { elements, duration = 250, easing = 'cubic-bezier(0.4, 0, 0.2, 1)' } = config;
4638
+ const { elements, duration = 250, easing = 'cubic-bezier(0.4, 0, 0.2, 1)', ignoreReducedMotion } = config;
4681
4639
  const flips = elements.map((el) => {
4682
4640
  const element = 'element' in el ? el.element : el;
4683
4641
  const originElement = 'originElement' in el ? el.originElement : undefined;
4684
- return createFlipAnimation({ element, originElement, duration, easing });
4642
+ return createFlipAnimation({ element, originElement, duration, easing, ignoreReducedMotion });
4685
4643
  });
4686
4644
  const onStart$ = combineLatest(flips.map((animation) => animation.onStart$));
4687
4645
  const onFinish$ = combineLatest(flips.map((animation) => animation.onFinish$));
@@ -4705,7 +4663,7 @@ const createFlipAnimationGroup = (config) => {
4705
4663
  };
4706
4664
  };
4707
4665
  const createFlipAnimation = (config) => {
4708
- const { element: el, originElement = el, duration = 250, easing = 'cubic-bezier(0.4, 0, 0.2, 1)' } = config;
4666
+ const { element: el, originElement = el, duration = 250, easing = 'cubic-bezier(0.4, 0, 0.2, 1)', ignoreReducedMotion = false, } = config;
4709
4667
  let initialRect = originElement.getBoundingClientRect();
4710
4668
  let animation = null;
4711
4669
  const onStart$ = new Subject();
@@ -4737,6 +4695,10 @@ const createFlipAnimation = (config) => {
4737
4695
  scaleX: initialRect.width / lastRect.width,
4738
4696
  scaleY: initialRect.height / lastRect.height,
4739
4697
  };
4698
+ // Under reduced motion the element jumps straight to its final position. Collapsing the duration
4699
+ // instead of bailing out keeps the `Animation` lifecycle intact, so callers that wait on
4700
+ // `onFinish$` to clean up still get their event.
4701
+ const effectiveDuration = !ignoreReducedMotion && matchesReducedMotion(el) ? 0 : duration;
4740
4702
  animation = el.animate([
4741
4703
  {
4742
4704
  transformOrigin: 'top left',
@@ -4750,7 +4712,7 @@ const createFlipAnimation = (config) => {
4750
4712
  transform: 'none',
4751
4713
  },
4752
4714
  ], {
4753
- duration,
4715
+ duration: effectiveDuration,
4754
4716
  easing,
4755
4717
  fill: 'both',
4756
4718
  });
@@ -5015,7 +4977,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
5015
4977
  }]
5016
4978
  }], ctorParameters: () => [], propDecorators: { commitThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitThreshold", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], dragTapped: [{ type: i0.Output, args: ["dragTapped"] }], dragStarted: [{ type: i0.Output, args: ["dragStarted"] }], dragMoved: [{ type: i0.Output, args: ["dragMoved"] }], dragEnded: [{ type: i0.Output, args: ["dragEnded"] }] } });
5017
4979
 
5018
- const FOCUSABLE_SELECTOR = [
4980
+ const FOCUSABLE_SELECTOR = /* @__PURE__ */ [
5019
4981
  'a[href]',
5020
4982
  'area[href]',
5021
4983
  'button:not([disabled])',
@@ -5176,6 +5138,56 @@ const resetPositioningStyles = (config, hostElement, paneElement, renderer) => {
5176
5138
  renderer.removeAttribute(paneElement, 'data-overlay-placement');
5177
5139
  setBaseElementStyles(config, hostElement, paneElement, renderer);
5178
5140
  };
5141
+ let anchoredPositionSetup = null;
5142
+ /**
5143
+ * Installs the anchored positioning implementation. Called by `anchoredOverlayPosition()`, which is
5144
+ * the only thing that pulls `@floating-ui/dom` into the bundle - apps that never anchor an overlay
5145
+ * do not ship it.
5146
+ *
5147
+ * @internal
5148
+ */
5149
+ const registerAnchoredPositionSetup = (setup) => {
5150
+ anchoredPositionSetup = setup;
5151
+ };
5152
+ const setupPositioning = (config, hostElement, paneElement, overlayRef, renderer) => {
5153
+ const strategy = config.positionStrategy ?? { kind: 'center' };
5154
+ if (strategy.kind === 'anchored') {
5155
+ renderer.setStyle(hostElement, {
5156
+ pointerEvents: config.hasBackdrop === false ? 'none' : 'auto',
5157
+ });
5158
+ if (!anchoredPositionSetup) {
5159
+ if (isDevMode()) {
5160
+ console.error('An overlay was mounted with an anchored position strategy, but anchored positioning is not installed. Build the strategy with `anchoredOverlayPosition()` from @ethlete/core so the positioning code is part of the bundle.', strategy);
5161
+ }
5162
+ applyCenteredPosition(hostElement, paneElement, renderer, { kind: 'center' });
5163
+ return () => undefined;
5164
+ }
5165
+ return anchoredPositionSetup(strategy, paneElement, overlayRef, renderer);
5166
+ }
5167
+ if (strategy.kind === 'global') {
5168
+ applyGlobalPosition(hostElement, paneElement, renderer, strategy);
5169
+ return () => undefined;
5170
+ }
5171
+ applyCenteredPosition(hostElement, paneElement, renderer, strategy);
5172
+ return () => undefined;
5173
+ };
5174
+
5175
+ let middlewareExtras = null;
5176
+ /**
5177
+ * Installs the `size` / `arrow` / `hide` middleware. Called by
5178
+ * `enableAnchoredOverlayPositionExtras()`.
5179
+ *
5180
+ * @internal
5181
+ */
5182
+ const registerAnchoredPositionMiddlewareExtras = (extras) => {
5183
+ middlewareExtras = extras;
5184
+ };
5185
+ const requireMiddlewareExtras = (feature) => {
5186
+ if (!middlewareExtras && isDevMode()) {
5187
+ console.error(`An anchored overlay uses "${feature}", but the matching floating-ui middleware is not installed. Call \`enableAnchoredOverlayPositionExtras()\` from @ethlete/core once during app setup.`);
5188
+ }
5189
+ return middlewareExtras;
5190
+ };
5179
5191
  const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, renderer) => {
5180
5192
  // mirrorWidth needs a real element to measure; virtual references fall back to max-content
5181
5193
  const mirrorWidthElement = strategy.mirrorWidth && isHTMLElement(strategy.referenceElement) ? strategy.referenceElement : null;
@@ -5202,29 +5214,41 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
5202
5214
  boundary: strategy.boundary,
5203
5215
  }));
5204
5216
  }
5205
- // size must run AFTER shift so the available space is measured from the shifted position —
5217
+ // size must run AFTER shift so the available space is measured from the shifted position -
5206
5218
  // otherwise a cross-axis-shifted pane gets its max size capped to the unshifted leftover space
5207
5219
  if (strategy.autoResize) {
5208
- middleware.push(size({
5209
- padding: strategy.viewportPadding ?? 8,
5210
- apply({ availableHeight, availableWidth }) {
5211
- renderer.setCssProperties(paneElement, {
5212
- '--et-overlay-max-width': `${availableWidth}px`,
5213
- '--et-overlay-max-height': `${availableHeight}px`,
5214
- });
5215
- },
5216
- }));
5220
+ const extras = requireMiddlewareExtras('autoResize');
5221
+ if (extras) {
5222
+ middleware.push(extras.size({
5223
+ padding: strategy.viewportPadding ?? 8,
5224
+ apply({ availableHeight, availableWidth }) {
5225
+ renderer.setCssProperties(paneElement, {
5226
+ '--et-overlay-max-width': `${availableWidth}px`,
5227
+ '--et-overlay-max-height': `${availableHeight}px`,
5228
+ });
5229
+ },
5230
+ }));
5231
+ }
5217
5232
  }
5218
5233
  if (arrowElement) {
5219
- middleware.push(arrow({
5220
- element: arrowElement,
5221
- padding: strategy.arrowPadding ?? 4,
5222
- }));
5234
+ const extras = requireMiddlewareExtras('arrow');
5235
+ if (extras) {
5236
+ middleware.push(extras.arrow({
5237
+ element: arrowElement,
5238
+ // keeps the arrow off a rounded pane corner by default: 12px covers the radius of the
5239
+ // built-in boxed panes (--et-overlay-radius). Panes with a larger radius have to pass their
5240
+ // own - an arrow whose base sits on the corner arc looks detached from the pane.
5241
+ padding: strategy.arrowPadding ?? 12,
5242
+ }));
5243
+ }
5223
5244
  }
5224
5245
  if (strategy.autoHide || strategy.autoCloseIfReferenceHidden) {
5225
- middleware.push(hide({
5226
- strategy: 'referenceHidden',
5227
- }));
5246
+ const extras = requireMiddlewareExtras('autoHide');
5247
+ if (extras) {
5248
+ middleware.push(extras.hide({
5249
+ strategy: 'referenceHidden',
5250
+ }));
5251
+ }
5228
5252
  }
5229
5253
  computePosition(strategy.referenceElement, paneElement, {
5230
5254
  placement: strategy.placement ?? 'bottom',
@@ -5265,20 +5289,29 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
5265
5289
  cleanup();
5266
5290
  };
5267
5291
  };
5268
- const setupPositioning = (config, hostElement, paneElement, overlayRef, renderer) => {
5269
- const strategy = config.positionStrategy ?? { kind: 'center' };
5270
- if (strategy.kind === 'anchored') {
5271
- renderer.setStyle(hostElement, {
5272
- pointerEvents: config.hasBackdrop === false ? 'none' : 'auto',
5273
- });
5274
- return createAnchoredPositionCleanup(strategy, paneElement, overlayRef, renderer);
5275
- }
5276
- if (strategy.kind === 'global') {
5277
- applyGlobalPosition(hostElement, paneElement, renderer, strategy);
5278
- return () => undefined;
5279
- }
5280
- applyCenteredPosition(hostElement, paneElement, renderer, strategy);
5281
- return () => undefined;
5292
+ /**
5293
+ * Builds an anchored position strategy and installs the anchored positioning implementation.
5294
+ *
5295
+ * Anchored positioning is the only part of the overlay runtime that needs `@floating-ui/dom`, and it
5296
+ * is reachable only through this function - an app that positions its overlays centered or globally
5297
+ * never bundles it. Always build anchored strategies with this helper instead of a plain
5298
+ * `{ kind: 'anchored', ... }` literal.
5299
+ *
5300
+ * Overlays that set `autoResize`, `autoHide`, `autoCloseIfReferenceHidden` or render an arrow
5301
+ * additionally need `enableAnchoredOverlayPositionExtras()`.
5302
+ */
5303
+ const anchoredOverlayPosition = (config) => {
5304
+ registerAnchoredPositionSetup(createAnchoredPositionCleanup);
5305
+ return { kind: 'anchored', ...config };
5306
+ };
5307
+
5308
+ /**
5309
+ * Installs the floating-ui middleware anchored overlays need for `autoResize`, `autoHide`,
5310
+ * `autoCloseIfReferenceHidden` and arrows. Call it once wherever such an overlay is built - an
5311
+ * anchored overlay that uses none of those features does not bundle this middleware.
5312
+ */
5313
+ const enableAnchoredOverlayPositionExtras = () => {
5314
+ registerAnchoredPositionMiddlewareExtras({ size, arrow, hide });
5282
5315
  };
5283
5316
 
5284
5317
  const createOverlayRuntimeRef = (id, config, elements, requestClose) => {
@@ -5298,13 +5331,13 @@ const createOverlayRuntimeRef = (id, config, elements, requestClose) => {
5298
5331
  elements,
5299
5332
  state: _state.asReadonly(),
5300
5333
  componentInstance: _componentInstance.asReadonly(),
5301
- // Internal — driven by overlay-runtime.ts
5334
+ // Internal - driven by overlay-runtime.ts
5302
5335
  beforeOpenedSubject,
5303
5336
  close(result, source = 'api') {
5304
5337
  if (_state() === 'closing' || _state() === 'closed') {
5305
5338
  return;
5306
5339
  }
5307
- // `reference-detached` is a forced teardown (the anchor is gone) — never vetoable.
5340
+ // `reference-detached` is a forced teardown (the anchor is gone) - never vetoable.
5308
5341
  if (source !== 'reference-detached') {
5309
5342
  for (const guard of closeGuards) {
5310
5343
  if (!guard({ result, source })) {
@@ -5314,7 +5347,7 @@ const createOverlayRuntimeRef = (id, config, elements, requestClose) => {
5314
5347
  }
5315
5348
  requestClose(result, source);
5316
5349
  },
5317
- /** Close bypassing every registered close guard — used by a guard's owner to commit a
5350
+ /** Close bypassing every registered close guard - used by a guard's owner to commit a
5318
5351
  * close it previously vetoed (e.g. after an async confirm resolved). */
5319
5352
  forceClose(result, source = 'api') {
5320
5353
  if (_state() === 'closing' || _state() === 'closed') {
@@ -5381,7 +5414,7 @@ const createOverlayRuntimeRef = (id, config, elements, requestClose) => {
5381
5414
  };
5382
5415
  };
5383
5416
 
5384
- const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
5417
+ const OVERLAY_RUNTIME_DEF = /* @__PURE__ */ defineRootProvider(() => {
5385
5418
  const appRef = inject(ApplicationRef);
5386
5419
  const destroyRef = inject(DestroyRef);
5387
5420
  const document = inject(DOCUMENT);
@@ -5488,7 +5521,7 @@ const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
5488
5521
  hostElement: paneElement,
5489
5522
  bindings: config.bindings ?? [],
5490
5523
  });
5491
- // pane classes are applied after component creation — Angular replaces the host
5524
+ // pane classes are applied after component creation - Angular replaces the host
5492
5525
  // element's class attribute with the component's static host class on creation
5493
5526
  renderer.addClass(paneElement, 'et-overlay-runtime-pane', ...(config.paneClass ?? []));
5494
5527
  appRef.attachView(componentRef.hostView);
@@ -5535,7 +5568,7 @@ const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
5535
5568
  }
5536
5569
  const lifecycle = getAnimatedLifecycle(componentRef);
5537
5570
  animationDebugLog(`runtime ${config.id}`, `close requested (source "${closeEvent.source}", lifecycle state "${lifecycle?.state$.value ?? 'none'}", delegate ${config.animationDelegate?.leave ? 'yes' : 'no'})`);
5538
- // The reference element is gone — there is nothing left to animate away from, so tear the
5571
+ // The reference element is gone - there is nothing left to animate away from, so tear the
5539
5572
  // overlay down synchronously instead of playing a leave transition from a stale position.
5540
5573
  if (!lifecycle || closeEvent.source === 'reference-detached') {
5541
5574
  destroyMountedOverlay(closeEvent);
@@ -5644,6 +5677,8 @@ const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
5644
5677
  openEntries,
5645
5678
  };
5646
5679
  }, { name: 'OverlayRuntime' });
5680
+ const provideOverlayRuntime = /* @__PURE__ */ toProvideFn(OVERLAY_RUNTIME_DEF);
5681
+ const injectOverlayRuntime = /* @__PURE__ */ toInjectFn(OVERLAY_RUNTIME_DEF);
5647
5682
 
5648
5683
  class NormalizeGameResultTypePipe {
5649
5684
  constructor() {
@@ -6684,7 +6719,7 @@ const toStringBinding = (binding) => {
6684
6719
  return value !== null && value !== undefined ? String(value) : null;
6685
6720
  });
6686
6721
  };
6687
- const createBulkPropertyBinding = (propertyPrefix, applyConfigFn) => {
6722
+ const createBulkPropertyBinding = (_propertyPrefix, applyConfigFn) => {
6688
6723
  return (config) => {
6689
6724
  Object.entries(config).forEach(([key, value]) => {
6690
6725
  if (value !== undefined) {
@@ -6694,11 +6729,202 @@ const createBulkPropertyBinding = (propertyPrefix, applyConfigFn) => {
6694
6729
  };
6695
6730
  };
6696
6731
 
6732
+ /** The rendered favicon is this many device-independent pixels square. */
6733
+ const FAVICON_SIZE = 64;
6734
+ /** Fallback when the color theming tokens aren't resolvable (no theme applied to the root element). */
6735
+ const FALLBACK_OVERLAY_COLOR = '#e11d48';
6736
+ const readThemeColor = (document) => {
6737
+ const value = getComputedStyle(document.documentElement).getPropertyValue('--et-theme-color-primary-solid').trim();
6738
+ return value || FALLBACK_OVERLAY_COLOR;
6739
+ };
6740
+ const drawDot = (ctx, color) => {
6741
+ const radius = FAVICON_SIZE * 0.19;
6742
+ const center = FAVICON_SIZE - radius - 3;
6743
+ // Punch a hole first so the badge reads on top of a busy icon, then fill it.
6744
+ ctx.save();
6745
+ ctx.globalCompositeOperation = 'destination-out';
6746
+ ctx.beginPath();
6747
+ ctx.arc(center, center, radius * 1.35, 0, Math.PI * 2);
6748
+ ctx.fill();
6749
+ ctx.restore();
6750
+ ctx.beginPath();
6751
+ ctx.arc(center, center, radius, 0, Math.PI * 2);
6752
+ ctx.fillStyle = color;
6753
+ ctx.fill();
6754
+ };
6755
+ const drawProgress = (ctx, value, color, trackColor) => {
6756
+ const lineWidth = FAVICON_SIZE * 0.14;
6757
+ const radius = (FAVICON_SIZE - lineWidth) / 2;
6758
+ const center = FAVICON_SIZE / 2;
6759
+ const clamped = Math.min(100, Math.max(0, value));
6760
+ const start = -Math.PI / 2;
6761
+ ctx.lineWidth = lineWidth;
6762
+ ctx.lineCap = 'butt';
6763
+ // Clear the ring's lane so the base icon doesn't show through a semi-transparent track.
6764
+ ctx.save();
6765
+ ctx.globalCompositeOperation = 'destination-out';
6766
+ ctx.strokeStyle = '#000';
6767
+ ctx.beginPath();
6768
+ ctx.arc(center, center, radius, 0, Math.PI * 2);
6769
+ ctx.stroke();
6770
+ ctx.restore();
6771
+ ctx.strokeStyle = trackColor;
6772
+ ctx.beginPath();
6773
+ ctx.arc(center, center, radius, 0, Math.PI * 2);
6774
+ ctx.stroke();
6775
+ if (clamped > 0) {
6776
+ ctx.strokeStyle = color;
6777
+ ctx.beginPath();
6778
+ ctx.arc(center, center, radius, start, start + (Math.PI * 2 * clamped) / 100);
6779
+ ctx.stroke();
6780
+ }
6781
+ };
6782
+ const FAVICON_STORE_DEF = /* @__PURE__ */ defineRootProvider(() => {
6783
+ const document = inject(DOCUMENT);
6784
+ const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
6785
+ const overlays = signal(new Map(), /* @ts-ignore */
6786
+ ...(ngDevMode ? [{ debugName: "overlays" }] : /* istanbul ignore next */ []));
6787
+ const activeOverlay = computed(() => {
6788
+ const all = Array.from(overlays().values());
6789
+ return all.find((overlay) => overlay.kind === 'progress') ?? all[all.length - 1] ?? null;
6790
+ }, /* @ts-ignore */
6791
+ ...(ngDevMode ? [{ debugName: "activeOverlay" }] : /* istanbul ignore next */ []));
6792
+ let iconLink = null;
6793
+ let createdIconLink = false;
6794
+ let originalHref = null;
6795
+ let baseImage = null;
6796
+ let baseImageFailed = false;
6797
+ const resolveIconLink = () => {
6798
+ if (iconLink) {
6799
+ return iconLink;
6800
+ }
6801
+ iconLink = document.querySelector('link[rel~="icon"]');
6802
+ if (!iconLink) {
6803
+ iconLink = document.createElement('link');
6804
+ iconLink.rel = 'icon';
6805
+ document.head.appendChild(iconLink);
6806
+ createdIconLink = true;
6807
+ }
6808
+ originalHref = createdIconLink ? null : iconLink.getAttribute('href');
6809
+ return iconLink;
6810
+ };
6811
+ const restore = () => {
6812
+ if (!iconLink)
6813
+ return;
6814
+ if (createdIconLink) {
6815
+ iconLink.remove();
6816
+ iconLink = null;
6817
+ createdIconLink = false;
6818
+ return;
6819
+ }
6820
+ if (originalHref === null) {
6821
+ iconLink.removeAttribute('href');
6822
+ }
6823
+ else {
6824
+ iconLink.setAttribute('href', originalHref);
6825
+ }
6826
+ };
6827
+ // The base icon is loaded once and reused; a favicon that can't be read (cross-origin without
6828
+ // CORS headers, an SVG without intrinsic size) draws on an empty canvas instead of failing.
6829
+ const loadBaseImage = () => {
6830
+ if (baseImage || baseImageFailed || !originalHref) {
6831
+ return Promise.resolve(baseImage);
6832
+ }
6833
+ return new Promise((resolve) => {
6834
+ const image = new Image();
6835
+ image.onload = () => {
6836
+ baseImage = image;
6837
+ resolve(image);
6838
+ };
6839
+ image.onerror = () => {
6840
+ baseImageFailed = true;
6841
+ resolve(null);
6842
+ };
6843
+ image.src = originalHref;
6844
+ });
6845
+ };
6846
+ const render = async (overlay) => {
6847
+ const canvas = document.createElement('canvas');
6848
+ canvas.width = FAVICON_SIZE;
6849
+ canvas.height = FAVICON_SIZE;
6850
+ // No 2d context (a non-browser DOM implementation) - leave the favicon untouched entirely.
6851
+ const ctx = canvas.getContext('2d');
6852
+ if (!ctx)
6853
+ return;
6854
+ const link = resolveIconLink();
6855
+ const image = await loadBaseImage();
6856
+ // A later state may have won the race while the base image loaded.
6857
+ if (activeOverlay() !== overlay)
6858
+ return;
6859
+ if (image) {
6860
+ ctx.drawImage(image, 0, 0, FAVICON_SIZE, FAVICON_SIZE);
6861
+ }
6862
+ const color = overlay.color ?? readThemeColor(document);
6863
+ if (overlay.kind === 'dot') {
6864
+ drawDot(ctx, color);
6865
+ }
6866
+ else {
6867
+ drawProgress(ctx, overlay.value, color, overlay.trackColor ?? 'rgba(127, 127, 127, 0.35)');
6868
+ }
6869
+ try {
6870
+ link.setAttribute('href', canvas.toDataURL('image/png'));
6871
+ }
6872
+ catch {
6873
+ // A tainted canvas (cross-origin base icon) can't be exported - leave the favicon as it was.
6874
+ restore();
6875
+ }
6876
+ };
6877
+ if (isBrowser) {
6878
+ effect(() => {
6879
+ const overlay = activeOverlay();
6880
+ if (!overlay) {
6881
+ restore();
6882
+ return;
6883
+ }
6884
+ void render(overlay);
6885
+ });
6886
+ }
6887
+ const addOverlay = (id, overlay) => {
6888
+ overlays.update((current) => new Map(current).set(id, overlay));
6889
+ };
6890
+ const removeOverlay = (id) => {
6891
+ overlays.update((current) => {
6892
+ const next = new Map(current);
6893
+ next.delete(id);
6894
+ return next;
6895
+ });
6896
+ };
6897
+ return { activeOverlay, addOverlay, removeOverlay };
6898
+ }, { name: 'Favicon Store' });
6899
+ /**
6900
+ * Owns the `<link rel="icon">` href so overlays can be drawn onto the site's favicon and taken back
6901
+ * off again. The original href is captured on first use and restored once the last overlay is gone.
6902
+ *
6903
+ * Registrations are keyed, and a `progress` overlay wins over a `dot` - a ring that also carries a
6904
+ * badge reads as neither.
6905
+ */
6906
+ const provideFaviconStore = /* @__PURE__ */ toProvideFn(FAVICON_STORE_DEF);
6907
+ const injectFaviconStore = /* @__PURE__ */ toInjectFn(FAVICON_STORE_DEF);
6908
+ /**
6909
+ * Draw an overlay on the site's favicon while the binding has a value - an unsaved-changes dot, an
6910
+ * upload's progress ring. The favicon is restored when the binding goes empty or the injector is
6911
+ * destroyed. No-op during SSR.
6912
+ *
6913
+ * ```ts
6914
+ * applyFaviconOverlay(computed(() => (this.saving() ? { kind: 'progress', value: this.percent() } : null)));
6915
+ * ```
6916
+ */
6917
+ const applyFaviconOverlay = (binding) => {
6918
+ const faviconStore = injectFaviconStore();
6919
+ const overlayId = Symbol('favicon-overlay');
6920
+ applyHeadBinding(binding, (value) => faviconStore.addOverlay(overlayId, value), () => faviconStore.removeOverlay(overlayId));
6921
+ };
6922
+
6697
6923
  var jsonLd = /*#__PURE__*/Object.freeze({
6698
6924
  __proto__: null
6699
6925
  });
6700
6926
 
6701
- const DEFAULT_MULTI_INSTANCE_RELS = new Set([
6927
+ const DEFAULT_MULTI_INSTANCE_RELS = /* @__PURE__ */ new Set([
6702
6928
  'alternate',
6703
6929
  'icon',
6704
6930
  'apple-touch-icon',
@@ -6711,10 +6937,12 @@ const DEFAULT_MULTI_INSTANCE_RELS = new Set([
6711
6937
  'stylesheet',
6712
6938
  'modulepreload',
6713
6939
  ]);
6714
- const [provideLinkStoreConfig, injectLinkStoreConfig] = createStaticRootProvider({
6940
+ const LINK_STORE_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider({
6715
6941
  multiInstanceRels: DEFAULT_MULTI_INSTANCE_RELS,
6716
6942
  }, { name: 'Link Store Config' });
6717
- const [provideLinkStore, injectLinkStore] = createRootProvider(() => {
6943
+ const provideLinkStoreConfig = /* @__PURE__ */ toProvideFn(LINK_STORE_CONFIG_DEF);
6944
+ const injectLinkStoreConfig = /* @__PURE__ */ toInjectFn(LINK_STORE_CONFIG_DEF);
6945
+ const LINK_STORE_DEF = /* @__PURE__ */ defineRootProvider(() => {
6718
6946
  const document = inject(DOCUMENT$1);
6719
6947
  const config = injectLinkStoreConfig();
6720
6948
  const linksByKey = signal(new Map(), /* @ts-ignore */
@@ -6750,7 +6978,6 @@ const [provideLinkStore, injectLinkStore] = createRootProvider(() => {
6750
6978
  };
6751
6979
  const createLinkElement = (linkConfig) => {
6752
6980
  const link = document.createElement('link');
6753
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
6754
6981
  const { key, allowMultiple, ...attributes } = linkConfig;
6755
6982
  Object.entries(attributes).forEach(([attrKey, value]) => {
6756
6983
  if (value !== undefined && value !== null) {
@@ -6835,6 +7062,8 @@ const [provideLinkStore, injectLinkStore] = createRootProvider(() => {
6835
7062
  };
6836
7063
  return { addLink, removeLink, getLinkKey };
6837
7064
  }, { name: 'Link Store' });
7065
+ const provideLinkStore = /* @__PURE__ */ toProvideFn(LINK_STORE_DEF);
7066
+ const injectLinkStore = /* @__PURE__ */ toInjectFn(LINK_STORE_DEF);
6838
7067
  const applyLinkBinding = (binding) => {
6839
7068
  const linkStore = injectLinkStore();
6840
7069
  const linkId = Symbol('link-tag');
@@ -6844,7 +7073,7 @@ const applyLinkBinding = (binding) => {
6844
7073
  linkStore.addLink(linkId, config);
6845
7074
  }, () => linkStore.removeLink(linkId, currentKey));
6846
7075
  };
6847
- const applyCanonicalBinding = createPropertyBinding((href) => ({ rel: 'canonical', href }), applyLinkBinding);
7076
+ const applyCanonicalBinding = /* @__PURE__ */ createPropertyBinding((href) => ({ rel: 'canonical', href }), applyLinkBinding);
6848
7077
  const applyAlternateBinding = (hreflang, binding) => {
6849
7078
  applyLinkBinding(computed(() => {
6850
7079
  const url = untracked(() => (isSignal(binding) ? binding() : binding));
@@ -6856,8 +7085,8 @@ const applyAlternateLanguagesBindings = (config) => {
6856
7085
  applyAlternateBinding(hreflang, binding);
6857
7086
  });
6858
7087
  };
6859
- const applyPrevBinding = createPropertyBinding((href) => ({ rel: 'prev', href }), applyLinkBinding);
6860
- const applyNextBinding = createPropertyBinding((href) => ({ rel: 'next', href }), applyLinkBinding);
7088
+ const applyPrevBinding = /* @__PURE__ */ createPropertyBinding((href) => ({ rel: 'prev', href }), applyLinkBinding);
7089
+ const applyNextBinding = /* @__PURE__ */ createPropertyBinding((href) => ({ rel: 'next', href }), applyLinkBinding);
6861
7090
  const applyResourceHintsBindings = (config) => {
6862
7091
  if (config.preconnect) {
6863
7092
  createArrayPropertyBinding((href, index) => ({ rel: 'preconnect', href, key: `preconnect:${index}` }), applyLinkBinding)(config.preconnect);
@@ -6873,20 +7102,26 @@ const applyResourceHintsBindings = (config) => {
6873
7102
  }
6874
7103
  };
6875
7104
 
6876
- const [provideTitleConfig, injectTitleConfig] = createStaticRootProvider({
7105
+ const TITLE_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider({
6877
7106
  divider: '|',
6878
7107
  defaultTitle: '',
6879
7108
  transformer: (title) => title,
6880
7109
  }, { name: 'Title Config' });
6881
- const [provideTitleStore, injectTitleStore] = createRootProvider(() => {
7110
+ const provideTitleConfig = /* @__PURE__ */ toProvideFn(TITLE_CONFIG_DEF);
7111
+ const injectTitleConfig = /* @__PURE__ */ toInjectFn(TITLE_CONFIG_DEF);
7112
+ const TITLE_STORE_DEF = /* @__PURE__ */ defineRootProvider(() => {
6882
7113
  const config = injectTitleConfig();
6883
7114
  const titleParts = signal(new Map(), /* @ts-ignore */
6884
7115
  ...(ngDevMode ? [{ debugName: "titleParts" }] : /* istanbul ignore next */ []));
7116
+ const titleMarkers = signal(new Map(), /* @ts-ignore */
7117
+ ...(ngDevMode ? [{ debugName: "titleMarkers" }] : /* istanbul ignore next */ []));
6885
7118
  const { currentLocale } = injectLocale();
6886
7119
  const titleService = inject(Title);
6887
7120
  const isRouterInitialized = injectIsRouterInitialized();
6888
- const defaultTitle = config.defaultTitle ?? titleService.getTitle();
6889
- const title = computed(() => {
7121
+ // Falls back on an empty configured default too, so a store created lazily (e.g. by a title
7122
+ // marker) doesn't wipe a title that came from `index.html`.
7123
+ const defaultTitle = config.defaultTitle || titleService.getTitle();
7124
+ const composedTitle = computed(() => {
6890
7125
  const partsMap = titleParts();
6891
7126
  if (!partsMap.size || !isRouterInitialized()) {
6892
7127
  return config.transformer(defaultTitle, currentLocale());
@@ -6910,6 +7145,20 @@ const [provideTitleStore, injectTitleStore] = createRootProvider(() => {
6910
7145
  }
6911
7146
  return finalTextParts.join(` ${config.divider} `);
6912
7147
  }, /* @ts-ignore */
7148
+ ...(ngDevMode ? [{ debugName: "composedTitle" }] : /* istanbul ignore next */ []));
7149
+ /**
7150
+ * The composed title with any markers prefixed - what actually lands in `document.title`.
7151
+ * Markers sit outside the divider logic, so `● Page | App` rather than `● | Page | App`.
7152
+ */
7153
+ const title = computed(() => {
7154
+ // Deduplicated: two forms with unsaved changes should read as one marker, not two.
7155
+ const markers = Array.from(new Set(titleMarkers().values()));
7156
+ const composed = composedTitle();
7157
+ if (!markers.length) {
7158
+ return composed;
7159
+ }
7160
+ return `${markers.join(' ')} ${composed}`.trim();
7161
+ }, /* @ts-ignore */
6913
7162
  ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
6914
7163
  const addPart = (id, part) => {
6915
7164
  titleParts.update((parts) => new Map(parts).set(id, part));
@@ -6921,19 +7170,41 @@ const [provideTitleStore, injectTitleStore] = createRootProvider(() => {
6921
7170
  return newParts;
6922
7171
  });
6923
7172
  };
7173
+ const addMarker = (id, marker) => {
7174
+ titleMarkers.update((markers) => new Map(markers).set(id, marker));
7175
+ };
7176
+ const removeMarker = (id) => {
7177
+ titleMarkers.update((markers) => {
7178
+ const newMarkers = new Map(markers);
7179
+ newMarkers.delete(id);
7180
+ return newMarkers;
7181
+ });
7182
+ };
6924
7183
  effect(() => {
6925
7184
  const titleText = title();
6926
7185
  titleService.setTitle(titleText);
6927
7186
  });
6928
- return { title, addPart, removePart };
7187
+ return { title, addPart, removePart, addMarker, removeMarker };
6929
7188
  }, { name: 'Title Store' });
7189
+ const provideTitleStore = /* @__PURE__ */ toProvideFn(TITLE_STORE_DEF);
7190
+ const injectTitleStore = /* @__PURE__ */ toInjectFn(TITLE_STORE_DEF);
6930
7191
  const applyHeadTitleBinding = (binding, options) => {
6931
7192
  const titleStore = injectTitleStore();
6932
7193
  const partId = Symbol('title-part');
6933
7194
  applyHeadBinding(binding, (value) => titleStore.addPart(partId, { text: `${value}`, ...options }), () => titleStore.removePart(partId), (value) => value !== null && value !== undefined && value !== '');
6934
7195
  };
7196
+ /**
7197
+ * Prefix the tab title with a short marker (`● Page | App`) while the binding has a value - the
7198
+ * unsaved-changes dot, a pending-count badge, and the like. The marker is removed when the binding
7199
+ * goes empty and on destroy. Unlike a title part, it is not joined by the divider.
7200
+ */
7201
+ const applyHeadTitleMarker = (binding) => {
7202
+ const titleStore = injectTitleStore();
7203
+ const markerId = Symbol('title-marker');
7204
+ applyHeadBinding(binding, (value) => titleStore.addMarker(markerId, value), () => titleStore.removeMarker(markerId), (value) => value !== null && value !== undefined && value !== '');
7205
+ };
6935
7206
 
6936
- const DEFAULT_MULTI_INSTANCE_TAGS = new Set([
7207
+ const DEFAULT_MULTI_INSTANCE_TAGS = /* @__PURE__ */ new Set([
6937
7208
  // Open Graph - Media
6938
7209
  'property="og:image"',
6939
7210
  'property="og:image:url"',
@@ -7002,11 +7273,13 @@ const DEFAULT_MULTI_INSTANCE_TAGS = new Set([
7002
7273
  'name="p:domain_verify"', // Pinterest
7003
7274
  'name="facebook-domain-verification"',
7004
7275
  ]);
7005
- const [provideMetaConfig, injectMetaConfig] = createStaticRootProvider({
7276
+ const META_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider({
7006
7277
  transformer: (content) => content,
7007
7278
  multiInstanceTags: DEFAULT_MULTI_INSTANCE_TAGS,
7008
7279
  }, { name: 'Meta Config' });
7009
- const [provideMetaStore, injectMetaStore] = createRootProvider(() => {
7280
+ const provideMetaConfig = /* @__PURE__ */ toProvideFn(META_CONFIG_DEF);
7281
+ const injectMetaConfig = /* @__PURE__ */ toInjectFn(META_CONFIG_DEF);
7282
+ const META_STORE_DEF = /* @__PURE__ */ defineRootProvider(() => {
7010
7283
  const metaService = inject(Meta);
7011
7284
  const config = injectMetaConfig();
7012
7285
  const { currentLocale } = injectLocale();
@@ -7032,7 +7305,6 @@ const [provideMetaStore, injectMetaStore] = createRootProvider(() => {
7032
7305
  if (transformedConfig.content) {
7033
7306
  transformedConfig.content = config.transformer(transformedConfig.content, locale);
7034
7307
  }
7035
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
7036
7308
  const { key, allowMultiple, ...metaConfig } = transformedConfig;
7037
7309
  return metaService.addTag(metaConfig);
7038
7310
  };
@@ -7119,6 +7391,8 @@ const [provideMetaStore, injectMetaStore] = createRootProvider(() => {
7119
7391
  });
7120
7392
  return { addTag, removeTag, getSelector };
7121
7393
  }, { name: 'Meta Store' });
7394
+ const provideMetaStore = /* @__PURE__ */ toProvideFn(META_STORE_DEF);
7395
+ const injectMetaStore = /* @__PURE__ */ toInjectFn(META_STORE_DEF);
7122
7396
  const applyMetaBinding = (binding) => {
7123
7397
  const metaStore = injectMetaStore();
7124
7398
  const tagId = Symbol('meta-tag');
@@ -7128,14 +7402,14 @@ const applyMetaBinding = (binding) => {
7128
7402
  metaStore.addTag(tagId, config);
7129
7403
  }, () => metaStore.removeTag(tagId, currentSelector));
7130
7404
  };
7131
- const applyDescriptionBinding = createPropertyBinding((content) => ({ name: 'description', content }), applyMetaBinding);
7405
+ const applyDescriptionBinding = /* @__PURE__ */ createPropertyBinding((content) => ({ name: 'description', content }), applyMetaBinding);
7132
7406
  const applyKeywordsBinding = (binding) => {
7133
7407
  applyMetaBinding(computed(() => {
7134
7408
  const keywords = untracked(() => (isSignal(binding) ? binding() : binding));
7135
7409
  return keywords && keywords.length > 0 ? { name: 'keywords', content: keywords.join(', ') } : null;
7136
7410
  }));
7137
7411
  };
7138
- const applyAuthorBinding = createPropertyBinding((content) => ({ name: 'author', content }), applyMetaBinding);
7412
+ const applyAuthorBinding = /* @__PURE__ */ createPropertyBinding((content) => ({ name: 'author', content }), applyMetaBinding);
7139
7413
  const applyRobotsBinding = (binding) => {
7140
7414
  applyMetaBinding(computed(() => {
7141
7415
  const config = untracked(() => (isSignal(binding) ? binding() : binding));
@@ -7448,10 +7722,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7448
7722
  }]
7449
7723
  }] });
7450
7724
 
7451
- const [provideStructuredDataConfig, injectStructuredDataConfig] = createStaticRootProvider({
7725
+ const STRUCTURED_DATA_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider({
7452
7726
  placement: 'body',
7453
7727
  }, { name: 'Structured Data Config' });
7454
- const [provideStructuredDataStore, injectStructuredDataStore] = createRootProvider(() => {
7728
+ const provideStructuredDataConfig = /* @__PURE__ */ toProvideFn(STRUCTURED_DATA_CONFIG_DEF);
7729
+ const injectStructuredDataConfig = /* @__PURE__ */ toInjectFn(STRUCTURED_DATA_CONFIG_DEF);
7730
+ const STRUCTURED_DATA_STORE_DEF = /* @__PURE__ */ defineRootProvider(() => {
7455
7731
  const document = inject(DOCUMENT$1);
7456
7732
  const renderer = injectRenderer();
7457
7733
  const config = injectStructuredDataConfig();
@@ -7468,7 +7744,11 @@ const [provideStructuredDataStore, injectStructuredDataStore] = createRootProvid
7468
7744
  renderer.removeChild(oldScript.parentNode, oldScript);
7469
7745
  }
7470
7746
  const script = renderer.createElement('script');
7471
- renderer.setAttributes(script, { type: 'application/ld+json', text: JSON.stringify(data) });
7747
+ renderer.setAttributes(script, { type: 'application/ld+json' });
7748
+ // The JSON is the script's *content*, not an attribute. Written as text rather than markup so a
7749
+ // `</script>` inside a string can't close the tag early - the escaping problem
7750
+ // `StructuredDataComponent` has to solve by hand because it goes through `innerHTML`.
7751
+ renderer.setTextContent(script, JSON.stringify(data));
7472
7752
  const target = getTargetElement();
7473
7753
  renderer.appendChild(target, script);
7474
7754
  newScripts.set(id, script);
@@ -7488,6 +7768,8 @@ const [provideStructuredDataStore, injectStructuredDataStore] = createRootProvid
7488
7768
  };
7489
7769
  return { addStructuredData, removeStructuredData };
7490
7770
  }, { name: 'Structured Data Store' });
7771
+ const provideStructuredDataStore = /* @__PURE__ */ toProvideFn(STRUCTURED_DATA_STORE_DEF);
7772
+ const injectStructuredDataStore = /* @__PURE__ */ toInjectFn(STRUCTURED_DATA_STORE_DEF);
7491
7773
  const applyStructuredDataBinding = (binding) => {
7492
7774
  const store = injectStructuredDataStore();
7493
7775
  const scriptId = Symbol('structured-data');
@@ -7537,12 +7819,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7537
7819
 
7538
7820
  /**
7539
7821
  * Cascades hover/active-driven color DOWN to descendants explicitly marked with
7540
- * `ColorInteractiveDirective` — this element does not color itself. Use it when a wrapper should
7822
+ * `ColorInteractiveDirective` - this element does not color itself. Use it when a wrapper should
7541
7823
  * expand a nested control's hit/visual area (e.g. clicking a checkbox's label should light up the
7542
- * checkbox itself), never to give a wrapper its own interactive coloring — that's
7824
+ * checkbox itself), never to give a wrapper its own interactive coloring - that's
7543
7825
  * `ColorInteractiveDirective`.
7544
7826
  *
7545
- * Does nothing unless at least one descendant carries `ColorInteractiveDirective` — check for
7827
+ * Does nothing unless at least one descendant carries `ColorInteractiveDirective` - check for
7546
7828
  * that before adding this. It was mistakenly added to a component with no such descendant once
7547
7829
  * already, where it did nothing but was confusing to read.
7548
7830
  */
@@ -7568,9 +7850,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7568
7850
  * Marks a descendant as NOT counting toward an ancestor's `ColorInteractiveDirective`
7569
7851
  * `:hover`/`:active` reaction (via the `:has()` guard in color-interactive-styles.component.css).
7570
7852
  * Use it for content that's incidentally inside an interactive ancestor but isn't part of what
7571
- * makes that ancestor interactive — e.g. a hint/error block sitting inside a form field's frame.
7853
+ * makes that ancestor interactive - e.g. a hint/error block sitting inside a form field's frame.
7572
7854
  *
7573
- * This only suppresses the ancestor's OWN reaction to hovering/pressing the excluded element — it
7855
+ * This only suppresses the ancestor's OWN reaction to hovering/pressing the excluded element - it
7574
7856
  * has no effect on custom-property inheritance into descendants past the excluded element. If
7575
7857
  * what you actually need is "stop my own cosmetic styling from reacting to a parent's unrelated
7576
7858
  * interaction," this is the wrong tool; that's the class of bug `ColorInteractiveHasFocusDirective`
@@ -7592,7 +7874,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7592
7874
 
7593
7875
  /**
7594
7876
  * Tints `--et-theme-color-primary-*` etc. only when a descendant genuinely has `:focus-visible`
7595
- * (`:has(:focus-visible)`) — never on mouse `:hover`/`:active`. Use this for a wrapper (like a
7877
+ * (`:has(:focus-visible)`) - never on mouse `:hover`/`:active`. Use this for a wrapper (like a
7596
7878
  * form field's frame) that should show the theme color while its control is focused, but must
7597
7879
  * NOT react to incidental mouse interaction with unrelated nested widgets (a toolbar button, a
7598
7880
  * rendered link) that happen to live inside the same wrapper.
@@ -7604,7 +7886,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7604
7886
  * Deliberately does NOT also carry `ColorInteractiveDirective`'s `et-color-interactive` class:
7605
7887
  * that class's plain `:hover`/`:active` rules locally override the same (inheriting)
7606
7888
  * `--et-theme-color-primary-rgb`, so anything bubbling up from ANYWHERE in the subtree would
7607
- * leak the transient hover/press color into every descendant reading it for unrelated cosmetics —
7889
+ * leak the transient hover/press color into every descendant reading it for unrelated cosmetics -
7608
7890
  * exactly the bug this directive's only consumer (form-field) shipped once, before the two
7609
7891
  * behaviors were split apart. If you need the plain hover/active reaction too, add
7610
7892
  * `ColorInteractiveDirective` explicitly and make sure you actually want both.
@@ -7630,13 +7912,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7630
7912
  /**
7631
7913
  * Makes an element react to ITS OWN `:hover`/`:active`/`:focus-visible`/`[disabled]` by tinting
7632
7914
  * `--et-theme-color-primary-*`/`-on-primary-*`/`-ink-*` with the current color theme. Use this on
7633
- * a single, self-contained interactive element — a button, a checkbox, a segmented button.
7915
+ * a single, self-contained interactive element - a button, a checkbox, a segmented button.
7634
7916
  *
7635
7917
  * Do NOT apply this to a wrapper that also contains OTHER, independently-interactive elements
7636
7918
  * (e.g. a toolbar with its own buttons, or a form-field's control slot). These are ordinary
7637
7919
  * inheriting CSS custom properties, so a descendant reading them for its own cosmetics (a caret
7638
7920
  * color, a link color) gets swept along whenever ANYTHING inside the wrapper is hovered or
7639
- * clicked — not just when the wrapper itself is the thing being interacted with. That exact bug
7921
+ * clicked - not just when the wrapper itself is the thing being interacted with. That exact bug
7640
7922
  * shipped once already; see ColorInteractiveContainerDirective/ColorInteractiveHasFocusDirective
7641
7923
  * for the shapes that avoid it.
7642
7924
  */
@@ -7664,19 +7946,23 @@ const SURFACE_TYPE = {
7664
7946
  };
7665
7947
  const createCssSurfaceName = (name) => name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`);
7666
7948
  const resolveSurfaceByElevation = (themes, type, elevation) => themes.find((t) => t.type === type && t.elevation === elevation) ?? null;
7667
- const [ɵProvideSurfaceThemes, injectSurfaceThemes] = createStaticProvider(undefined, {
7949
+ const SURFACE_THEMES_DEF = /* @__PURE__ */ defineStaticProvider(undefined, {
7668
7950
  name: 'Surface Themes',
7669
7951
  });
7670
- const [ɵProvideSurfaceThemesPrefix, injectSurfaceThemesPrefix] = createStaticProvider('et', {
7952
+ const ɵProvideSurfaceThemes = /* @__PURE__ */ toProvideFn(SURFACE_THEMES_DEF);
7953
+ const injectSurfaceThemes = /* @__PURE__ */ toInjectFn(SURFACE_THEMES_DEF);
7954
+ const SURFACE_THEMES_PREFIX_DEF = /* @__PURE__ */ defineStaticProvider('et', {
7671
7955
  name: 'Surface Themes Prefix',
7672
7956
  });
7957
+ const ɵProvideSurfaceThemesPrefix = /* @__PURE__ */ toProvideFn(SURFACE_THEMES_PREFIX_DEF);
7958
+ const injectSurfaceThemesPrefix = /* @__PURE__ */ toInjectFn(SURFACE_THEMES_PREFIX_DEF);
7673
7959
  const provideSurfaceThemesWithTailwind4 = (themes, prefix = 'et') => [
7674
7960
  ɵProvideSurfaceThemes(themes),
7675
7961
  ɵProvideSurfaceThemesPrefix(prefix),
7676
7962
  ];
7677
7963
 
7678
7964
  const SURFACE_PROVIDER = new InjectionToken('SurfaceProvider');
7679
- const FORCED_SURFACE_UNSET = Symbol('FORCED_SURFACE_UNSET');
7965
+ const FORCED_SURFACE_UNSET = /* @__PURE__ */ Symbol('FORCED_SURFACE_UNSET');
7680
7966
  class ProvideSurfaceDirective {
7681
7967
  constructor() {
7682
7968
  this.themes = injectSurfaceThemes({ optional: true });
@@ -7775,7 +8061,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7775
8061
  }], propDecorators: { surface: [{ type: i0.Input, args: [{ isSignal: true, alias: "etProvideSurface", required: false }] }] } });
7776
8062
 
7777
8063
  let uniqueId = 0;
7778
- const [provideSurfaceContextTracker, injectSurfaceContextTracker] = createRootProvider(() => {
8064
+ const SURFACE_CONTEXT_TRACKER_DEF = /* @__PURE__ */ defineRootProvider(() => {
7779
8065
  const entries = signal([], /* @ts-ignore */
7780
8066
  ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
7781
8067
  const topEntry = computed(() => {
@@ -7808,12 +8094,14 @@ const [provideSurfaceContextTracker, injectSurfaceContextTracker] = createRootPr
7808
8094
  };
7809
8095
  return { topType, topElevation, register, surfaceForElement };
7810
8096
  }, { name: 'SurfaceContextTracker' });
8097
+ const provideSurfaceContextTracker = /* @__PURE__ */ toProvideFn(SURFACE_CONTEXT_TRACKER_DEF);
8098
+ const injectSurfaceContextTracker = /* @__PURE__ */ toInjectFn(SURFACE_CONTEXT_TRACKER_DEF);
7811
8099
 
7812
8100
  class AutoSurfaceDirective {
7813
8101
  /**
7814
8102
  * Mark this auto-surface as its overlay's *own painted surface*. An overlay *panel*
7815
8103
  * (menu, select/date/cascader panel, tooltip, toggletip, rich-text-editor popups) is
7816
- * the overlay's surface, so it must paint the overlay's registered elevation exactly —
8104
+ * the overlay's surface, so it must paint the overlay's registered elevation exactly -
7817
8105
  * not stack a level above a parent surface. The elevation is read from the surface-context
7818
8106
  * tracker (the pane it renders into), which is authoritative across the portal boundary;
7819
8107
  * the panel's own declaration injector points back at the trigger location and cannot be
@@ -7841,7 +8129,7 @@ class AutoSurfaceDirective {
7841
8129
  ...(ngDevMode ? [{ debugName: "isOverlaySurface" }] : /* istanbul ignore next */ []));
7842
8130
  // Bumped by the afterEveryRender watcher in the constructor whenever this element's overlay
7843
8131
  // containment changes. surfaceForElement() below does a non-reactive element.contains() read,
7844
- // so the computed must be re-triggered when this element is (re)grafted into an overlay pane —
8132
+ // so the computed must be re-triggered when this element is (re)grafted into an overlay pane -
7845
8133
  // otherwise a value cached from before the graft (one elevation too low) sticks forever.
7846
8134
  this.domSettleTick = signal(0, /* @ts-ignore */
7847
8135
  ...(ngDevMode ? [{ debugName: "domSettleTick" }] : /* istanbul ignore next */ []));
@@ -7857,7 +8145,7 @@ class AutoSurfaceDirective {
7857
8145
  const contextElevation = contextProvider?.elevation() ?? null;
7858
8146
  const contextType = contextProvider?.surfaceType() ?? null;
7859
8147
  // An overlay's projected/portaled content keeps the injector of where it was
7860
- // *declared* (the trigger location), not the pane it renders into — so the
8148
+ // *declared* (the trigger location), not the pane it renders into - so the
7861
8149
  // injector-derived parent surface is one elevation too low. The surface-context
7862
8150
  // tracker records each open overlay's surface *and* its pane element, so we consult
7863
8151
  // only the overlay whose pane actually contains this element in the DOM (portaling
@@ -7868,7 +8156,7 @@ class AutoSurfaceDirective {
7868
8156
  const overlayType = overlaySurface?.type ?? null;
7869
8157
  // A panel that IS its overlay's own surface (see matchOverlaySurface) must paint exactly the
7870
8158
  // overlay's elevation: the overlay container already resolved it (one above the trigger) and
7871
- // registered it in the tracker. It must NOT re-derive from its declaration injector — that
8159
+ // registered it in the tracker. It must NOT re-derive from its declaration injector - that
7872
8160
  // points back at the trigger location and disagrees with the pane whenever the trigger itself
7873
8161
  // sits on a surface, which would double-elevate the panel relative to its own pane.
7874
8162
  if (this.isOverlaySurface()) {
@@ -7901,7 +8189,7 @@ class AutoSurfaceDirective {
7901
8189
  ...(ngDevMode ? [{ debugName: "resolvedSurface" }] : /* istanbul ignore next */ []));
7902
8190
  // Projected/portaled auto-surface content (e.g. a select option's avatar) can have its
7903
8191
  // resolvedSurface computed run *before* it is grafted into the overlay pane it visually lives
7904
- // in — or while it is briefly mounted in an off-pane measuring container, as windowed lists do.
8192
+ // in - or while it is briefly mounted in an off-pane measuring container, as windowed lists do.
7905
8193
  // surfaceForElement()'s element.contains() check then returns the wrong (or no) overlay, so it
7906
8194
  // resolves off the declaration injector (the trigger location) one elevation too low. That check
7907
8195
  // is a plain DOM read, not a signal, so nothing re-runs the computed once the element reaches its
@@ -7918,14 +8206,13 @@ class AutoSurfaceDirective {
7918
8206
  this.domSettleTick.update((v) => v + 1);
7919
8207
  }
7920
8208
  else if (el.isConnected && ++stableRenders >= 2) {
7921
- // containment has held for a couple of renders and the element is in the DOM — done moving
8209
+ // containment has held for a couple of renders and the element is in the DOM - done moving
7922
8210
  settleWatcher.destroy();
7923
8211
  }
7924
8212
  });
7925
8213
  effect(() => {
7926
8214
  const surface = this.resolvedSurface();
7927
8215
  untracked(() => {
7928
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7929
8216
  setInputSignal(this.ownSurfaceProvider.surface, surface);
7930
8217
  });
7931
8218
  });
@@ -7942,12 +8229,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7942
8229
  }], ctorParameters: () => [], propDecorators: { surfaceProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "surfaceProvider", required: false }] }] } });
7943
8230
 
7944
8231
  const createCssColorThemeName = (name) => name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`);
7945
- const [ɵProvideColorThemes, injectColorThemes] = createStaticProvider(undefined, {
8232
+ const COLOR_THEMES_DEF = /* @__PURE__ */ defineStaticProvider(undefined, {
7946
8233
  name: 'Color Themes',
7947
8234
  });
7948
- const [ɵProvideColorThemesPrefix, injectColorThemesPrefix] = createStaticProvider('et', {
8235
+ const ɵProvideColorThemes = /* @__PURE__ */ toProvideFn(COLOR_THEMES_DEF);
8236
+ const injectColorThemes = /* @__PURE__ */ toInjectFn(COLOR_THEMES_DEF);
8237
+ const COLOR_THEMES_PREFIX_DEF = /* @__PURE__ */ defineStaticProvider('et', {
7949
8238
  name: 'Themes Prefix',
7950
8239
  });
8240
+ const ɵProvideColorThemesPrefix = /* @__PURE__ */ toProvideFn(COLOR_THEMES_PREFIX_DEF);
8241
+ const injectColorThemesPrefix = /* @__PURE__ */ toInjectFn(COLOR_THEMES_PREFIX_DEF);
7951
8242
  const provideColorThemesWithTailwind4 = (themes, prefix = 'et') => [
7952
8243
  ɵProvideColorThemes(themes),
7953
8244
  ɵProvideColorThemesPrefix(prefix),
@@ -8166,7 +8457,7 @@ const COLOR_PROVIDER = new InjectionToken('ColorProvider');
8166
8457
  * The color provider attached to a bootstrapped root component (e.g. `ProvideColorDirective` in
8167
8458
  * the app component's `hostDirectives`), if any. Detached DOM such as overlay panes can't reach
8168
8459
  * it through element DI, so overlay containers fall back to this when resolving the provider to
8169
- * `syncWithProvider()` — resolve at sync time, not at injection time, since root components only
8460
+ * `syncWithProvider()` - resolve at sync time, not at injection time, since root components only
8170
8461
  * register with `ApplicationRef` once bootstrap completes.
8171
8462
  *
8172
8463
  * @internal
@@ -8180,7 +8471,7 @@ const resolveAppRootColorProvider = (appRef) => {
8180
8471
  }
8181
8472
  return null;
8182
8473
  };
8183
- const FORCED_COLOR_UNSET = Symbol('FORCED_COLOR_UNSET');
8474
+ const FORCED_COLOR_UNSET = /* @__PURE__ */ Symbol('FORCED_COLOR_UNSET');
8184
8475
  class ProvideColorDirective {
8185
8476
  constructor() {
8186
8477
  this.themes = injectColorThemes({ optional: true });
@@ -8200,8 +8491,8 @@ class ProvideColorDirective {
8200
8491
  }, /* @ts-ignore */
8201
8492
  ...(ngDevMode ? [{ debugName: "effectiveColor" }] : /* istanbul ignore next */ []));
8202
8493
  /**
8203
- * The color that actually applies at this provider's location: its own effective color, or —
8204
- * when this provider is passive (no input, nothing forced) — the nearest ancestor provider's.
8494
+ * The color that actually applies at this provider's location: its own effective color, or -
8495
+ * when this provider is passive (no input, nothing forced) - the nearest ancestor provider's.
8205
8496
  * Mirrors the CSS cascade, which falls through a provider's `-color--inherited` scope class;
8206
8497
  * consumers that re-apply context across detached DOM (overlay panes) must use this instead
8207
8498
  * of `effectiveColor` or a passive in-between provider (e.g. a form field's) erases the theme.
@@ -8294,14 +8585,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
8294
8585
  }] });
8295
8586
 
8296
8587
  /**
8297
- * Same shape as `ColorInteractiveDirective` — reacts to its own `:hover`/`:focus-visible`/
8298
- * `:active`/`[disabled]` — but drives `--et-surface-interaction-*`, a neutral/muted tint, instead
8588
+ * Same shape as `ColorInteractiveDirective` - reacts to its own `:hover`/`:focus-visible`/
8589
+ * `:active`/`[disabled]` - but drives `--et-surface-interaction-*`, a neutral/muted tint, instead
8299
8590
  * of the ambient color theme. Use it for chrome-level interaction feedback that shouldn't borrow
8300
8591
  * the page's brand color (a tab trigger's hover state, a toggle button that should look neutral
8301
- * until it's actually active — see `ButtonDirective`'s `mutedUntilPressed`).
8592
+ * until it's actually active - see `ButtonDirective`'s `mutedUntilPressed`).
8302
8593
  *
8303
8594
  * Has no container/exclude/has-focus variants: it has no descendant-cascading CSS at all, so
8304
- * applying it to a wrapper with independently-interactive children is safe by construction —
8595
+ * applying it to a wrapper with independently-interactive children is safe by construction -
8305
8596
  * unlike `ColorInteractiveDirective`, there's no bubbling-leak failure mode to worry about here.
8306
8597
  */
8307
8598
  class SurfaceInteractiveDirective {
@@ -8322,6 +8613,81 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
8322
8613
  }]
8323
8614
  }], ctorParameters: () => [] });
8324
8615
 
8616
+ const UNSAVED_CHANGES_COORDINATOR_DEF = /* @__PURE__ */ defineRootProvider(() => {
8617
+ const registrations = new Set();
8618
+ const _isCheckPending = signal(false, /* @ts-ignore */
8619
+ ...(ngDevMode ? [{ debugName: "_isCheckPending" }] : /* istanbul ignore next */ []));
8620
+ let pending = null;
8621
+ /** @internal Runs a tracker's confirm under the single-flight + abandon regime. */
8622
+ const runCheck = (runner) => {
8623
+ // A confirm is already on screen - adopt its answer rather than opening a second dialog.
8624
+ if (pending) {
8625
+ return pending.promise;
8626
+ }
8627
+ const controller = new AbortController();
8628
+ let settle;
8629
+ const abandoned = new Promise((resolve) => (settle = resolve));
8630
+ // Run the confirm synchronously - a dialog that opens a microtask later can miss a `signal` that
8631
+ // already aborted, and callers reasonably expect the dialog to be up when `runCheck()` returns.
8632
+ let confirmed;
8633
+ try {
8634
+ confirmed = runner({ signal: controller.signal }).catch(() => false);
8635
+ }
8636
+ catch {
8637
+ confirmed = Promise.resolve(false);
8638
+ }
8639
+ // Whichever settles first wins: a late answer from an abandoned confirm is ignored.
8640
+ const promise = Promise.race([confirmed, abandoned]).then((result) => {
8641
+ pending = null;
8642
+ _isCheckPending.set(false);
8643
+ return result;
8644
+ });
8645
+ pending = { promise, controller, settle };
8646
+ _isCheckPending.set(true);
8647
+ return promise;
8648
+ };
8649
+ /** @internal */
8650
+ const register = (registration) => {
8651
+ registrations.add(registration);
8652
+ return () => registrations.delete(registration);
8653
+ };
8654
+ const abandonAll = (reason = 'session-end') => {
8655
+ if (pending) {
8656
+ pending.controller.abort(reason);
8657
+ // The changes are unrecoverable at this point, so the discard is allowed: whatever waited on
8658
+ // this check (an overlay close, a `canDeactivate`) proceeds instead of hanging.
8659
+ pending.settle(true);
8660
+ }
8661
+ for (const registration of Array.from(registrations)) {
8662
+ registration.abandon(reason);
8663
+ }
8664
+ };
8665
+ return {
8666
+ /** Whether a confirm is currently on screen. */
8667
+ isCheckPending: _isCheckPending.asReadonly(),
8668
+ abandonAll,
8669
+ runCheck,
8670
+ register,
8671
+ };
8672
+ }, { name: 'Unsaved Changes Coordinator' });
8673
+ /**
8674
+ * App-wide coordination for the unsaved-changes family. Two jobs:
8675
+ *
8676
+ * - **One confirm at a time.** A page form, an overlay form and a route guard can all want a decision
8677
+ * at once; stacking three "discard your changes?" dialogs is never the right answer. A check that
8678
+ * starts while another is pending adopts the pending decision instead of asking again.
8679
+ * - **Abandoning guards when the session ends.** `abandonAll()` resolves the pending confirm, tells
8680
+ * its dialog to close (via {@link UnsavedChangesConfirmContext.signal}), and switches every live
8681
+ * guard off: further checks pass, and the tab locks release. Called automatically by
8682
+ * `@ethlete/query`'s auth provider on logout - the edits cannot be saved anymore, so guarding them
8683
+ * only strands dialogs over the login page and blocks the tab.
8684
+ *
8685
+ * Root-provided; every tracker registers itself. Inject it to abandon guards from app code (a session
8686
+ * timeout of your own, a hard reset) or to read whether a confirm is currently on screen.
8687
+ */
8688
+ const provideUnsavedChangesCoordinator = /* @__PURE__ */ toProvideFn(UNSAVED_CHANGES_COORDINATOR_DEF);
8689
+ const injectUnsavedChangesCoordinator = /* @__PURE__ */ toInjectFn(UNSAVED_CHANGES_COORDINATOR_DEF);
8690
+
8325
8691
  const isAbstractControl = (value) => !!value &&
8326
8692
  typeof value === 'object' &&
8327
8693
  typeof value.getRawValue === 'function' &&
@@ -8337,13 +8703,13 @@ const normalizeUnsavedChangesSource = (source) => {
8337
8703
  if (isFieldTree(source)) {
8338
8704
  return {
8339
8705
  value: computed(() => source().value()),
8340
- // FieldTree's value type is a generic conditional that TS can't narrow here — cast the writer.
8706
+ // FieldTree's value type is a generic conditional that TS can't narrow here - cast the writer.
8341
8707
  setValue: (next) => source().value.set(next),
8342
8708
  };
8343
8709
  }
8344
8710
  if (isSignal(source)) {
8345
8711
  const peek = untracked(source);
8346
- // A signal that currently holds a FieldTree — or nothing yet (the late/async form case).
8712
+ // A signal that currently holds a FieldTree - or nothing yet (the late/async form case).
8347
8713
  if (peek === null || peek === undefined || isFieldTree(peek)) {
8348
8714
  const fieldSignal = source;
8349
8715
  return {
@@ -8368,6 +8734,179 @@ const normalizeUnsavedChangesSource = (source) => {
8368
8734
  'an AbstractControl, or a WritableSignal.');
8369
8735
  };
8370
8736
 
8737
+ /** The default tab title marker - a filled dot, prefixed while there are unsaved changes. */
8738
+ const UNSAVED_CHANGES_TITLE_MARKER = '●';
8739
+ // The badge is a single app-wide surface, so every lock that wants one registers here and the badge
8740
+ // is derived from all of them - otherwise the first tracker to go clean would clear a badge that
8741
+ // another one still needs.
8742
+ const BADGE_HOLDERS = /* @__PURE__ */ new Map();
8743
+ const syncAppBadge = () => {
8744
+ const nav = typeof navigator === 'undefined' ? null : navigator;
8745
+ if (!nav?.setAppBadge || !nav.clearAppBadge) {
8746
+ return;
8747
+ }
8748
+ // Unsupported, or not installed - the promise rejects instead of throwing synchronously.
8749
+ const ignore = () => undefined;
8750
+ if (!BADGE_HOLDERS.size) {
8751
+ nav.clearAppBadge().catch(ignore);
8752
+ return;
8753
+ }
8754
+ const count = Array.from(BADGE_HOLDERS.values()).reduce((total, contents) => (typeof contents === 'number' ? total + contents : total), 0);
8755
+ // `setAppBadge()` without a count renders a plain dot.
8756
+ nav.setAppBadge(count || undefined).catch(ignore);
8757
+ };
8758
+ /**
8759
+ * The browser-tab flavor of the unsaved-changes family: while `hasChanges` reads `true`, the tab
8760
+ * cannot be closed or reloaded without the browser's confirm prompt, and - opt in - the tab title
8761
+ * carries a marker (optionally blinking), the favicon carries a dot, and the installed app carries a
8762
+ * badge.
8763
+ *
8764
+ * Every {@link createUnsavedChangesTracker} sets this up automatically (lock only); reach for it
8765
+ * directly when you track "unsaved" state that isn't a form, or want to lock the tab from a signal
8766
+ * you already have. Call from an injection context.
8767
+ *
8768
+ * ```ts
8769
+ * createUnsavedChangesTabLock({
8770
+ * hasChanges: computed(() => this.uploads().some((u) => u.pending)),
8771
+ * titleMarker: true,
8772
+ * flash: true,
8773
+ * });
8774
+ * ```
8775
+ */
8776
+ const createUnsavedChangesTabLock = (config) => {
8777
+ assertInInjectionContext(createUnsavedChangesTabLock);
8778
+ const { hasChanges } = config;
8779
+ const lock = config.lock ?? true;
8780
+ const flash = config.flash === true ? {} : config.flash || null;
8781
+ const favicon = config.favicon === true ? {} : config.favicon || null;
8782
+ const badge = config.badge ?? false;
8783
+ // Blinking needs something to blink, so it implies the default marker.
8784
+ const titleMarker = config.titleMarker === true
8785
+ ? UNSAVED_CHANGES_TITLE_MARKER
8786
+ : (config.titleMarker ?? (flash && !favicon ? UNSAVED_CHANGES_TITLE_MARKER : null));
8787
+ const destroyRef = inject(DestroyRef);
8788
+ const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
8789
+ const releaseFns = [];
8790
+ // The blink phase. Always `true` unless flashing is on, so the marker/favicon effects below can read
8791
+ // one signal either way.
8792
+ const flashPhase = signal(true, /* @ts-ignore */
8793
+ ...(ngDevMode ? [{ debugName: "flashPhase" }] : /* istanbul ignore next */ []));
8794
+ if (isBrowser && flash) {
8795
+ const interval = flash.interval ?? 1000;
8796
+ const whenHidden = flash.whenHidden ?? true;
8797
+ const isDocumentVisible = injectIsDocumentVisible();
8798
+ const shouldFlash = computed(() => hasChanges() && (!whenHidden || !isDocumentVisible()), /* @ts-ignore */
8799
+ ...(ngDevMode ? [{ debugName: "shouldFlash" }] : /* istanbul ignore next */ []));
8800
+ const flashEffect = effect((onCleanup) => {
8801
+ if (!shouldFlash()) {
8802
+ return;
8803
+ }
8804
+ const handle = setInterval(() => flashPhase.update((on) => !on), interval);
8805
+ onCleanup(() => {
8806
+ clearInterval(handle);
8807
+ flashPhase.set(true);
8808
+ });
8809
+ }, /* @ts-ignore */
8810
+ ...(ngDevMode ? [{ debugName: "flashEffect" }] : /* istanbul ignore next */ []));
8811
+ releaseFns.push(() => {
8812
+ flashEffect.destroy();
8813
+ flashPhase.set(true);
8814
+ });
8815
+ }
8816
+ /** Dirty, and in the "shown" half of a blink. */
8817
+ const isMarked = computed(() => hasChanges() && flashPhase(), /* @ts-ignore */
8818
+ ...(ngDevMode ? [{ debugName: "isMarked" }] : /* istanbul ignore next */ []));
8819
+ if (titleMarker) {
8820
+ const titleStore = injectTitleStore();
8821
+ const markerId = Symbol('unsaved-changes-title-marker');
8822
+ const markerEffect = effect(() => {
8823
+ const marked = isMarked();
8824
+ untracked(() => (marked ? titleStore.addMarker(markerId, titleMarker) : titleStore.removeMarker(markerId)));
8825
+ }, /* @ts-ignore */
8826
+ ...(ngDevMode ? [{ debugName: "markerEffect" }] : /* istanbul ignore next */ []));
8827
+ releaseFns.push(() => {
8828
+ markerEffect.destroy();
8829
+ titleStore.removeMarker(markerId);
8830
+ });
8831
+ }
8832
+ if (isBrowser && favicon) {
8833
+ const faviconStore = injectFaviconStore();
8834
+ const overlayId = Symbol('unsaved-changes-favicon');
8835
+ const faviconEffect = effect(() => {
8836
+ const marked = isMarked();
8837
+ untracked(() => marked
8838
+ ? faviconStore.addOverlay(overlayId, { kind: 'dot', color: favicon.color })
8839
+ : faviconStore.removeOverlay(overlayId));
8840
+ }, /* @ts-ignore */
8841
+ ...(ngDevMode ? [{ debugName: "faviconEffect" }] : /* istanbul ignore next */ []));
8842
+ releaseFns.push(() => {
8843
+ faviconEffect.destroy();
8844
+ faviconStore.removeOverlay(overlayId);
8845
+ });
8846
+ }
8847
+ if (isBrowser && lock) {
8848
+ const onBeforeUnload = (event) => {
8849
+ event.preventDefault();
8850
+ // Legacy Chrome / Safari gate the prompt on a truthy `returnValue` rather than on
8851
+ // `preventDefault()`. Deprecated, but still the only way to reach those engines.
8852
+ event.returnValue = true;
8853
+ };
8854
+ let attached = false;
8855
+ const attach = () => {
8856
+ if (attached)
8857
+ return;
8858
+ attached = true;
8859
+ window.addEventListener('beforeunload', onBeforeUnload);
8860
+ };
8861
+ const detach = () => {
8862
+ if (!attached)
8863
+ return;
8864
+ attached = false;
8865
+ window.removeEventListener('beforeunload', onBeforeUnload);
8866
+ };
8867
+ // Attached only while there are changes: a permanently registered `beforeunload` listener makes
8868
+ // the page ineligible for the back/forward cache, which would slow every back navigation.
8869
+ const lockEffect = effect(() => {
8870
+ const dirty = hasChanges();
8871
+ untracked(() => (dirty ? attach() : detach()));
8872
+ }, /* @ts-ignore */
8873
+ ...(ngDevMode ? [{ debugName: "lockEffect" }] : /* istanbul ignore next */ []));
8874
+ releaseFns.push(() => {
8875
+ lockEffect.destroy();
8876
+ detach();
8877
+ });
8878
+ }
8879
+ if (isBrowser && badge !== false) {
8880
+ const badgeId = Symbol('unsaved-changes-badge');
8881
+ const contents = badge === true ? true : badge;
8882
+ const badgeEffect = effect(() => {
8883
+ const dirty = hasChanges();
8884
+ untracked(() => {
8885
+ if (dirty) {
8886
+ BADGE_HOLDERS.set(badgeId, contents);
8887
+ }
8888
+ else {
8889
+ BADGE_HOLDERS.delete(badgeId);
8890
+ }
8891
+ syncAppBadge();
8892
+ });
8893
+ }, /* @ts-ignore */
8894
+ ...(ngDevMode ? [{ debugName: "badgeEffect" }] : /* istanbul ignore next */ []));
8895
+ releaseFns.push(() => {
8896
+ badgeEffect.destroy();
8897
+ BADGE_HOLDERS.delete(badgeId);
8898
+ syncAppBadge();
8899
+ });
8900
+ }
8901
+ const destroy = () => {
8902
+ while (releaseFns.length) {
8903
+ releaseFns.pop()?.();
8904
+ }
8905
+ };
8906
+ destroyRef.onDestroy(destroy);
8907
+ return { destroy };
8908
+ };
8909
+
8371
8910
  const toBooleanPromise = (result) => {
8372
8911
  if (result instanceof Promise) {
8373
8912
  return result.then(Boolean);
@@ -8382,8 +8921,15 @@ const toBooleanPromise = (result) => {
8382
8921
  * whether the watched form/value differs from it, and runs an async confirm before a discard.
8383
8922
  * Call from an injection context (a field initializer or constructor).
8384
8923
  *
8924
+ * While there are changes the browser tab is locked too (`beforeunload`), so closing or reloading the
8925
+ * tab needs a confirmation - see the `tab` config for the title-marker / app-badge extras and for
8926
+ * opting out.
8927
+ *
8928
+ * Every tracker registers with the app-wide {@link injectUnsavedChangesCoordinator}, which keeps a
8929
+ * single confirm on screen at a time and switches all guards off when the session ends (logout).
8930
+ *
8385
8931
  * Overlay and router flavors (`createOverlayUnsavedChangesGuard`, `createUnsavedChangesGuard`) build
8386
- * on this — use them when you want the guard wired to a close/navigation event automatically.
8932
+ * on this - use them when you want the guard wired to a close/navigation event automatically.
8387
8933
  */
8388
8934
  const createUnsavedChangesTracker = (config) => {
8389
8935
  assertInInjectionContext(createUnsavedChangesTracker);
@@ -8418,11 +8964,23 @@ const createUnsavedChangesTracker = (config) => {
8418
8964
  return !(compareFn ? compareFn(current, defaultValue) : equal(current, defaultValue));
8419
8965
  }, /* @ts-ignore */
8420
8966
  ...(ngDevMode ? [{ debugName: "hasChanges" }] : /* istanbul ignore next */ []));
8967
+ const tab = config.tab === false ? null : createUnsavedChangesTabLock({ ...config.tab, hasChanges });
8968
+ const coordinator = injectUnsavedChangesCoordinator();
8969
+ const _isAbandoned = signal(false, /* @ts-ignore */
8970
+ ...(ngDevMode ? [{ debugName: "_isAbandoned" }] : /* istanbul ignore next */ []));
8971
+ const unregister = coordinator.register({
8972
+ abandon: () => {
8973
+ _isAbandoned.set(true);
8974
+ // The session is gone: stop holding the tab hostage over edits that can no longer be saved.
8975
+ tab?.destroy();
8976
+ },
8977
+ });
8978
+ inject(DestroyRef).onDestroy(unregister);
8421
8979
  const runCheck = () => {
8422
- if (!untracked(hasChanges)) {
8980
+ if (untracked(_isAbandoned) || !untracked(hasChanges)) {
8423
8981
  return Promise.resolve(true);
8424
8982
  }
8425
- return toBooleanPromise(confirm(untracked(normalized.value)));
8983
+ return coordinator.runCheck((context) => toBooleanPromise(confirm(untracked(normalized.value), context)));
8426
8984
  };
8427
8985
  const refreshDefaultValue = () => _defaultValue.set(untracked(normalized.value));
8428
8986
  const restoreDefaultValue = () => {
@@ -8433,10 +8991,12 @@ const createUnsavedChangesTracker = (config) => {
8433
8991
  };
8434
8992
  return {
8435
8993
  hasChanges,
8994
+ isAbandoned: _isAbandoned.asReadonly(),
8436
8995
  defaultValue: _defaultValue.asReadonly(),
8437
8996
  runCheck,
8438
8997
  refreshDefaultValue,
8439
8998
  restoreDefaultValue,
8999
+ tab,
8440
9000
  };
8441
9001
  };
8442
9002
 
@@ -8459,5 +9019,5 @@ const createUnsavedChangesGuard = (config) => {
8459
9019
  * Generated bundle index. Do not edit.
8460
9020
  */
8461
9021
 
8462
- 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, createUnsavedChangesGuard, createUnsavedChangesTracker, createUserConsentProvider, deferredSignal, deleteCookie, dragGestureFrom, 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, holdScrollRestoration, htmlToMarkdown, inferMimeType, initials, injectAngularRootElement, injectAnimatedBlockSize, injectBoundaryElement, injectBreakpointInput, injectBreakpointIsMatched, injectBreakpointObserver, injectCanHover, injectColorThemes, injectColorThemesPrefix, injectCurrentBreakpoint, injectDeviceInputType, injectDisplayOrientation, injectErrorTheme, injectFocusVisibleTracker, injectFragment, injectHasPrecisionInput, injectHasTouchInput, injectHostElement, injectIs2Xl, injectIsDocumentVisible, 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, normalizeUnsavedChangesSource, 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, resolveAppRootColorProvider, 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 };
9022
+ export { ANIMATABLE_TOKEN, ANIMATED_IF_TOKEN, ANIMATED_LIFECYCLE_TOKEN, ANIMATION_DEBUG_STORAGE_KEY, AT_LEAST_ONE_REQUIRED, AnimatableDirective, AnimatedIfDirective, AnimatedLifecycleDirective, 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, SWIPE_VELOCITY_WINDOW_MS, ScrollObserverDirective, ScrollObserverEndDirective, ScrollObserverStartDirective, SeoDirective, SlugifyPipe, StructuredDataComponent, SurfaceInteractiveDirective, ToArrayPipe, TypedQueryList, UNSAVED_CHANGES_TITLE_MARKER, ValidateAtLeastOneRequired, Validators, anchoredOverlayPosition, animationDebugLog, applyAlternateBinding, applyAlternateLanguagesBindings, applyArticleBindings, applyAuthorBinding, applyCanonicalBinding, applyCenteredPosition, applyDescriptionBinding, applyFaviconOverlay, applyGlobalPosition, applyHeadBinding, applyHeadTitleBinding, applyHeadTitleMarker, 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, createRootThemeCss, createRoute, createRouterState, createRxHostListener, createSessionMemory, createSetup, createSwatchCss, createSwipeTracker, createTailwindColorThemes, createTailwindCssVar, createTailwindRgbVar, createThemeStyle, createUnsavedChangesGuard, createUnsavedChangesTabLock, createUnsavedChangesTracker, createUserConsentProvider, deferredSignal, defineLabels, defineProvider, defineRootProvider, defineStaticProvider, defineStaticRootProvider, deleteCookie, dragGestureFrom, easeElastic, easeIn, easeInOut, easeLinear, easeOut, easeOutBack, easeOutBackStrong, elementCanScroll, enableAnchoredOverlayPositionExtras, equal, firstElementSignal, focusElement, forceReflow, formatRuntimeError, fromNextFrame, getCookie, getDomain, getElementScrollCoordinates, getFocusableElements, getFormGroupValue, getGroupMatchPoints, getGroupMatchScore, getHeadingElement, getKnockoutMatchScore, getMatchScoreSubLine, getObjectProperty, getScrollContainerTarget, getScrollItemTarget, getScrollSnapTarget, hasCookie, holdScrollRestoration, htmlToMarkdown, inferMimeType, initials, injectAngularRootElement, injectAnimatedBlockSize, injectBoundaryElement, injectBreakpointInput, injectBreakpointIsMatched, injectBreakpointObserver, injectCanHover, injectColorThemes, injectColorThemesPrefix, injectCurrentBreakpoint, injectDeviceInputType, injectDisplayOrientation, injectErrorTheme, injectFaviconStore, injectFocusVisibleTracker, injectFragment, injectHasPrecisionInput, injectHasTouchInput, injectHostElement, injectIs2Xl, injectIsDocumentVisible, 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, injectRouterNavigationState, injectRouterState, injectScrollbarDimensions, injectStructuredDataConfig, injectStructuredDataStore, injectStyleManager, injectSuccessTheme, injectSurfaceContextTracker, injectSurfaceThemes, injectSurfaceThemesPrefix, injectTemplateRef, injectTitleConfig, injectTitleStore, injectUnsavedChangesCoordinator, injectUrl, injectViewportConfig, injectViewportDimensions, injectViewportSize, injectWarningTheme, isAnimationDebugEnabled, isArray, isElementSignal, isElementVisible, isFocusable, isGroupMatch, isHTMLElement, isKnockoutMatch, isObject, markdownToHtml, matchesReducedMotion, maybeSignalValue, memoizeSignal, nextFrame, normalizeGameResultType, normalizeMatchParticipant, normalizeMatchParticipants, normalizeMatchScore, normalizeMatchState, normalizeMatchType, normalizeUnsavedChangesSource, numberBreakpointAttribute, numberBreakpointTransform, previousSignalValue, provideBoundaryElement, provideBreakpointInstance, provideBreakpointObserver, provideColorThemes, provideColorThemesWithTailwind4, provideFaviconStore, provideFocusVisibleTracker, provideLinkStore, provideLinkStoreConfig, provideLocale, provideMetaConfig, provideMetaStore, provideOverlayRuntime, provideRenderer, provideStructuredDataConfig, provideStructuredDataStore, provideStyleManager, provideSurfaceContextTracker, provideSurfaceThemesWithTailwind4, provideTitleConfig, provideTitleStore, provideUnsavedChangesCoordinator, provideViewportConfig, randomId, readFromClipboard, registerAnchoredPositionMiddlewareExtras, registerAnchoredPositionSetup, resetPositioningStyles, resolveAppRootColorProvider, 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, toInjectFn, toProvideFn, toStringBinding, toToken, transformOrReturn, typedBreakpointTransform, unbindProps, useCursorDragScroll, writeScrollbarSizeToCssVariables, writeViewportSizeToCssVariables, ɵProvideColorThemes, ɵProvideColorThemesPrefix, ɵProvideSurfaceThemes, ɵProvideSurfaceThemesPrefix };
8463
9023
  //# sourceMappingURL=ethlete-core.mjs.map