@ethlete/core 5.0.0-next.36 → 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 [];
@@ -1989,8 +2049,8 @@ const elementCanScroll = (element, direction) => {
1989
2049
  };
1990
2050
  const createViewportRect = () => {
1991
2051
  // Collapses to a zero rect where there is no window (server). `isElementVisible` never reaches the
1992
- // intersection math there anyway — `elementCanScroll` already reports the document as unscrollable
1993
- // — so the rect only has to be a valid shape.
2052
+ // intersection math there anyway - `elementCanScroll` already reports the document as unscrollable
2053
+ // - so the rect only has to be a valid shape.
1994
2054
  const hasWindow = typeof window !== 'undefined';
1995
2055
  const width = hasWindow ? window.innerWidth : 0;
1996
2056
  const height = hasWindow ? window.innerHeight : 0;
@@ -2219,7 +2279,7 @@ const getScrollItemTarget = (entries, container, direction, scrollOrigin, axisDi
2219
2279
  const firstIndex = entries.indexOf(firstVisible);
2220
2280
  const lastIndex = entries.indexOf(lastVisible);
2221
2281
  const containerRect = container.getBoundingClientRect();
2222
- // 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).
2223
2283
  if (firstVisible === lastVisible) {
2224
2284
  const itemRect = firstVisible.target.getBoundingClientRect();
2225
2285
  const isStartEdgeVisible = axisDirection === 'horizontal'
@@ -2458,20 +2518,21 @@ const signalElementLastScrollDirection = (el) => {
2458
2518
  const signalHostElementLastScrollDirection = () => signalElementLastScrollDirection(inject(ElementRef));
2459
2519
 
2460
2520
  /**
2461
- * 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
2462
2522
  * tab or a minimised window.
2463
2523
  *
2464
2524
  * Not the same question as whether an element is on screen: an IntersectionObserver reports a fully visible
2465
2525
  * element in a background tab, because the tab's own visibility is no part of what it measures. Anything
2466
- * 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
2467
2527
  * hidden tab rather than stopping them, so work keeps piling up out of sight.
2468
2528
  */
2469
- const injectIsDocumentVisible = memoizeSignal(() => {
2529
+ const injectIsDocumentVisible = /* @__PURE__ */ memoizeSignal(() => {
2470
2530
  const document = inject(DOCUMENT);
2471
2531
  const isVisible = () => document.visibilityState === 'visible';
2472
2532
  return toSignal(fromEvent(document, 'visibilitychange').pipe(map(isVisible)), { initialValue: isVisible() });
2473
2533
  });
2474
2534
 
2535
+ const DEFAULT_SCROLL_STATE_MUTATIONS = { childList: true, subtree: true, attributes: true };
2475
2536
  const areScrollStatesEqual = (a, b) => {
2476
2537
  return (a.canScroll === b.canScroll &&
2477
2538
  a.canScrollHorizontally === b.canScrollHorizontally &&
@@ -2482,7 +2543,7 @@ const signalElementScrollState = (el, options) => {
2482
2543
  const elements = buildElementSignal(el);
2483
2544
  const observedEl = firstElementSignal(elements);
2484
2545
  const elementDimensions = signalElementDimensions(elements);
2485
- const elementMutations = signalElementMutations(elements, { childList: true, subtree: true, attributes: true });
2546
+ const elementMutations = signalElementMutations(elements, options?.mutations ?? DEFAULT_SCROLL_STATE_MUTATIONS);
2486
2547
  const isRendered = signalIsRendered();
2487
2548
  const initialScrollPosition = options?.initialScrollPosition;
2488
2549
  if (initialScrollPosition) {
@@ -2596,7 +2657,6 @@ const useCursorDragScroll = (el, options) => {
2596
2657
  const element = firstElementSignal(elements);
2597
2658
  const destroyRef = inject(DestroyRef);
2598
2659
  const { enabled = signal(true), allowedDirection = 'both' } = options ?? {};
2599
- const scrollState = signalElementScrollState(elements);
2600
2660
  const renderer = injectRenderer();
2601
2661
  const isDragging = signal(false, /* @ts-ignore */
2602
2662
  ...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
@@ -2609,8 +2669,13 @@ const useCursorDragScroll = (el, options) => {
2609
2669
  const dragAmount = signal({ x: 0, y: 0 }, /* @ts-ignore */
2610
2670
  ...(ngDevMode ? [{ debugName: "dragAmount" }] : /* istanbul ignore next */ []));
2611
2671
  const document = inject(DOCUMENT);
2612
- const canScroll = computed(() => {
2613
- 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;
2614
2679
  const direction = maybeSignalValue(allowedDirection);
2615
2680
  switch (direction) {
2616
2681
  case 'both':
@@ -2621,7 +2686,8 @@ const useCursorDragScroll = (el, options) => {
2621
2686
  return currentScrollState.canScrollVertically;
2622
2687
  }
2623
2688
  }, /* @ts-ignore */
2624
- ...(ngDevMode ? [{ debugName: "canScroll" }] : /* istanbul ignore next */ []));
2689
+ ...(ngDevMode ? [{ debugName: "measuredCanScroll" }] : /* istanbul ignore next */ []));
2690
+ const canScroll = suppliedCanScroll ?? measuredCanScroll;
2625
2691
  // Cleanup if the element the cursor drag scroll is bound to gets changed
2626
2692
  effect(() => {
2627
2693
  const { previousElement } = element();
@@ -2722,7 +2788,7 @@ const useCursorDragScroll = (el, options) => {
2722
2788
  dragAmount.set({ x: 0, y: 0 });
2723
2789
  };
2724
2790
  const setupDragging = (e) => {
2725
- // 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
2726
2792
  // right click (or a ctrl-click on a Mac, which is the primary button) leaves the drag latched on and
2727
2793
  // every later mouse move scrolls the container.
2728
2794
  const dragEnd = merge(fromEvent(document, 'mouseup'), fromEvent(document, 'contextmenu'));
@@ -2749,7 +2815,7 @@ const useCursorDragScroll = (el, options) => {
2749
2815
  };
2750
2816
  };
2751
2817
 
2752
- const ET_PROPERTY_REMOVED = Symbol('ET_PROPERTY_REMOVED');
2818
+ const ET_PROPERTY_REMOVED = /* @__PURE__ */ Symbol('ET_PROPERTY_REMOVED');
2753
2819
  const transformOrReturn = (src, config) => {
2754
2820
  const transformer = config?.transform;
2755
2821
  if (transformer) {
@@ -2766,7 +2832,7 @@ const createInitialRoute = () => {
2766
2832
  return window.location.pathname + window.location.search + window.location.hash;
2767
2833
  };
2768
2834
  /** Inject the current router event */
2769
- const injectRouterEvent = memoizeSignal(() => {
2835
+ const injectRouterEvent = /* @__PURE__ */ memoizeSignal(() => {
2770
2836
  const router = inject(Router);
2771
2837
  const initialRoute = createInitialRoute();
2772
2838
  return toSignal(router.events, {
@@ -2777,7 +2843,7 @@ const injectRouterEvent = memoizeSignal(() => {
2777
2843
  * Signal that indicates whether the router has been initialized.
2778
2844
  * The router is considered initialized once the first NavigationEnd event with an id different than -1 has been emitted.
2779
2845
  */
2780
- const injectIsRouterInitialized = memoizeSignal(() => {
2846
+ const injectIsRouterInitialized = /* @__PURE__ */ memoizeSignal(() => {
2781
2847
  const event = injectRouterEvent();
2782
2848
  return computed(() => {
2783
2849
  const e = event();
@@ -2789,7 +2855,7 @@ const injectIsRouterInitialized = memoizeSignal(() => {
2789
2855
  * The url includes query params as well as the fragment. Use `injectRoute` instead if you are not intrusted in those.
2790
2856
  * @example "/my-page?query=1&param=true#fragment"
2791
2857
  */
2792
- const injectUrl = memoizeSignal(() => {
2858
+ const injectUrl = /* @__PURE__ */ memoizeSignal(() => {
2793
2859
  const event = injectRouterEvent();
2794
2860
  const router = inject(Router);
2795
2861
  // Captured once for reads that happen before the router has committed any
@@ -2799,7 +2865,7 @@ const injectUrl = memoizeSignal(() => {
2799
2865
  // We depend on `event` and `currentNavigation` only to get re-evaluated
2800
2866
  // around each navigation, but we read the *committed* url straight from the
2801
2867
  // router. The router commits `currentUrlTree` before it activates components,
2802
- // 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 -
2803
2869
  // unlike the NavigationEnd event, which only reaches this signal *after*
2804
2870
  // activation.
2805
2871
  event();
@@ -2819,7 +2885,7 @@ const injectUrl = memoizeSignal(() => {
2819
2885
  * Inject the current route
2820
2886
  * @example "/my-page"
2821
2887
  */
2822
- const injectRoute = memoizeSignal(() => {
2888
+ const injectRoute = /* @__PURE__ */ memoizeSignal(() => {
2823
2889
  const url = injectUrl();
2824
2890
  return computed(() => {
2825
2891
  const fullUrl = url();
@@ -2854,14 +2920,9 @@ const createRoute = (router) => {
2854
2920
  /**
2855
2921
  * Inject the complete router state. This includes the current route data, path params, query params, title and fragment.
2856
2922
  */
2857
- const injectRouterState = memoizeSignal(() => {
2923
+ const injectRouterState = /* @__PURE__ */ memoizeSignal(() => {
2858
2924
  const event = injectRouterEvent();
2859
2925
  const router = inject(Router);
2860
- // We only depend on `event` to get re-evaluated on every router event, but we
2861
- // read the *committed* snapshot straight from the router. The router commits
2862
- // `routerState` before it activates components (and emits ActivationStart
2863
- // before NavigationEnd), so this is already correct inside a child component's
2864
- // constructor — unlike an effect, which only flushes after construction.
2865
2926
  return computed(() => {
2866
2927
  event();
2867
2928
  return createRouterState(router);
@@ -2930,7 +2991,7 @@ const injectPathParam = (key, config) => {
2930
2991
  * Inject query params that changed during navigation. Unchanged query params will be ignored.
2931
2992
  * Removed query params will be represented by the symbol `ET_PROPERTY_REMOVED`.
2932
2993
  */
2933
- const injectQueryParamChanges = memoizeSignal(() => {
2994
+ const injectQueryParamChanges = /* @__PURE__ */ memoizeSignal(() => {
2934
2995
  const queryParams = injectQueryParams();
2935
2996
  const prevQueryParams = previousSignalValue(queryParams);
2936
2997
  return computed(() => {
@@ -2954,7 +3015,7 @@ const injectQueryParamChanges = memoizeSignal(() => {
2954
3015
  * Inject path params that changed during navigation. Unchanged path params will be ignored.
2955
3016
  * Removed path params will be represented by the symbol `ET_PROPERTY_REMOVED`.
2956
3017
  */
2957
- const injectPathParamChanges = memoizeSignal(() => {
3018
+ const injectPathParamChanges = /* @__PURE__ */ memoizeSignal(() => {
2958
3019
  const pathParams = injectPathParams();
2959
3020
  const prevPathParams = previousSignalValue(pathParams);
2960
3021
  return computed(() => {
@@ -2976,11 +3037,11 @@ const injectPathParamChanges = memoizeSignal(() => {
2976
3037
  });
2977
3038
  /**
2978
3039
  * The state handed to the navigation that is currently in progress, i.e. what
2979
- * `router.navigate(…, { state })` passed — typed to whatever you expect.
3040
+ * `router.navigate(…, { state })` passed - typed to whatever you expect.
2980
3041
  *
2981
3042
  * Read **synchronously**, in a constructor or a resolver, and deliberately not a signal: navigation state exists
2982
3043
  * only for the duration of the navigation that carries it. By the time an effect flushed, the navigation would be
2983
- * over and the answer always `null`. Returns `null` when there is no navigation in flight or it carried no state —
3044
+ * over and the answer always `null`. Returns `null` when there is no navigation in flight or it carried no state -
2984
3045
  * which is the normal case for a page arrived at by typing its URL, so always handle it.
2985
3046
  *
2986
3047
  * Use it for the things a URL should not carry: a "you were redirected because…" reason, a pre-fetched object the
@@ -3000,21 +3061,16 @@ const injectRouterNavigationState = () => {
3000
3061
  return router.getCurrentNavigation()?.extras.state ?? null;
3001
3062
  };
3002
3063
 
3003
- const ET_DISABLE_SCROLL_TOP = Symbol('ET_DISABLE_SCROLL_TOP');
3004
- const ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE = Symbol('ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE');
3005
- 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');
3006
3067
  const routerDisableScrollTop = (config = {}) => {
3007
3068
  return {
3008
3069
  ...(!config.asReturnRoute ? { [ET_DISABLE_SCROLL_TOP]: true } : { [ET_DISABLE_SCROLL_TOP_AS_RETURN_ROUTE]: true }),
3009
3070
  ...(config.onPathParamChange ? { [ET_DISABLE_SCROLL_TOP_ON_PATH_PARAM_CHANGE]: true } : {}),
3010
3071
  };
3011
3072
  };
3012
- /**
3013
- * Registry of "not settled yet" reporters. Lives in the root injector so a route component can
3014
- * reach the registration created by the app level `setupScrollRestoration` call.
3015
- * @internal
3016
- */
3017
- const [, injectScrollRestorationHolds] = createRootProvider(() => {
3073
+ const SCROLL_RESTORATION_HOLDS_DEF = /* @__PURE__ */ defineRootProvider(() => {
3018
3074
  const holds = new Set();
3019
3075
  return {
3020
3076
  add: (isPending) => {
@@ -3030,11 +3086,17 @@ const [, injectScrollRestorationHolds] = createRootProvider(() => {
3030
3086
  },
3031
3087
  };
3032
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);
3033
3095
  /**
3034
3096
  * Suspend scroll restoration while `isPending` reads `true`.
3035
3097
  *
3036
3098
  * Restoration already waits for the scroll container to grow tall enough to reach the saved offset,
3037
- * 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 -
3038
3100
  * a slow list request, for instance. Registers for the lifetime of the current injection context.
3039
3101
  *
3040
3102
  * @example
@@ -3068,7 +3130,7 @@ const setupScrollRestoration = (config = {}) => {
3068
3130
  return el ?? document.documentElement;
3069
3131
  };
3070
3132
  // The browser restores the offset against the still-loading (much shorter) document, which is the
3071
- // exact problem we are solving — take it over completely.
3133
+ // exact problem we are solving - take it over completely.
3072
3134
  if (restoreEnabled && 'scrollRestoration' in history) {
3073
3135
  history.scrollRestoration = 'manual';
3074
3136
  }
@@ -3107,7 +3169,7 @@ const setupScrollRestoration = (config = {}) => {
3107
3169
  }
3108
3170
  const now = Date.now();
3109
3171
  // A registered hold means the page knows more data is coming, so the content is expected to
3110
- // 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.
3111
3173
  if (holds.isHeld() && now < hardDeadline) {
3112
3174
  deadline = now + restoreTimeout;
3113
3175
  }
@@ -3120,7 +3182,7 @@ const setupScrollRestoration = (config = {}) => {
3120
3182
  }
3121
3183
  cancelPendingRestore();
3122
3184
  };
3123
- // 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
3124
3186
  // components exist but the DOM still shows the previous page, whose height would be measured
3125
3187
  // instead. The timeout is a safety net in case no render pass follows.
3126
3188
  const start = () => {
@@ -3244,7 +3306,7 @@ const createLogger = (config) => {
3244
3306
  };
3245
3307
 
3246
3308
  const escapeHtml = (str) => str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3247
- /** 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
3248
3310
  * may already contain entities (e.g. Markdown serialized from the editor's DOM, where `&` is
3249
3311
  * stored as `&amp;`), where plain escaping would double-escape them. */
3250
3312
  const escapeHtmlPreservingEntities = (str) => str
@@ -3253,7 +3315,7 @@ const escapeHtmlPreservingEntities = (str) => str
3253
3315
  .replace(/>/g, '&gt;')
3254
3316
  .replace(/"/g, '&quot;');
3255
3317
  /** Rejects URL schemes that execute script when navigated. Whitespace/control characters are
3256
- * 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
3257
3319
  * otherwise slip through. */
3258
3320
  const isSafeUrl = (url) =>
3259
3321
  // eslint-disable-next-line no-control-regex -- stripping control characters is the point: browsers ignore them inside URLs
@@ -3280,7 +3342,7 @@ const parseSeparatorAligns = (line) => parseTableRow(line).map((cell) => {
3280
3342
  /** The GFM separator token for a column alignment. */
3281
3343
  const separatorFor = (align) => align === 'center' ? ':---:' : align === 'right' ? '---:' : align === 'left' ? ':---' : '---';
3282
3344
  const alignStyle = (align) => (align ? ` style="text-align: ${align}"` : '');
3283
- /** 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 -
3284
3346
  * CommonMark emphasis must not face whitespace on the inside (`** fett**` doesn't parse), and
3285
3347
  * `&nbsp;` counts as whitespace there too. Whitespace-only content stays unwrapped. */
3286
3348
  const emphasize = (marker, content) => {
@@ -3294,9 +3356,9 @@ const makePlaceholder = (kind, idx) => `\u{E000}${kind}${idx}\u{E001}`;
3294
3356
  const placeholderRe = (kind) => new RegExp(`\u{E000}${kind}(\\d+)\u{E001}`, 'gu');
3295
3357
  const isPlaceholder = (kind, str) => new RegExp(`^\u{E000}${kind}\\d+\u{E001}$`, 'u').test(str);
3296
3358
  /** Tags allowed to survive inside an aligned block's raw-HTML passthrough. */
3297
- 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']);
3298
3360
  /** Reduces raw inline HTML to the editor's own vocabulary: allowed tags lose all their attributes
3299
- * (`<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
3300
3362
  * dropped, and the remaining text is escaped. */
3301
3363
  const sanitizeInlineHtml = (html) => {
3302
3364
  const kept = [];
@@ -3321,7 +3383,7 @@ const processInline = (text) => {
3321
3383
  });
3322
3384
  // "Open in new tab" links round-trip as raw HTML because Markdown has no `target` syntax. Extract
3323
3385
  // them before the raw-HTML escape below, keeping only a safe href, `target="_blank"` and a forced
3324
- // `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
3325
3387
  // inner markup is processed recursively. Anything else that looks like raw HTML is still escaped.
3326
3388
  const newTabLinks = [];
3327
3389
  text = text.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (full, attrs, inner) => {
@@ -3332,11 +3394,11 @@ const processInline = (text) => {
3332
3394
  const anchor = `<a href="${href}" target="_blank" rel="noopener noreferrer">${processInline(inner)}</a>`;
3333
3395
  return makePlaceholder('NTLINK', newTabLinks.push(anchor) - 1);
3334
3396
  });
3335
- // 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
3336
3398
  // into the DOM via innerHTML, so anything else would let a crafted value inject markup. `<u>`
3337
3399
  // is the one deliberate exception: underline has no Markdown form and round-trips as raw <u>.
3338
3400
  text = escapeHtmlPreservingEntities(text).replace(/&lt;(\/?)u&gt;/gi, '<$1u>');
3339
- // 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
3340
3402
  // run (CommonMark's flanking rule), so literal asterisks as in `2 * 3 * 4` stay text.
3341
3403
  text = text.replace(/\*\*\*([^\s*](?:.*?[^\s*])?)\*\*\*/g, '<strong><em>$1</em></strong>');
3342
3404
  text = text.replace(/\*\*([^\s*](?:.*?[^\s*])?)\*\*/g, '<strong>$1</strong>');
@@ -3399,6 +3461,75 @@ const findListItems = (inner) => {
3399
3461
  }
3400
3462
  return items;
3401
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
+ };
3402
3533
  /** Serializes a list's inner HTML to Markdown, indenting nested lists two spaces per level. */
3403
3534
  const listToMarkdown = (inner, ordered, depth) => {
3404
3535
  const indent = ' '.repeat(depth);
@@ -3417,7 +3548,7 @@ const listToMarkdown = (inner, ordered, depth) => {
3417
3548
  })
3418
3549
  .join('\n');
3419
3550
  };
3420
- /** 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. */
3421
3552
  const parseListLines = (lines) => lines
3422
3553
  .map((line) => {
3423
3554
  const match = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line);
@@ -3491,7 +3622,7 @@ const markdownToHtml = (markdown) => {
3491
3622
  // Horizontal rule
3492
3623
  if (/^(---|\*\*\*|___)\s*$/.test(trimmed))
3493
3624
  return '<hr>';
3494
- // 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
3495
3626
  const tableLines = trimmed.split('\n');
3496
3627
  if (tableLines.length >= 2 && /\|/.test(tableLines[0] ?? '') && isTableSeparatorLine(tableLines[1] ?? '')) {
3497
3628
  const aligns = parseSeparatorAligns(tableLines[1] ?? '');
@@ -3509,15 +3640,10 @@ const markdownToHtml = (markdown) => {
3509
3640
  : '';
3510
3641
  return `<table>${thead}${tbody}</table>`;
3511
3642
  }
3512
- // Blockquote — each line runs through the inline pass on its own (processInline escapes raw
3513
- // HTML, so joining first would escape the <br> separators too)
3514
- if (/^> /.test(trimmed)) {
3515
- const content = trimmed
3516
- .split('\n')
3517
- .map((l) => processInline(l.replace(/^>\s?/, '')))
3518
- .join('<br>');
3519
- return `<blockquote>${content}</blockquote>`;
3520
- }
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'));
3521
3647
  // List (unordered/ordered, with indentation-based nesting)
3522
3648
  if (/^([-*+]|\d+\.)\s/.test(trimmed)) {
3523
3649
  const lines = parseListLines(trimmed.split('\n'));
@@ -3525,7 +3651,7 @@ const markdownToHtml = (markdown) => {
3525
3651
  return buildListHtml(lines, 0, lines[0].indent).html;
3526
3652
  }
3527
3653
  }
3528
- // Paragraph — single newlines within a block become <br>
3654
+ // Paragraph - single newlines within a block become <br>
3529
3655
  return `<p>${trimmed
3530
3656
  .split('\n')
3531
3657
  .map((l) => processInline(l))
@@ -3545,21 +3671,21 @@ const htmlToMarkdown = (html) => {
3545
3671
  return '';
3546
3672
  let md = html;
3547
3673
  // Aligned blocks: text-align has no Markdown form, so preserve them verbatim as native HTML (their
3548
- // 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
3549
3675
  // below rewrite them, restored after the final tag-strip.
3550
3676
  const alignedBlocks = [];
3551
3677
  md = md.replace(/<(p|h[1-6]|div)\b[^>]*\bstyle="[^"]*text-align[^"]*"[^>]*>[\s\S]*?<\/\1>/gi, (match) => makePlaceholder('ALIGN', alignedBlocks.push(match) - 1));
3552
- // Code blocks — process before inline code
3678
+ // Code blocks - process before inline code
3553
3679
  md = md.replace(/<pre[^>]*><code[^>]*class="language-([^"]+)"[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, lang, code) => `\n\`\`\`${lang}\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
3554
3680
  md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, code) => `\n\`\`\`\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
3555
3681
  md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => `\n\`\`\`\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
3556
- // 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
3557
3683
  // bold/italic/link/code into markdown; leftover tags are stripped at the end.
3558
3684
  for (let i = 6; i >= 1; i--) {
3559
3685
  md = md.replace(new RegExp(`<h${i}[^>]*>([\\s\\S]*?)<\\/h${i}>`, 'gi'), (_, content) => `\n${'#'.repeat(i)} ${content.trim()}\n`);
3560
3686
  }
3561
3687
  // Bold + italic (combined before individual). Boundary whitespace is hoisted out of the
3562
- // 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**`.
3563
3689
  md = md.replace(/<strong[^>]*><em[^>]*>([\s\S]*?)<\/em><\/strong>/gi, (_, c) => emphasize('***', c));
3564
3690
  md = md.replace(/<em[^>]*><strong[^>]*>([\s\S]*?)<\/strong><\/em>/gi, (_, c) => emphasize('***', c));
3565
3691
  md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _tag, c) => emphasize('**', c));
@@ -3568,7 +3694,7 @@ const htmlToMarkdown = (html) => {
3568
3694
  // Inline code
3569
3695
  md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, code) => `\`${unescapeHtml(code)}\``);
3570
3696
  // Links and images. A link with `target="_blank"` has no Markdown form, so it round-trips as raw
3571
- // 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
3572
3698
  // become `[text](url)`.
3573
3699
  const newTabLinks = [];
3574
3700
  md = md.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (_, attrs, inner) => {
@@ -3582,20 +3708,17 @@ const htmlToMarkdown = (html) => {
3582
3708
  md = md.replace(/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '![$2]($1)');
3583
3709
  md = md.replace(/<img[^>]+alt="([^"]*)"[^>]*src="([^"]*)"[^>]*\/?>/gi, '![$1]($2)');
3584
3710
  md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, '![]($1)');
3585
- // 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
3586
3712
  // inner markup is already converted above) so the block passes below don't strip the tag; the
3587
3713
  // placeholder is restored after the final tag-strip.
3588
3714
  const underlines = [];
3589
3715
  md = md.replace(/<u[^>]*>([\s\S]*?)<\/u>/gi, (_, inner) => makePlaceholder('U', underlines.push(inner) - 1));
3590
- // Block quotes — <br> line breaks become one quoted line each (stripTags would swallow them)
3591
- md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
3592
- return (stripTags(content.replace(/<br\s*\/?>/gi, '\n'))
3593
- .trim()
3594
- .split('\n')
3595
- .map((line) => `> ${line.trim()}`)
3596
- .join('\n') + '\n');
3597
- });
3598
- // 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).
3599
3722
  for (let list = findList(md); list; list = findList(md)) {
3600
3723
  md = `${md.slice(0, list.start)}\n${listToMarkdown(list.inner, list.ordered, 0)}\n${md.slice(list.end)}`;
3601
3724
  }
@@ -3623,10 +3746,10 @@ const htmlToMarkdown = (html) => {
3623
3746
  const lines = [`| ${headerCells.join(' | ')} |`, `| ${separator} |`, ...bodyRows.map((r) => `| ${r} |`)];
3624
3747
  return `\n${lines.join('\n')}\n`;
3625
3748
  });
3626
- // 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
3627
3750
  // the tag-strip removes it, so it round-trips (a single newline within a block renders as <br>)
3628
3751
  md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, content) => `\n${stripTags(content.replace(/<br\s*\/?>/gi, '\n')).trim()}\n`);
3629
- // 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
3630
3753
  // paragraphs, and silently stripping them would merge adjacent blocks into one.
3631
3754
  md = md.replace(/<\/div\s*>/gi, '\n\n').replace(/<div[^>]*>/gi, '\n\n');
3632
3755
  md = md.replace(/<br\s*\/?>/gi, '\n');
@@ -3685,17 +3808,11 @@ class RuntimeError extends Error {
3685
3808
  this.code = code;
3686
3809
  this.data = data;
3687
3810
  if (data !== RUNTIME_ERROR_NO_DATA) {
3688
- try {
3689
- const _data = clone(data);
3690
- setTimeout(() => {
3691
- console.error(_data);
3692
- }, 1);
3693
- }
3694
- catch {
3695
- setTimeout(() => {
3696
- console.error(data);
3697
- }, 1);
3698
- }
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);
3699
3816
  }
3700
3817
  }
3701
3818
  }
@@ -3732,7 +3849,7 @@ const canUseSessionMemory = () => {
3732
3849
  if (typeof globalThis.document === 'undefined') {
3733
3850
  return false;
3734
3851
  }
3735
- return globalThis.sessionStorage != null;
3852
+ return globalThis.sessionStorage !== null && globalThis.sessionStorage !== undefined;
3736
3853
  }
3737
3854
  catch {
3738
3855
  return false;
@@ -3859,8 +3976,8 @@ const createSwipeTracker = (startEvent) => {
3859
3976
  if (firstInWindow === -1)
3860
3977
  return { x: 0, y: 0 };
3861
3978
  const newest = samples[samples.length - 1];
3862
- // Measure across the window when it holds a span of its own. With a single sample in it — a
3863
- // pointer emitting moves slower than the window — reach back one sample for a baseline instead
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
3864
3981
  // of reporting nothing.
3865
3982
  const hasSpanInWindow = samples.length - firstInWindow >= 2;
3866
3983
  const baseline = samples[hasSpanInWindow ? firstInWindow : Math.max(0, firstInWindow - 1)];
@@ -3897,7 +4014,7 @@ const createSwipeTracker = (startEvent) => {
3897
4014
  return { update, end, cancel };
3898
4015
  };
3899
4016
 
3900
- const [, injectAngularRootElement] = createRootProvider(() => {
4017
+ const ANGULAR_ROOT_ELEMENT_DEF = /* @__PURE__ */ defineRootProvider(() => {
3901
4018
  const appRef = inject(ApplicationRef);
3902
4019
  const rootElement = signal(null, /* @ts-ignore */
3903
4020
  ...(ngDevMode ? [{ debugName: "rootElement" }] : /* istanbul ignore next */ []));
@@ -3913,8 +4030,9 @@ const [, injectAngularRootElement] = createRootProvider(() => {
3913
4030
  setTimeout(poll);
3914
4031
  return rootElement;
3915
4032
  });
4033
+ const injectAngularRootElement = /* @__PURE__ */ toInjectFn(ANGULAR_ROOT_ELEMENT_DEF);
3916
4034
 
3917
- const [provideBoundaryElement, injectBoundaryElement, BOUNDARY_ELEMENT_TOKEN] = createProvider(() => {
4035
+ const BOUNDARY_ELEMENT_DEF = /* @__PURE__ */ defineProvider(() => {
3918
4036
  const hostElement = inject(ElementRef, { optional: true });
3919
4037
  const document = inject(DOCUMENT);
3920
4038
  const override = signal(null, /* @ts-ignore */
@@ -3926,8 +4044,11 @@ const [provideBoundaryElement, injectBoundaryElement, BOUNDARY_ELEMENT_TOKEN] =
3926
4044
  override,
3927
4045
  };
3928
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);
3929
4050
 
3930
- const [provideFocusVisibleTracker, injectFocusVisibleTracker, FOCUS_VISIBLE_TRACKER_TOKEN] = createRootProvider(() => {
4051
+ const FOCUS_VISIBLE_TRACKER_DEF = /* @__PURE__ */ defineRootProvider(() => {
3931
4052
  const document = inject(DOCUMENT);
3932
4053
  const destroyRef = inject(DestroyRef);
3933
4054
  const isFocusVisible = signal(false, /* @ts-ignore */
@@ -3969,8 +4090,11 @@ const [provideFocusVisibleTracker, injectFocusVisibleTracker, FOCUS_VISIBLE_TRAC
3969
4090
  isFocusVisible: isFocusVisible.asReadonly(),
3970
4091
  };
3971
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);
3972
4096
 
3973
- const [provideLocale, injectLocale] = createRootProvider(() => {
4097
+ const LOCALE_DEF = /* @__PURE__ */ defineRootProvider(() => {
3974
4098
  const currentLocale = signal('en', /* @ts-ignore */
3975
4099
  ...(ngDevMode ? [{ debugName: "currentLocale" }] : /* istanbul ignore next */ []));
3976
4100
  return {
@@ -3979,12 +4103,14 @@ const [provideLocale, injectLocale] = createRootProvider(() => {
3979
4103
  }, {
3980
4104
  name: 'Locale',
3981
4105
  });
4106
+ const provideLocale = /* @__PURE__ */ toProvideFn(LOCALE_DEF);
4107
+ const injectLocale = /* @__PURE__ */ toInjectFn(LOCALE_DEF);
3982
4108
 
3983
4109
  const resolveSource = (source, locale) => typeof source === 'function' ? source(locale) : source;
3984
4110
  /**
3985
4111
  * The one mechanism for the strings a component renders or announces itself. Every domain in the UI
3986
- * library that has such strings exposes exactly this pair — `provide<Domain>Labels` to localize a
3987
- * subtree, `inject<Domain>Labels()` to read the result — so localizing an app is the same move
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
3988
4114
  * repeated per domain rather than a different one each time.
3989
4115
  *
3990
4116
  * `defaults` may itself be locale-derived, for a domain that ships more than one language (the
@@ -3992,7 +4118,7 @@ const resolveSource = (source, locale) => typeof source === 'function' ? source(
3992
4118
  * resolved to, so translating into a third language while keeping a shipped one as the base is the
3993
4119
  * same call.
3994
4120
  *
3995
- * The injector returns a **signal**, because both halves can change at runtime — read it in a
4121
+ * The injector returns a **signal**, because both halves can change at runtime - read it in a
3996
4122
  * template or computed, never destructure it once.
3997
4123
  *
3998
4124
  * @example
@@ -4003,8 +4129,11 @@ const resolveSource = (source, locale) => typeof source === 'function' ? source(
4003
4129
  * overflow: 'Show hidden levels',
4004
4130
  * };
4005
4131
  *
4006
- * export const [provideBreadcrumbLabels, injectBreadcrumbLabels, BREADCRUMB_LABELS] =
4007
- * createLabels<BreadcrumbLabels>('BREADCRUMB_LABELS', DEFAULT_BREADCRUMB_LABELS);
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);
4008
4137
  *
4009
4138
  * @example
4010
4139
  * // consumer, fixed wording
@@ -4013,24 +4142,26 @@ const resolveSource = (source, locale) => typeof source === 'function' ? source(
4013
4142
  * // consumer, driven by the app's i18n and re-resolved on a locale change
4014
4143
  * provideBreadcrumbLabels((locale) => ({ navigation: translate('breadcrumb.nav', locale) }));
4015
4144
  */
4016
- const createLabels = (name, defaults) => {
4145
+ const defineLabels = (name, defaults) => {
4017
4146
  const token = new InjectionToken(name, {
4018
4147
  providedIn: 'root',
4019
4148
  factory: () => ({}),
4020
4149
  });
4021
- const provide = (labels) => ({ provide: token, useValue: labels });
4022
- const injectFn = () => {
4023
- const source = inject(token);
4024
- const { currentLocale } = injectLocale();
4025
- return computed(() => {
4026
- const locale = currentLocale();
4027
- return { ...resolveSource(defaults, locale), ...resolveSource(source, locale) };
4028
- });
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,
4029
4161
  };
4030
- return [provide, injectFn, token];
4031
4162
  };
4032
4163
 
4033
- const [provideStyleManager, injectStyleManager] = createRootProvider(() => {
4164
+ const STYLE_MANAGER_DEF = /* @__PURE__ */ defineRootProvider(() => {
4034
4165
  const appRef = inject(ApplicationRef);
4035
4166
  const destroyRef = inject(DestroyRef);
4036
4167
  const document = inject(DOCUMENT);
@@ -4089,6 +4220,8 @@ const [provideStyleManager, injectStyleManager] = createRootProvider(() => {
4089
4220
  });
4090
4221
  return { mount };
4091
4222
  }, { name: 'StyleManager' });
4223
+ const provideStyleManager = /* @__PURE__ */ toProvideFn(STYLE_MANAGER_DEF);
4224
+ const injectStyleManager = /* @__PURE__ */ toInjectFn(STYLE_MANAGER_DEF);
4092
4225
 
4093
4226
  const createUserConsentProvider = (options) => ({
4094
4227
  provide: options.for,
@@ -4123,7 +4256,7 @@ const forceReflow = (element) => {
4123
4256
  *
4124
4257
  * Inside a component, prefer the reactive `injectPrefersReducedMotion()`. This is for the animation
4125
4258
  * helpers that run from effects, event handlers and imperative APIs, where there is no injection
4126
- * context. Pass the element being animated so the query resolves against its own window — otherwise
4259
+ * context. Pass the element being animated so the query resolves against its own window - otherwise
4127
4260
  * an animation inside an iframe would read the top-level document's preference.
4128
4261
  */
4129
4262
  const matchesReducedMotion = (element) => {
@@ -4354,7 +4487,7 @@ class AnimatedLifecycleDirective {
4354
4487
  if (!e.cancelled && e.transitionId === transitionId) {
4355
4488
  return of(true);
4356
4489
  }
4357
- // 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
4358
4491
  // restarted a running transition mid-flight (an anchored overlay whose ancestor pane got
4359
4492
  // destroyed) or a foreign transition on the same element finished. If nothing is
4360
4493
  // animating anymore the expected end event can never arrive, so treat the transition as
@@ -4501,321 +4634,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
4501
4634
  }]
4502
4635
  }], ctorParameters: () => [], propDecorators: { ifValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "etAnimatedIf", required: false }] }] } });
4503
4636
 
4504
- class AnimatedOverlayDirective {
4505
- constructor() {
4506
- this.overlayService = inject(Overlay);
4507
- this.injector = inject(Injector);
4508
- this.viewContainerRef = inject(ViewContainerRef);
4509
- this.zone = inject(NgZone);
4510
- this.elementRef = inject(ElementRef);
4511
- this.rootBoundary = injectBoundaryElement({ optional: true });
4512
- this.destroyRef = inject(DestroyRef);
4513
- this.floatingElCleanupFn = null;
4514
- this.unmountSubscription = null;
4515
- this.placement = input('bottom', /* @ts-ignore */
4516
- ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
4517
- this.fallbackPlacements = input(/* @ts-ignore */
4518
- ...(ngDevMode ? [undefined, { debugName: "fallbackPlacements" }] : /* istanbul ignore next */ []));
4519
- this.offset = input(null, /* @ts-ignore */
4520
- ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
4521
- this.arrowPadding = input(4, /* @ts-ignore */
4522
- ...(ngDevMode ? [{ debugName: "arrowPadding" }] : /* istanbul ignore next */ []));
4523
- this.viewportPadding = input(8, /* @ts-ignore */
4524
- ...(ngDevMode ? [{ debugName: "viewportPadding" }] : /* istanbul ignore next */ []));
4525
- this.autoResize = input(false, { ...(ngDevMode ? { debugName: "autoResize" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4526
- this.shift = input(true, { ...(ngDevMode ? { debugName: "shift" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4527
- this.autoHide = input(false, { ...(ngDevMode ? { debugName: "autoHide" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4528
- this.autoCloseIfReferenceHidden = input(false, { ...(ngDevMode ? { debugName: "autoCloseIfReferenceHidden" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4529
- this.referenceElement = input(this.elementRef.nativeElement, /* @ts-ignore */
4530
- ...(ngDevMode ? [{ debugName: "referenceElement" }] : /* istanbul ignore next */ []));
4531
- this.mirrorWidth = input(false, { ...(ngDevMode ? { debugName: "mirrorWidth" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4532
- this.portal = null;
4533
- this.overlayRef = null;
4534
- this.componentRef = null;
4535
- this.beforeOpened$ = new Subject();
4536
- this.afterOpened$ = new Subject();
4537
- this.beforeClosed$ = new Subject();
4538
- this.afterClosed$ = new Subject();
4539
- this.state = signal('init', /* @ts-ignore */
4540
- ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
4541
- this.isMounted = computed(() => this.state() === 'mounted', /* @ts-ignore */
4542
- ...(ngDevMode ? [{ debugName: "isMounted" }] : /* istanbul ignore next */ []));
4543
- this.isMounting = computed(() => this.state() === 'mounting', /* @ts-ignore */
4544
- ...(ngDevMode ? [{ debugName: "isMounting" }] : /* istanbul ignore next */ []));
4545
- this.isUnmounted = computed(() => this.state() === 'unmounted', /* @ts-ignore */
4546
- ...(ngDevMode ? [{ debugName: "isUnmounted" }] : /* istanbul ignore next */ []));
4547
- this.isUnmounting = computed(() => this.state() === 'unmounting', /* @ts-ignore */
4548
- ...(ngDevMode ? [{ debugName: "isUnmounting" }] : /* istanbul ignore next */ []));
4549
- this.canMount = computed(() => !this.isMounted() && !this.isMounting(), /* @ts-ignore */
4550
- ...(ngDevMode ? [{ debugName: "canMount" }] : /* istanbul ignore next */ []));
4551
- this.canUnmount = computed(() => (this.isMounted() || this.isMounting()) && !this.isUnmounting(), /* @ts-ignore */
4552
- ...(ngDevMode ? [{ debugName: "canUnmount" }] : /* istanbul ignore next */ []));
4553
- this.isHidden = signal(false, /* @ts-ignore */
4554
- ...(ngDevMode ? [{ debugName: "isHidden" }] : /* istanbul ignore next */ []));
4555
- this.isMounted$ = toObservable(this.isMounted);
4556
- this.isMounting$ = toObservable(this.isMounting);
4557
- this.isUnmounted$ = toObservable(this.isUnmounted);
4558
- this.isUnmounting$ = toObservable(this.isUnmounting);
4559
- this.canMount$ = toObservable(this.canMount);
4560
- this.canUnmount$ = toObservable(this.canUnmount);
4561
- this.isHidden$ = toObservable(this.isHidden);
4562
- this.referenceElementDimensions = signalElementDimensions(computed(() => (this.mirrorWidth() ? this.referenceElement() : null)));
4563
- effect(() => {
4564
- const dimensions = this.referenceElementDimensions();
4565
- if (!dimensions || !this.mirrorWidth())
4566
- return;
4567
- this.overlayRef?.updateSize({
4568
- width: this.elementRef.nativeElement.offsetWidth,
4569
- });
4570
- });
4571
- this.destroyRef.onDestroy(() => this.destroy());
4572
- }
4573
- mount(config) {
4574
- if (this.isUnmounting()) {
4575
- if (!this.componentRef) {
4576
- if (isDevMode()) {
4577
- console.warn('AnimatedOverlayDirective: Cannot remount. Component ref is not available.');
4578
- }
4579
- return;
4580
- }
4581
- this.unmountSubscription?.unsubscribe();
4582
- this.unmountSubscription = null;
4583
- this.applyComponentData(config.data);
4584
- this.applyColorProvider(config.colorProvider);
4585
- const lifecycle = this.componentRef.instance.animatedLifecycle();
4586
- if (lifecycle) {
4587
- this.state.set('mounting');
4588
- this.beforeOpened$.next();
4589
- lifecycle.enter();
4590
- lifecycle.state$
4591
- .pipe(filter((s) => s === 'entered'), tap(() => {
4592
- if (this.state() === 'mounting') {
4593
- this.state.set('mounted');
4594
- this.afterOpened$.next();
4595
- }
4596
- }), take(1), takeUntilDestroyed(this.destroyRef))
4597
- .subscribe();
4598
- }
4599
- return this.componentRef.instance;
4600
- }
4601
- if (!this.canMount()) {
4602
- if (isDevMode()) {
4603
- console.warn('AnimatedOverlayDirective: Cannot mount. Component is already mounted or currently mounting.');
4604
- }
4605
- return;
4606
- }
4607
- const { component, providers, data, colorProvider } = config;
4608
- this.state.set('mounting');
4609
- this.beforeOpened$.next();
4610
- const injector = Injector.create({
4611
- parent: this.injector,
4612
- providers: providers ?? [],
4613
- });
4614
- this.overlayRef = this.overlayService.create();
4615
- this.portal = new ComponentPortal(component, this.viewContainerRef, injector);
4616
- this.componentRef = this.overlayRef.attach(this.portal);
4617
- this.applyComponentData(data);
4618
- this.applyColorProvider(colorProvider);
4619
- this.updateOverlaySize();
4620
- this.setupFloatingUI();
4621
- return this.componentRef.instance;
4622
- }
4623
- unmount() {
4624
- if (!this.isMounted() && !this.isMounting() && !this.isUnmounting()) {
4625
- if (isDevMode()) {
4626
- console.warn(`AnimatedOverlayDirective: Cannot unmount. Component is currently ${this.state()}`);
4627
- }
4628
- return;
4629
- }
4630
- if (this.isUnmounting()) {
4631
- return;
4632
- }
4633
- if (!this.componentRef) {
4634
- if (isDevMode()) {
4635
- console.warn('AnimatedOverlayDirective: Cannot unmount. Component ref is not available.');
4636
- }
4637
- return;
4638
- }
4639
- this.state.set('unmounting');
4640
- this.beforeClosed$.next();
4641
- if (this.isHidden()) {
4642
- this.destroy();
4643
- return;
4644
- }
4645
- const lifecycle = this.componentRef.instance.animatedLifecycle();
4646
- if (!lifecycle) {
4647
- this.destroy();
4648
- return;
4649
- }
4650
- lifecycle.leave();
4651
- this.unmountSubscription = lifecycle.state$
4652
- .pipe(filter((s) => s === 'left'), take(1), takeUntilDestroyed(this.destroyRef))
4653
- .subscribe(() => {
4654
- this.unmountSubscription = null;
4655
- this.destroy();
4656
- });
4657
- }
4658
- applyComponentData(data) {
4659
- if (!this.componentRef || !data)
4660
- return;
4661
- Object.assign(this.componentRef.instance, data);
4662
- this.componentRef.changeDetectorRef.markForCheck();
4663
- }
4664
- applyColorProvider(colorProvider) {
4665
- if (!this.componentRef || !colorProvider)
4666
- return;
4667
- this.componentRef.instance.setColorFromProvider(colorProvider);
4668
- }
4669
- updateOverlaySize() {
4670
- if (!this.overlayRef || !this.mirrorWidth())
4671
- return;
4672
- this.overlayRef.updateSize({
4673
- width: this.elementRef.nativeElement.offsetWidth,
4674
- });
4675
- }
4676
- setupFloatingUI() {
4677
- this.zone.runOutsideAngular(() => {
4678
- if (!this.componentRef)
4679
- return;
4680
- const floatingEl = this.componentRef.location.nativeElement;
4681
- const floatingElArrow = this.getFloatingArrow();
4682
- floatingEl.classList.add('et-floating-element');
4683
- const refEl = this.referenceElement();
4684
- const boundary = this.rootBoundary?.value();
4685
- this.floatingElCleanupFn = autoUpdate(refEl, floatingEl, () => {
4686
- this.updateFloatingPosition(floatingEl, refEl, boundary, floatingElArrow);
4687
- });
4688
- this.waitForRenderAndAnimate();
4689
- });
4690
- }
4691
- getFloatingArrow() {
4692
- if (!this.componentRef)
4693
- return null;
4694
- return this.componentRef.location.nativeElement.querySelector('[et-floating-arrow]');
4695
- }
4696
- updateFloatingPosition(floatingEl, refEl, boundary, floatingElArrow) {
4697
- if (!this.componentRef)
4698
- return;
4699
- const middleware = this.buildFloatingMiddleware(floatingEl, boundary, floatingElArrow);
4700
- computePosition(refEl, floatingEl, {
4701
- placement: this.placement(),
4702
- middleware,
4703
- }).then(({ x, y, placement, middlewareData }) => {
4704
- this.applyFloatingStyles(floatingEl, x, y, placement);
4705
- this.applyArrowStyles(floatingEl, middlewareData, floatingElArrow);
4706
- this.handleReferenceHidden(floatingEl, middlewareData);
4707
- });
4708
- }
4709
- buildFloatingMiddleware(floatingEl, boundary, floatingElArrow) {
4710
- const middleware = [];
4711
- const offsetValue = this.offset();
4712
- if (offsetValue !== null) {
4713
- middleware.push(offset(offsetValue));
4714
- }
4715
- middleware.push(flip({
4716
- fallbackPlacements: this.fallbackPlacements() ?? undefined,
4717
- fallbackAxisSideDirection: 'start',
4718
- boundary,
4719
- }));
4720
- if (this.autoResize()) {
4721
- middleware.push(size({
4722
- padding: this.viewportPadding() ?? undefined,
4723
- apply({ availableHeight, availableWidth }) {
4724
- floatingEl.style.setProperty('--et-floating-max-width', `${availableWidth}px`);
4725
- floatingEl.style.setProperty('--et-floating-max-height', `${availableHeight}px`);
4726
- },
4727
- }));
4728
- }
4729
- if (this.shift()) {
4730
- middleware.push(shift({
4731
- limiter: limitShift(),
4732
- padding: this.viewportPadding() ?? undefined,
4733
- boundary,
4734
- }));
4735
- }
4736
- if (floatingElArrow) {
4737
- middleware.push(arrow({ element: floatingElArrow, padding: this.arrowPadding() ?? undefined }));
4738
- }
4739
- if (this.autoHide() || this.autoCloseIfReferenceHidden()) {
4740
- middleware.push(hide({ strategy: 'referenceHidden', boundary }));
4741
- }
4742
- return middleware;
4743
- }
4744
- applyFloatingStyles(floatingEl, x, y, placement) {
4745
- floatingEl.style.setProperty('--et-floating-translate', `translate3d(${x}px, ${y}px, 0)`);
4746
- floatingEl.setAttribute('et-floating-placement', placement);
4747
- }
4748
- applyArrowStyles(floatingEl, middlewareData, floatingElArrow) {
4749
- if (!middlewareData.arrow || !floatingElArrow)
4750
- return;
4751
- const { x: arrowX, y: arrowY } = middlewareData.arrow;
4752
- floatingEl.style.setProperty('--et-floating-arrow-translate', `translate3d(${arrowX ?? 0}px, ${arrowY ?? 0}px, 0)`);
4753
- }
4754
- handleReferenceHidden(floatingEl, middlewareData) {
4755
- if (middlewareData.hide?.referenceHidden) {
4756
- if (this.autoCloseIfReferenceHidden()) {
4757
- this.unmount();
4758
- }
4759
- else {
4760
- floatingEl.classList.add('et-floating-element--hidden');
4761
- this.isHidden.set(true);
4762
- }
4763
- }
4764
- else if (this.autoHide()) {
4765
- floatingEl.classList.remove('et-floating-element--hidden');
4766
- this.isHidden.set(false);
4767
- }
4768
- }
4769
- waitForRenderAndAnimate() {
4770
- nextFrame(() => {
4771
- if (!this.componentRef || this.state() !== 'mounting') {
4772
- return;
4773
- }
4774
- const lifecycle = this.componentRef.instance.animatedLifecycle?.();
4775
- if (!lifecycle) {
4776
- console.error('AnimatedOverlayDirective: The component does not have an AnimatedLifecycleDirective. Please add one to the component.');
4777
- return;
4778
- }
4779
- lifecycle.enter();
4780
- lifecycle.state$
4781
- .pipe(filter((s) => s === 'entered'), tap(() => {
4782
- if (this.state() === 'mounting') {
4783
- this.state.set('mounted');
4784
- this.afterOpened$.next();
4785
- }
4786
- }), take(1), takeUntilDestroyed(this.destroyRef))
4787
- .subscribe();
4788
- });
4789
- }
4790
- destroy() {
4791
- this.zone.runOutsideAngular(() => {
4792
- this.floatingElCleanupFn?.();
4793
- });
4794
- if (this.overlayRef) {
4795
- this.overlayRef.dispose();
4796
- this.overlayRef = null;
4797
- }
4798
- if (this.componentRef) {
4799
- this.componentRef.destroy();
4800
- this.componentRef = null;
4801
- }
4802
- this.state.set('unmounted');
4803
- this.afterClosed$.next();
4804
- }
4805
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: AnimatedOverlayDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
4806
- 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 }); }
4807
- }
4808
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: AnimatedOverlayDirective, decorators: [{
4809
- type: Directive,
4810
- args: [{
4811
- selector: '[etAnimatedOverlay]',
4812
- exportAs: 'etAnimatedOverlay',
4813
- host: {
4814
- class: 'et-animated-overlay',
4815
- },
4816
- }]
4817
- }], 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 }] }] } });
4818
-
4819
4637
  const createFlipAnimationGroup = (config) => {
4820
4638
  const { elements, duration = 250, easing = 'cubic-bezier(0.4, 0, 0.2, 1)', ignoreReducedMotion } = config;
4821
4639
  const flips = elements.map((el) => {
@@ -5159,7 +4977,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
5159
4977
  }]
5160
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"] }] } });
5161
4979
 
5162
- const FOCUSABLE_SELECTOR = [
4980
+ const FOCUSABLE_SELECTOR = /* @__PURE__ */ [
5163
4981
  'a[href]',
5164
4982
  'area[href]',
5165
4983
  'button:not([disabled])',
@@ -5320,6 +5138,56 @@ const resetPositioningStyles = (config, hostElement, paneElement, renderer) => {
5320
5138
  renderer.removeAttribute(paneElement, 'data-overlay-placement');
5321
5139
  setBaseElementStyles(config, hostElement, paneElement, renderer);
5322
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
+ };
5323
5191
  const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, renderer) => {
5324
5192
  // mirrorWidth needs a real element to measure; virtual references fall back to max-content
5325
5193
  const mirrorWidthElement = strategy.mirrorWidth && isHTMLElement(strategy.referenceElement) ? strategy.referenceElement : null;
@@ -5346,32 +5214,41 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
5346
5214
  boundary: strategy.boundary,
5347
5215
  }));
5348
5216
  }
5349
- // 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 -
5350
5218
  // otherwise a cross-axis-shifted pane gets its max size capped to the unshifted leftover space
5351
5219
  if (strategy.autoResize) {
5352
- middleware.push(size({
5353
- padding: strategy.viewportPadding ?? 8,
5354
- apply({ availableHeight, availableWidth }) {
5355
- renderer.setCssProperties(paneElement, {
5356
- '--et-overlay-max-width': `${availableWidth}px`,
5357
- '--et-overlay-max-height': `${availableHeight}px`,
5358
- });
5359
- },
5360
- }));
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
+ }
5361
5232
  }
5362
5233
  if (arrowElement) {
5363
- middleware.push(arrow({
5364
- element: arrowElement,
5365
- // keeps the arrow off a rounded pane corner by default: 12px covers the radius of the
5366
- // built-in boxed panes (--et-overlay-radius). Panes with a larger radius have to pass their
5367
- // own — an arrow whose base sits on the corner arc looks detached from the pane.
5368
- padding: strategy.arrowPadding ?? 12,
5369
- }));
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
+ }
5370
5244
  }
5371
5245
  if (strategy.autoHide || strategy.autoCloseIfReferenceHidden) {
5372
- middleware.push(hide({
5373
- strategy: 'referenceHidden',
5374
- }));
5246
+ const extras = requireMiddlewareExtras('autoHide');
5247
+ if (extras) {
5248
+ middleware.push(extras.hide({
5249
+ strategy: 'referenceHidden',
5250
+ }));
5251
+ }
5375
5252
  }
5376
5253
  computePosition(strategy.referenceElement, paneElement, {
5377
5254
  placement: strategy.placement ?? 'bottom',
@@ -5412,20 +5289,29 @@ const createAnchoredPositionCleanup = (strategy, paneElement, overlayRef, render
5412
5289
  cleanup();
5413
5290
  };
5414
5291
  };
5415
- const setupPositioning = (config, hostElement, paneElement, overlayRef, renderer) => {
5416
- const strategy = config.positionStrategy ?? { kind: 'center' };
5417
- if (strategy.kind === 'anchored') {
5418
- renderer.setStyle(hostElement, {
5419
- pointerEvents: config.hasBackdrop === false ? 'none' : 'auto',
5420
- });
5421
- return createAnchoredPositionCleanup(strategy, paneElement, overlayRef, renderer);
5422
- }
5423
- if (strategy.kind === 'global') {
5424
- applyGlobalPosition(hostElement, paneElement, renderer, strategy);
5425
- return () => undefined;
5426
- }
5427
- applyCenteredPosition(hostElement, paneElement, renderer, strategy);
5428
- 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 });
5429
5315
  };
5430
5316
 
5431
5317
  const createOverlayRuntimeRef = (id, config, elements, requestClose) => {
@@ -5445,13 +5331,13 @@ const createOverlayRuntimeRef = (id, config, elements, requestClose) => {
5445
5331
  elements,
5446
5332
  state: _state.asReadonly(),
5447
5333
  componentInstance: _componentInstance.asReadonly(),
5448
- // Internal — driven by overlay-runtime.ts
5334
+ // Internal - driven by overlay-runtime.ts
5449
5335
  beforeOpenedSubject,
5450
5336
  close(result, source = 'api') {
5451
5337
  if (_state() === 'closing' || _state() === 'closed') {
5452
5338
  return;
5453
5339
  }
5454
- // `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.
5455
5341
  if (source !== 'reference-detached') {
5456
5342
  for (const guard of closeGuards) {
5457
5343
  if (!guard({ result, source })) {
@@ -5461,7 +5347,7 @@ const createOverlayRuntimeRef = (id, config, elements, requestClose) => {
5461
5347
  }
5462
5348
  requestClose(result, source);
5463
5349
  },
5464
- /** 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
5465
5351
  * close it previously vetoed (e.g. after an async confirm resolved). */
5466
5352
  forceClose(result, source = 'api') {
5467
5353
  if (_state() === 'closing' || _state() === 'closed') {
@@ -5528,7 +5414,7 @@ const createOverlayRuntimeRef = (id, config, elements, requestClose) => {
5528
5414
  };
5529
5415
  };
5530
5416
 
5531
- const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
5417
+ const OVERLAY_RUNTIME_DEF = /* @__PURE__ */ defineRootProvider(() => {
5532
5418
  const appRef = inject(ApplicationRef);
5533
5419
  const destroyRef = inject(DestroyRef);
5534
5420
  const document = inject(DOCUMENT);
@@ -5635,7 +5521,7 @@ const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
5635
5521
  hostElement: paneElement,
5636
5522
  bindings: config.bindings ?? [],
5637
5523
  });
5638
- // pane classes are applied after component creation — Angular replaces the host
5524
+ // pane classes are applied after component creation - Angular replaces the host
5639
5525
  // element's class attribute with the component's static host class on creation
5640
5526
  renderer.addClass(paneElement, 'et-overlay-runtime-pane', ...(config.paneClass ?? []));
5641
5527
  appRef.attachView(componentRef.hostView);
@@ -5682,7 +5568,7 @@ const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
5682
5568
  }
5683
5569
  const lifecycle = getAnimatedLifecycle(componentRef);
5684
5570
  animationDebugLog(`runtime ${config.id}`, `close requested (source "${closeEvent.source}", lifecycle state "${lifecycle?.state$.value ?? 'none'}", delegate ${config.animationDelegate?.leave ? 'yes' : 'no'})`);
5685
- // 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
5686
5572
  // overlay down synchronously instead of playing a leave transition from a stale position.
5687
5573
  if (!lifecycle || closeEvent.source === 'reference-detached') {
5688
5574
  destroyMountedOverlay(closeEvent);
@@ -5791,6 +5677,8 @@ const [provideOverlayRuntime, injectOverlayRuntime] = createRootProvider(() => {
5791
5677
  openEntries,
5792
5678
  };
5793
5679
  }, { name: 'OverlayRuntime' });
5680
+ const provideOverlayRuntime = /* @__PURE__ */ toProvideFn(OVERLAY_RUNTIME_DEF);
5681
+ const injectOverlayRuntime = /* @__PURE__ */ toInjectFn(OVERLAY_RUNTIME_DEF);
5794
5682
 
5795
5683
  class NormalizeGameResultTypePipe {
5796
5684
  constructor() {
@@ -6831,7 +6719,7 @@ const toStringBinding = (binding) => {
6831
6719
  return value !== null && value !== undefined ? String(value) : null;
6832
6720
  });
6833
6721
  };
6834
- const createBulkPropertyBinding = (propertyPrefix, applyConfigFn) => {
6722
+ const createBulkPropertyBinding = (_propertyPrefix, applyConfigFn) => {
6835
6723
  return (config) => {
6836
6724
  Object.entries(config).forEach(([key, value]) => {
6837
6725
  if (value !== undefined) {
@@ -6841,11 +6729,202 @@ const createBulkPropertyBinding = (propertyPrefix, applyConfigFn) => {
6841
6729
  };
6842
6730
  };
6843
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
+
6844
6923
  var jsonLd = /*#__PURE__*/Object.freeze({
6845
6924
  __proto__: null
6846
6925
  });
6847
6926
 
6848
- const DEFAULT_MULTI_INSTANCE_RELS = new Set([
6927
+ const DEFAULT_MULTI_INSTANCE_RELS = /* @__PURE__ */ new Set([
6849
6928
  'alternate',
6850
6929
  'icon',
6851
6930
  'apple-touch-icon',
@@ -6858,10 +6937,12 @@ const DEFAULT_MULTI_INSTANCE_RELS = new Set([
6858
6937
  'stylesheet',
6859
6938
  'modulepreload',
6860
6939
  ]);
6861
- const [provideLinkStoreConfig, injectLinkStoreConfig] = createStaticRootProvider({
6940
+ const LINK_STORE_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider({
6862
6941
  multiInstanceRels: DEFAULT_MULTI_INSTANCE_RELS,
6863
6942
  }, { name: 'Link Store Config' });
6864
- 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(() => {
6865
6946
  const document = inject(DOCUMENT$1);
6866
6947
  const config = injectLinkStoreConfig();
6867
6948
  const linksByKey = signal(new Map(), /* @ts-ignore */
@@ -6897,7 +6978,6 @@ const [provideLinkStore, injectLinkStore] = createRootProvider(() => {
6897
6978
  };
6898
6979
  const createLinkElement = (linkConfig) => {
6899
6980
  const link = document.createElement('link');
6900
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
6901
6981
  const { key, allowMultiple, ...attributes } = linkConfig;
6902
6982
  Object.entries(attributes).forEach(([attrKey, value]) => {
6903
6983
  if (value !== undefined && value !== null) {
@@ -6982,6 +7062,8 @@ const [provideLinkStore, injectLinkStore] = createRootProvider(() => {
6982
7062
  };
6983
7063
  return { addLink, removeLink, getLinkKey };
6984
7064
  }, { name: 'Link Store' });
7065
+ const provideLinkStore = /* @__PURE__ */ toProvideFn(LINK_STORE_DEF);
7066
+ const injectLinkStore = /* @__PURE__ */ toInjectFn(LINK_STORE_DEF);
6985
7067
  const applyLinkBinding = (binding) => {
6986
7068
  const linkStore = injectLinkStore();
6987
7069
  const linkId = Symbol('link-tag');
@@ -6991,7 +7073,7 @@ const applyLinkBinding = (binding) => {
6991
7073
  linkStore.addLink(linkId, config);
6992
7074
  }, () => linkStore.removeLink(linkId, currentKey));
6993
7075
  };
6994
- const applyCanonicalBinding = createPropertyBinding((href) => ({ rel: 'canonical', href }), applyLinkBinding);
7076
+ const applyCanonicalBinding = /* @__PURE__ */ createPropertyBinding((href) => ({ rel: 'canonical', href }), applyLinkBinding);
6995
7077
  const applyAlternateBinding = (hreflang, binding) => {
6996
7078
  applyLinkBinding(computed(() => {
6997
7079
  const url = untracked(() => (isSignal(binding) ? binding() : binding));
@@ -7003,8 +7085,8 @@ const applyAlternateLanguagesBindings = (config) => {
7003
7085
  applyAlternateBinding(hreflang, binding);
7004
7086
  });
7005
7087
  };
7006
- const applyPrevBinding = createPropertyBinding((href) => ({ rel: 'prev', href }), applyLinkBinding);
7007
- 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);
7008
7090
  const applyResourceHintsBindings = (config) => {
7009
7091
  if (config.preconnect) {
7010
7092
  createArrayPropertyBinding((href, index) => ({ rel: 'preconnect', href, key: `preconnect:${index}` }), applyLinkBinding)(config.preconnect);
@@ -7020,20 +7102,26 @@ const applyResourceHintsBindings = (config) => {
7020
7102
  }
7021
7103
  };
7022
7104
 
7023
- const [provideTitleConfig, injectTitleConfig] = createStaticRootProvider({
7105
+ const TITLE_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider({
7024
7106
  divider: '|',
7025
7107
  defaultTitle: '',
7026
7108
  transformer: (title) => title,
7027
7109
  }, { name: 'Title Config' });
7028
- 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(() => {
7029
7113
  const config = injectTitleConfig();
7030
7114
  const titleParts = signal(new Map(), /* @ts-ignore */
7031
7115
  ...(ngDevMode ? [{ debugName: "titleParts" }] : /* istanbul ignore next */ []));
7116
+ const titleMarkers = signal(new Map(), /* @ts-ignore */
7117
+ ...(ngDevMode ? [{ debugName: "titleMarkers" }] : /* istanbul ignore next */ []));
7032
7118
  const { currentLocale } = injectLocale();
7033
7119
  const titleService = inject(Title);
7034
7120
  const isRouterInitialized = injectIsRouterInitialized();
7035
- const defaultTitle = config.defaultTitle ?? titleService.getTitle();
7036
- 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(() => {
7037
7125
  const partsMap = titleParts();
7038
7126
  if (!partsMap.size || !isRouterInitialized()) {
7039
7127
  return config.transformer(defaultTitle, currentLocale());
@@ -7057,6 +7145,20 @@ const [provideTitleStore, injectTitleStore] = createRootProvider(() => {
7057
7145
  }
7058
7146
  return finalTextParts.join(` ${config.divider} `);
7059
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 */
7060
7162
  ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
7061
7163
  const addPart = (id, part) => {
7062
7164
  titleParts.update((parts) => new Map(parts).set(id, part));
@@ -7068,19 +7170,41 @@ const [provideTitleStore, injectTitleStore] = createRootProvider(() => {
7068
7170
  return newParts;
7069
7171
  });
7070
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
+ };
7071
7183
  effect(() => {
7072
7184
  const titleText = title();
7073
7185
  titleService.setTitle(titleText);
7074
7186
  });
7075
- return { title, addPart, removePart };
7187
+ return { title, addPart, removePart, addMarker, removeMarker };
7076
7188
  }, { name: 'Title Store' });
7189
+ const provideTitleStore = /* @__PURE__ */ toProvideFn(TITLE_STORE_DEF);
7190
+ const injectTitleStore = /* @__PURE__ */ toInjectFn(TITLE_STORE_DEF);
7077
7191
  const applyHeadTitleBinding = (binding, options) => {
7078
7192
  const titleStore = injectTitleStore();
7079
7193
  const partId = Symbol('title-part');
7080
7194
  applyHeadBinding(binding, (value) => titleStore.addPart(partId, { text: `${value}`, ...options }), () => titleStore.removePart(partId), (value) => value !== null && value !== undefined && value !== '');
7081
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
+ };
7082
7206
 
7083
- const DEFAULT_MULTI_INSTANCE_TAGS = new Set([
7207
+ const DEFAULT_MULTI_INSTANCE_TAGS = /* @__PURE__ */ new Set([
7084
7208
  // Open Graph - Media
7085
7209
  'property="og:image"',
7086
7210
  'property="og:image:url"',
@@ -7149,11 +7273,13 @@ const DEFAULT_MULTI_INSTANCE_TAGS = new Set([
7149
7273
  'name="p:domain_verify"', // Pinterest
7150
7274
  'name="facebook-domain-verification"',
7151
7275
  ]);
7152
- const [provideMetaConfig, injectMetaConfig] = createStaticRootProvider({
7276
+ const META_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider({
7153
7277
  transformer: (content) => content,
7154
7278
  multiInstanceTags: DEFAULT_MULTI_INSTANCE_TAGS,
7155
7279
  }, { name: 'Meta Config' });
7156
- 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(() => {
7157
7283
  const metaService = inject(Meta);
7158
7284
  const config = injectMetaConfig();
7159
7285
  const { currentLocale } = injectLocale();
@@ -7179,7 +7305,6 @@ const [provideMetaStore, injectMetaStore] = createRootProvider(() => {
7179
7305
  if (transformedConfig.content) {
7180
7306
  transformedConfig.content = config.transformer(transformedConfig.content, locale);
7181
7307
  }
7182
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
7183
7308
  const { key, allowMultiple, ...metaConfig } = transformedConfig;
7184
7309
  return metaService.addTag(metaConfig);
7185
7310
  };
@@ -7266,6 +7391,8 @@ const [provideMetaStore, injectMetaStore] = createRootProvider(() => {
7266
7391
  });
7267
7392
  return { addTag, removeTag, getSelector };
7268
7393
  }, { name: 'Meta Store' });
7394
+ const provideMetaStore = /* @__PURE__ */ toProvideFn(META_STORE_DEF);
7395
+ const injectMetaStore = /* @__PURE__ */ toInjectFn(META_STORE_DEF);
7269
7396
  const applyMetaBinding = (binding) => {
7270
7397
  const metaStore = injectMetaStore();
7271
7398
  const tagId = Symbol('meta-tag');
@@ -7275,14 +7402,14 @@ const applyMetaBinding = (binding) => {
7275
7402
  metaStore.addTag(tagId, config);
7276
7403
  }, () => metaStore.removeTag(tagId, currentSelector));
7277
7404
  };
7278
- const applyDescriptionBinding = createPropertyBinding((content) => ({ name: 'description', content }), applyMetaBinding);
7405
+ const applyDescriptionBinding = /* @__PURE__ */ createPropertyBinding((content) => ({ name: 'description', content }), applyMetaBinding);
7279
7406
  const applyKeywordsBinding = (binding) => {
7280
7407
  applyMetaBinding(computed(() => {
7281
7408
  const keywords = untracked(() => (isSignal(binding) ? binding() : binding));
7282
7409
  return keywords && keywords.length > 0 ? { name: 'keywords', content: keywords.join(', ') } : null;
7283
7410
  }));
7284
7411
  };
7285
- const applyAuthorBinding = createPropertyBinding((content) => ({ name: 'author', content }), applyMetaBinding);
7412
+ const applyAuthorBinding = /* @__PURE__ */ createPropertyBinding((content) => ({ name: 'author', content }), applyMetaBinding);
7286
7413
  const applyRobotsBinding = (binding) => {
7287
7414
  applyMetaBinding(computed(() => {
7288
7415
  const config = untracked(() => (isSignal(binding) ? binding() : binding));
@@ -7595,10 +7722,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7595
7722
  }]
7596
7723
  }] });
7597
7724
 
7598
- const [provideStructuredDataConfig, injectStructuredDataConfig] = createStaticRootProvider({
7725
+ const STRUCTURED_DATA_CONFIG_DEF = /* @__PURE__ */ defineStaticRootProvider({
7599
7726
  placement: 'body',
7600
7727
  }, { name: 'Structured Data Config' });
7601
- 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(() => {
7602
7731
  const document = inject(DOCUMENT$1);
7603
7732
  const renderer = injectRenderer();
7604
7733
  const config = injectStructuredDataConfig();
@@ -7615,7 +7744,11 @@ const [provideStructuredDataStore, injectStructuredDataStore] = createRootProvid
7615
7744
  renderer.removeChild(oldScript.parentNode, oldScript);
7616
7745
  }
7617
7746
  const script = renderer.createElement('script');
7618
- 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));
7619
7752
  const target = getTargetElement();
7620
7753
  renderer.appendChild(target, script);
7621
7754
  newScripts.set(id, script);
@@ -7635,6 +7768,8 @@ const [provideStructuredDataStore, injectStructuredDataStore] = createRootProvid
7635
7768
  };
7636
7769
  return { addStructuredData, removeStructuredData };
7637
7770
  }, { name: 'Structured Data Store' });
7771
+ const provideStructuredDataStore = /* @__PURE__ */ toProvideFn(STRUCTURED_DATA_STORE_DEF);
7772
+ const injectStructuredDataStore = /* @__PURE__ */ toInjectFn(STRUCTURED_DATA_STORE_DEF);
7638
7773
  const applyStructuredDataBinding = (binding) => {
7639
7774
  const store = injectStructuredDataStore();
7640
7775
  const scriptId = Symbol('structured-data');
@@ -7684,12 +7819,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7684
7819
 
7685
7820
  /**
7686
7821
  * Cascades hover/active-driven color DOWN to descendants explicitly marked with
7687
- * `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
7688
7823
  * expand a nested control's hit/visual area (e.g. clicking a checkbox's label should light up the
7689
- * 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
7690
7825
  * `ColorInteractiveDirective`.
7691
7826
  *
7692
- * Does nothing unless at least one descendant carries `ColorInteractiveDirective` — check for
7827
+ * Does nothing unless at least one descendant carries `ColorInteractiveDirective` - check for
7693
7828
  * that before adding this. It was mistakenly added to a component with no such descendant once
7694
7829
  * already, where it did nothing but was confusing to read.
7695
7830
  */
@@ -7715,9 +7850,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7715
7850
  * Marks a descendant as NOT counting toward an ancestor's `ColorInteractiveDirective`
7716
7851
  * `:hover`/`:active` reaction (via the `:has()` guard in color-interactive-styles.component.css).
7717
7852
  * Use it for content that's incidentally inside an interactive ancestor but isn't part of what
7718
- * 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.
7719
7854
  *
7720
- * 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
7721
7856
  * has no effect on custom-property inheritance into descendants past the excluded element. If
7722
7857
  * what you actually need is "stop my own cosmetic styling from reacting to a parent's unrelated
7723
7858
  * interaction," this is the wrong tool; that's the class of bug `ColorInteractiveHasFocusDirective`
@@ -7739,7 +7874,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7739
7874
 
7740
7875
  /**
7741
7876
  * Tints `--et-theme-color-primary-*` etc. only when a descendant genuinely has `:focus-visible`
7742
- * (`: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
7743
7878
  * form field's frame) that should show the theme color while its control is focused, but must
7744
7879
  * NOT react to incidental mouse interaction with unrelated nested widgets (a toolbar button, a
7745
7880
  * rendered link) that happen to live inside the same wrapper.
@@ -7751,7 +7886,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7751
7886
  * Deliberately does NOT also carry `ColorInteractiveDirective`'s `et-color-interactive` class:
7752
7887
  * that class's plain `:hover`/`:active` rules locally override the same (inheriting)
7753
7888
  * `--et-theme-color-primary-rgb`, so anything bubbling up from ANYWHERE in the subtree would
7754
- * 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 -
7755
7890
  * exactly the bug this directive's only consumer (form-field) shipped once, before the two
7756
7891
  * behaviors were split apart. If you need the plain hover/active reaction too, add
7757
7892
  * `ColorInteractiveDirective` explicitly and make sure you actually want both.
@@ -7777,13 +7912,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7777
7912
  /**
7778
7913
  * Makes an element react to ITS OWN `:hover`/`:active`/`:focus-visible`/`[disabled]` by tinting
7779
7914
  * `--et-theme-color-primary-*`/`-on-primary-*`/`-ink-*` with the current color theme. Use this on
7780
- * 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.
7781
7916
  *
7782
7917
  * Do NOT apply this to a wrapper that also contains OTHER, independently-interactive elements
7783
7918
  * (e.g. a toolbar with its own buttons, or a form-field's control slot). These are ordinary
7784
7919
  * inheriting CSS custom properties, so a descendant reading them for its own cosmetics (a caret
7785
7920
  * color, a link color) gets swept along whenever ANYTHING inside the wrapper is hovered or
7786
- * 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
7787
7922
  * shipped once already; see ColorInteractiveContainerDirective/ColorInteractiveHasFocusDirective
7788
7923
  * for the shapes that avoid it.
7789
7924
  */
@@ -7811,19 +7946,23 @@ const SURFACE_TYPE = {
7811
7946
  };
7812
7947
  const createCssSurfaceName = (name) => name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`);
7813
7948
  const resolveSurfaceByElevation = (themes, type, elevation) => themes.find((t) => t.type === type && t.elevation === elevation) ?? null;
7814
- const [ɵProvideSurfaceThemes, injectSurfaceThemes] = createStaticProvider(undefined, {
7949
+ const SURFACE_THEMES_DEF = /* @__PURE__ */ defineStaticProvider(undefined, {
7815
7950
  name: 'Surface Themes',
7816
7951
  });
7817
- 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', {
7818
7955
  name: 'Surface Themes Prefix',
7819
7956
  });
7957
+ const ɵProvideSurfaceThemesPrefix = /* @__PURE__ */ toProvideFn(SURFACE_THEMES_PREFIX_DEF);
7958
+ const injectSurfaceThemesPrefix = /* @__PURE__ */ toInjectFn(SURFACE_THEMES_PREFIX_DEF);
7820
7959
  const provideSurfaceThemesWithTailwind4 = (themes, prefix = 'et') => [
7821
7960
  ɵProvideSurfaceThemes(themes),
7822
7961
  ɵProvideSurfaceThemesPrefix(prefix),
7823
7962
  ];
7824
7963
 
7825
7964
  const SURFACE_PROVIDER = new InjectionToken('SurfaceProvider');
7826
- const FORCED_SURFACE_UNSET = Symbol('FORCED_SURFACE_UNSET');
7965
+ const FORCED_SURFACE_UNSET = /* @__PURE__ */ Symbol('FORCED_SURFACE_UNSET');
7827
7966
  class ProvideSurfaceDirective {
7828
7967
  constructor() {
7829
7968
  this.themes = injectSurfaceThemes({ optional: true });
@@ -7922,7 +8061,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
7922
8061
  }], propDecorators: { surface: [{ type: i0.Input, args: [{ isSignal: true, alias: "etProvideSurface", required: false }] }] } });
7923
8062
 
7924
8063
  let uniqueId = 0;
7925
- const [provideSurfaceContextTracker, injectSurfaceContextTracker] = createRootProvider(() => {
8064
+ const SURFACE_CONTEXT_TRACKER_DEF = /* @__PURE__ */ defineRootProvider(() => {
7926
8065
  const entries = signal([], /* @ts-ignore */
7927
8066
  ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
7928
8067
  const topEntry = computed(() => {
@@ -7955,12 +8094,14 @@ const [provideSurfaceContextTracker, injectSurfaceContextTracker] = createRootPr
7955
8094
  };
7956
8095
  return { topType, topElevation, register, surfaceForElement };
7957
8096
  }, { name: 'SurfaceContextTracker' });
8097
+ const provideSurfaceContextTracker = /* @__PURE__ */ toProvideFn(SURFACE_CONTEXT_TRACKER_DEF);
8098
+ const injectSurfaceContextTracker = /* @__PURE__ */ toInjectFn(SURFACE_CONTEXT_TRACKER_DEF);
7958
8099
 
7959
8100
  class AutoSurfaceDirective {
7960
8101
  /**
7961
8102
  * Mark this auto-surface as its overlay's *own painted surface*. An overlay *panel*
7962
8103
  * (menu, select/date/cascader panel, tooltip, toggletip, rich-text-editor popups) is
7963
- * 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 -
7964
8105
  * not stack a level above a parent surface. The elevation is read from the surface-context
7965
8106
  * tracker (the pane it renders into), which is authoritative across the portal boundary;
7966
8107
  * the panel's own declaration injector points back at the trigger location and cannot be
@@ -7988,7 +8129,7 @@ class AutoSurfaceDirective {
7988
8129
  ...(ngDevMode ? [{ debugName: "isOverlaySurface" }] : /* istanbul ignore next */ []));
7989
8130
  // Bumped by the afterEveryRender watcher in the constructor whenever this element's overlay
7990
8131
  // containment changes. surfaceForElement() below does a non-reactive element.contains() read,
7991
- // 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 -
7992
8133
  // otherwise a value cached from before the graft (one elevation too low) sticks forever.
7993
8134
  this.domSettleTick = signal(0, /* @ts-ignore */
7994
8135
  ...(ngDevMode ? [{ debugName: "domSettleTick" }] : /* istanbul ignore next */ []));
@@ -8004,7 +8145,7 @@ class AutoSurfaceDirective {
8004
8145
  const contextElevation = contextProvider?.elevation() ?? null;
8005
8146
  const contextType = contextProvider?.surfaceType() ?? null;
8006
8147
  // An overlay's projected/portaled content keeps the injector of where it was
8007
- // *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
8008
8149
  // injector-derived parent surface is one elevation too low. The surface-context
8009
8150
  // tracker records each open overlay's surface *and* its pane element, so we consult
8010
8151
  // only the overlay whose pane actually contains this element in the DOM (portaling
@@ -8015,7 +8156,7 @@ class AutoSurfaceDirective {
8015
8156
  const overlayType = overlaySurface?.type ?? null;
8016
8157
  // A panel that IS its overlay's own surface (see matchOverlaySurface) must paint exactly the
8017
8158
  // overlay's elevation: the overlay container already resolved it (one above the trigger) and
8018
- // 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
8019
8160
  // points back at the trigger location and disagrees with the pane whenever the trigger itself
8020
8161
  // sits on a surface, which would double-elevate the panel relative to its own pane.
8021
8162
  if (this.isOverlaySurface()) {
@@ -8048,7 +8189,7 @@ class AutoSurfaceDirective {
8048
8189
  ...(ngDevMode ? [{ debugName: "resolvedSurface" }] : /* istanbul ignore next */ []));
8049
8190
  // Projected/portaled auto-surface content (e.g. a select option's avatar) can have its
8050
8191
  // resolvedSurface computed run *before* it is grafted into the overlay pane it visually lives
8051
- // 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.
8052
8193
  // surfaceForElement()'s element.contains() check then returns the wrong (or no) overlay, so it
8053
8194
  // resolves off the declaration injector (the trigger location) one elevation too low. That check
8054
8195
  // is a plain DOM read, not a signal, so nothing re-runs the computed once the element reaches its
@@ -8065,14 +8206,13 @@ class AutoSurfaceDirective {
8065
8206
  this.domSettleTick.update((v) => v + 1);
8066
8207
  }
8067
8208
  else if (el.isConnected && ++stableRenders >= 2) {
8068
- // 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
8069
8210
  settleWatcher.destroy();
8070
8211
  }
8071
8212
  });
8072
8213
  effect(() => {
8073
8214
  const surface = this.resolvedSurface();
8074
8215
  untracked(() => {
8075
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
8076
8216
  setInputSignal(this.ownSurfaceProvider.surface, surface);
8077
8217
  });
8078
8218
  });
@@ -8089,12 +8229,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
8089
8229
  }], ctorParameters: () => [], propDecorators: { surfaceProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "surfaceProvider", required: false }] }] } });
8090
8230
 
8091
8231
  const createCssColorThemeName = (name) => name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`);
8092
- const [ɵProvideColorThemes, injectColorThemes] = createStaticProvider(undefined, {
8232
+ const COLOR_THEMES_DEF = /* @__PURE__ */ defineStaticProvider(undefined, {
8093
8233
  name: 'Color Themes',
8094
8234
  });
8095
- 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', {
8096
8238
  name: 'Themes Prefix',
8097
8239
  });
8240
+ const ɵProvideColorThemesPrefix = /* @__PURE__ */ toProvideFn(COLOR_THEMES_PREFIX_DEF);
8241
+ const injectColorThemesPrefix = /* @__PURE__ */ toInjectFn(COLOR_THEMES_PREFIX_DEF);
8098
8242
  const provideColorThemesWithTailwind4 = (themes, prefix = 'et') => [
8099
8243
  ɵProvideColorThemes(themes),
8100
8244
  ɵProvideColorThemesPrefix(prefix),
@@ -8313,7 +8457,7 @@ const COLOR_PROVIDER = new InjectionToken('ColorProvider');
8313
8457
  * The color provider attached to a bootstrapped root component (e.g. `ProvideColorDirective` in
8314
8458
  * the app component's `hostDirectives`), if any. Detached DOM such as overlay panes can't reach
8315
8459
  * it through element DI, so overlay containers fall back to this when resolving the provider to
8316
- * `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
8317
8461
  * register with `ApplicationRef` once bootstrap completes.
8318
8462
  *
8319
8463
  * @internal
@@ -8327,7 +8471,7 @@ const resolveAppRootColorProvider = (appRef) => {
8327
8471
  }
8328
8472
  return null;
8329
8473
  };
8330
- const FORCED_COLOR_UNSET = Symbol('FORCED_COLOR_UNSET');
8474
+ const FORCED_COLOR_UNSET = /* @__PURE__ */ Symbol('FORCED_COLOR_UNSET');
8331
8475
  class ProvideColorDirective {
8332
8476
  constructor() {
8333
8477
  this.themes = injectColorThemes({ optional: true });
@@ -8347,8 +8491,8 @@ class ProvideColorDirective {
8347
8491
  }, /* @ts-ignore */
8348
8492
  ...(ngDevMode ? [{ debugName: "effectiveColor" }] : /* istanbul ignore next */ []));
8349
8493
  /**
8350
- * The color that actually applies at this provider's location: its own effective color, or —
8351
- * 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.
8352
8496
  * Mirrors the CSS cascade, which falls through a provider's `-color--inherited` scope class;
8353
8497
  * consumers that re-apply context across detached DOM (overlay panes) must use this instead
8354
8498
  * of `effectiveColor` or a passive in-between provider (e.g. a form field's) erases the theme.
@@ -8441,14 +8585,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
8441
8585
  }] });
8442
8586
 
8443
8587
  /**
8444
- * Same shape as `ColorInteractiveDirective` — reacts to its own `:hover`/`:focus-visible`/
8445
- * `: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
8446
8590
  * of the ambient color theme. Use it for chrome-level interaction feedback that shouldn't borrow
8447
8591
  * the page's brand color (a tab trigger's hover state, a toggle button that should look neutral
8448
- * until it's actually active — see `ButtonDirective`'s `mutedUntilPressed`).
8592
+ * until it's actually active - see `ButtonDirective`'s `mutedUntilPressed`).
8449
8593
  *
8450
8594
  * Has no container/exclude/has-focus variants: it has no descendant-cascading CSS at all, so
8451
- * 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 -
8452
8596
  * unlike `ColorInteractiveDirective`, there's no bubbling-leak failure mode to worry about here.
8453
8597
  */
8454
8598
  class SurfaceInteractiveDirective {
@@ -8469,6 +8613,81 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
8469
8613
  }]
8470
8614
  }], ctorParameters: () => [] });
8471
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
+
8472
8691
  const isAbstractControl = (value) => !!value &&
8473
8692
  typeof value === 'object' &&
8474
8693
  typeof value.getRawValue === 'function' &&
@@ -8484,13 +8703,13 @@ const normalizeUnsavedChangesSource = (source) => {
8484
8703
  if (isFieldTree(source)) {
8485
8704
  return {
8486
8705
  value: computed(() => source().value()),
8487
- // 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.
8488
8707
  setValue: (next) => source().value.set(next),
8489
8708
  };
8490
8709
  }
8491
8710
  if (isSignal(source)) {
8492
8711
  const peek = untracked(source);
8493
- // 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).
8494
8713
  if (peek === null || peek === undefined || isFieldTree(peek)) {
8495
8714
  const fieldSignal = source;
8496
8715
  return {
@@ -8515,6 +8734,179 @@ const normalizeUnsavedChangesSource = (source) => {
8515
8734
  'an AbstractControl, or a WritableSignal.');
8516
8735
  };
8517
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
+
8518
8910
  const toBooleanPromise = (result) => {
8519
8911
  if (result instanceof Promise) {
8520
8912
  return result.then(Boolean);
@@ -8529,8 +8921,15 @@ const toBooleanPromise = (result) => {
8529
8921
  * whether the watched form/value differs from it, and runs an async confirm before a discard.
8530
8922
  * Call from an injection context (a field initializer or constructor).
8531
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
+ *
8532
8931
  * Overlay and router flavors (`createOverlayUnsavedChangesGuard`, `createUnsavedChangesGuard`) build
8533
- * 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.
8534
8933
  */
8535
8934
  const createUnsavedChangesTracker = (config) => {
8536
8935
  assertInInjectionContext(createUnsavedChangesTracker);
@@ -8565,11 +8964,23 @@ const createUnsavedChangesTracker = (config) => {
8565
8964
  return !(compareFn ? compareFn(current, defaultValue) : equal(current, defaultValue));
8566
8965
  }, /* @ts-ignore */
8567
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);
8568
8979
  const runCheck = () => {
8569
- if (!untracked(hasChanges)) {
8980
+ if (untracked(_isAbandoned) || !untracked(hasChanges)) {
8570
8981
  return Promise.resolve(true);
8571
8982
  }
8572
- return toBooleanPromise(confirm(untracked(normalized.value)));
8983
+ return coordinator.runCheck((context) => toBooleanPromise(confirm(untracked(normalized.value), context)));
8573
8984
  };
8574
8985
  const refreshDefaultValue = () => _defaultValue.set(untracked(normalized.value));
8575
8986
  const restoreDefaultValue = () => {
@@ -8580,10 +8991,12 @@ const createUnsavedChangesTracker = (config) => {
8580
8991
  };
8581
8992
  return {
8582
8993
  hasChanges,
8994
+ isAbandoned: _isAbandoned.asReadonly(),
8583
8995
  defaultValue: _defaultValue.asReadonly(),
8584
8996
  runCheck,
8585
8997
  refreshDefaultValue,
8586
8998
  restoreDefaultValue,
8999
+ tab,
8587
9000
  };
8588
9001
  };
8589
9002
 
@@ -8606,5 +9019,5 @@ const createUnsavedChangesGuard = (config) => {
8606
9019
  * Generated bundle index. Do not edit.
8607
9020
  */
8608
9021
 
8609
- 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, SWIPE_VELOCITY_WINDOW_MS, 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, createLabels, 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, injectRouterNavigationState, 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, matchesReducedMotion, 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 };
8610
9023
  //# sourceMappingURL=ethlete-core.mjs.map