ng-hub-ui-loading 22.0.0 → 22.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, inject, input, booleanAttribute, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, ApplicationRef, PLATFORM_ID, signal, createComponent, Injectable } from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, input, booleanAttribute, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, ApplicationRef, PLATFORM_ID, signal, untracked, createComponent, Injectable, NgZone, DestroyRef, provideAppInitializer } from '@angular/core';
3
3
  import { resolveHubAccent } from 'ng-hub-ui-utils';
4
4
  import { DOCUMENT, isPlatformBrowser } from '@angular/common';
5
+ import { HttpContextToken, HttpContext } from '@angular/common/http';
6
+ import { finalize } from 'rxjs';
7
+ import { NavigationEnd, NavigationCancel, NavigationError, NavigationSkipped, Router, NavigationStart } from '@angular/router';
5
8
 
6
9
  /**
7
10
  * Neutral defaults applied when an application provides no configuration.
@@ -169,7 +172,16 @@ class HubLoadingService {
169
172
  document = inject(DOCUMENT);
170
173
  platformId = inject(PLATFORM_ID);
171
174
  config = inject(HUB_LOADING_CONFIG);
172
- /** Number of callers currently requesting the overlay. */
175
+ /**
176
+ * Number of callers currently requesting the overlay.
177
+ *
178
+ * Read through `untracked` wherever it steers this service's own logic. A caller is not
179
+ * necessarily outside a reactive context — `hide()` from inside an `effect()` is ordinary
180
+ * code — and a tracked read there subscribes that effect to the counter, so anyone else's
181
+ * `show()` re-runs it and retires a reference it never registered, taking the overlay down
182
+ * while other callers are still waiting. Only `isLoading` reads it tracked, and that one is
183
+ * meant to: it exists to be watched.
184
+ */
173
185
  pending = signal(0, /* @ts-ignore */
174
186
  ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
175
187
  /** Live reference to the mounted overlay; `null` whenever nothing is showing. */
@@ -201,7 +213,7 @@ class HubLoadingService {
201
213
  */
202
214
  hide() {
203
215
  this.pending.update((count) => Math.max(0, count - 1));
204
- if (this.pending() === 0) {
216
+ if (untracked(this.pending) === 0) {
205
217
  this.unmount();
206
218
  }
207
219
  }
@@ -302,11 +314,575 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
302
314
  args: [{ providedIn: 'root' }]
303
315
  }] });
304
316
 
317
+ /**
318
+ * Default trickle curve: large steps early, then progressively smaller ones.
319
+ *
320
+ * The shape is what makes an invented number believable. Moving fast at the start
321
+ * matches the part of a page load that really is fast (the request goes out, the shell
322
+ * responds); slowing near the end matches the part nobody can predict, and leaves room
323
+ * for `complete()` to arrive without the bar having to jump backwards.
324
+ *
325
+ * Exported so a consumer can wrap it rather than rewrite it.
326
+ *
327
+ * @param progress - Current value, 0–100.
328
+ * @returns Amount to add on this tick.
329
+ */
330
+ function hubLoadingBarTrickle(progress) {
331
+ if (progress < 20) {
332
+ return 10;
333
+ }
334
+ if (progress < 50) {
335
+ return 4;
336
+ }
337
+ if (progress < 80) {
338
+ return 2;
339
+ }
340
+ if (progress < 99) {
341
+ return 0.5;
342
+ }
343
+ return 0;
344
+ }
345
+ /**
346
+ * Neutral defaults applied when an application provides no configuration.
347
+ *
348
+ * These are the values documented as each input's default, so overriding the token
349
+ * silently re-bases the whole application without touching a template.
350
+ */
351
+ const HUB_LOADING_BAR_DEFAULT_CONFIG = {
352
+ min: 8,
353
+ max: 99,
354
+ trickleSpeed: 250,
355
+ trickle: true,
356
+ trickleFn: hubLoadingBarTrickle,
357
+ delay: 100,
358
+ completeDelay: 300,
359
+ color: null,
360
+ glow: true,
361
+ ariaLabel: 'Loading'
362
+ };
363
+ /**
364
+ * Resolved defaults shared by `<hub-loading-bar>` and `HubLoadingBarService`.
365
+ *
366
+ * Declared with a root factory so the token is always injectable, even when the
367
+ * application never calls {@link provideHubLoadingBar}.
368
+ */
369
+ const HUB_LOADING_BAR_CONFIG = new InjectionToken('HUB_LOADING_BAR_CONFIG', {
370
+ providedIn: 'root',
371
+ factory: () => HUB_LOADING_BAR_DEFAULT_CONFIG
372
+ });
373
+ /**
374
+ * Registers application-wide loading-bar defaults — the accent, the pacing, the
375
+ * translated label — so individual call sites stay bare.
376
+ *
377
+ * @param config - Values overriding {@link HUB_LOADING_BAR_DEFAULT_CONFIG}; omitted keys keep their default.
378
+ * @returns Environment providers for the application bootstrap.
379
+ */
380
+ function provideHubLoadingBar(config = {}) {
381
+ return makeEnvironmentProviders([
382
+ {
383
+ provide: HUB_LOADING_BAR_CONFIG,
384
+ useValue: { ...HUB_LOADING_BAR_DEFAULT_CONFIG, ...config }
385
+ }
386
+ ]);
387
+ }
388
+
389
+ /**
390
+ * Drives the page-level loading bar: the thin strip under the navbar that says
391
+ * "something is on its way" without pretending to know how long it will take.
392
+ *
393
+ * Three decisions shape the whole service, and each exists because the naive version is
394
+ * worse:
395
+ *
396
+ * - **Reference counting, not a boolean.** A route change and the three requests its
397
+ * page fires are four independent callers. With a boolean, the first one to finish
398
+ * would take the bar down while the other three were still working. The bar completes
399
+ * when the count reaches zero, and {@link completeAll} is the escape hatch for a caller
400
+ * that never balanced its `start()`.
401
+ * - **A grace period before anything is painted.** Work that finishes within `delay`
402
+ * never shows a bar at all. A cached route that flashes a progress bar for 40 ms reads
403
+ * as a glitch, not as speed.
404
+ * - **A trickle that never reaches the end.** Nothing here knows the real percentage, so
405
+ * the bar advances in shrinking steps and stops at `max`. Only `complete()` may show
406
+ * 100, because only `complete()` knows it is true.
407
+ *
408
+ * Timers run outside the Angular zone. Inside it, a 250 ms interval would trigger change
409
+ * detection for the whole application on every tick and — far worse — would keep
410
+ * `ApplicationRef.isStable` false forever, which hangs server-side rendering. The state
411
+ * is signals, so change detection is still scheduled correctly when a value actually
412
+ * changes. On the server no timer is created at all: the counter stays truthful and the
413
+ * rendered HTML carries no bar to mismatch on hydration.
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * private readonly bar = inject(HubLoadingBarService);
418
+ *
419
+ * async import(): Promise<void> {
420
+ * this.bar.start();
421
+ * try {
422
+ * await this.api.import();
423
+ * } finally {
424
+ * this.bar.complete();
425
+ * }
426
+ * }
427
+ * ```
428
+ */
429
+ class HubLoadingBarService {
430
+ config = inject(HUB_LOADING_BAR_CONFIG);
431
+ zone = inject(NgZone);
432
+ platformId = inject(PLATFORM_ID);
433
+ /** Number of callers currently waiting on something. */
434
+ /**
435
+ * How many callers are holding the bar up.
436
+ *
437
+ * Read through `untracked` everywhere it steers this service's own logic. A caller is
438
+ * not necessarily outside a reactive context — Angular does not untrack interceptors,
439
+ * so a request fired from inside an `effect()` runs `start()` inside that effect — and a
440
+ * tracked read there subscribes the caller's effect to this counter. Every later request
441
+ * then re-runs it, which is a request loop with the bar as the feedback path. Only
442
+ * `isActive` reads it tracked, and that one is meant to: it exists to be watched.
443
+ */
444
+ pending = signal(0, /* @ts-ignore */
445
+ ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
446
+ /** Current fill, 0–100. */
447
+ _progress = signal(0, /* @ts-ignore */
448
+ ...(ngDevMode ? [{ debugName: "_progress" }] : /* istanbul ignore next */ []));
449
+ /** Whether the bar is painted right now; false during the grace period. */
450
+ _visible = signal(false, /* @ts-ignore */
451
+ ...(ngDevMode ? [{ debugName: "_visible" }] : /* istanbul ignore next */ []));
452
+ /** Handles of the running timers, so each can be cancelled independently. */
453
+ timers = {
454
+ reveal: null,
455
+ trickle: null,
456
+ complete: null
457
+ };
458
+ /** Current fill, 0–100. Bound by `<hub-loading-bar>`; safe to read during SSR. */
459
+ progress = this._progress.asReadonly();
460
+ /**
461
+ * Whether the bar is on screen. Differs from {@link isActive} at both ends of a cycle:
462
+ * false while the grace period runs, and still true during the completion tail.
463
+ */
464
+ isVisible = this._visible.asReadonly();
465
+ /**
466
+ * Whether any caller is still waiting — the honest "is the page loading?" question,
467
+ * regardless of whether the bar has decided to show itself yet.
468
+ */
469
+ isActive = computed(() => this.pending() > 0, /* @ts-ignore */
470
+ ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
471
+ constructor() {
472
+ // A root service outlives most things, but not a TestBed reset or an HMR reload;
473
+ // an orphan interval would keep ticking against a dead injector.
474
+ inject(DestroyRef).onDestroy(() => this.clearAllTimers());
475
+ }
476
+ /**
477
+ * Registers one caller. The first one starts a cycle; the rest simply join the count.
478
+ */
479
+ start() {
480
+ const wasIdle = untracked(this.pending) === 0;
481
+ this.pending.update((count) => count + 1);
482
+ if (!wasIdle) {
483
+ return;
484
+ }
485
+ if (this.timers.complete !== null) {
486
+ // Interrupting the completion tail. Drop the bar before rewinding it: the
487
+ // stylesheet only transitions the fill while the bar is visible, so rewinding
488
+ // it on screen would animate the progress running backwards.
489
+ this.clearTimer('complete');
490
+ this._visible.set(false);
491
+ }
492
+ this._progress.set(this.config.min);
493
+ if (!isPlatformBrowser(this.platformId)) {
494
+ return;
495
+ }
496
+ // `delay: 0` has to mean "paint now", not "paint on the next macrotask". A router
497
+ // navigation with no async guard settles inside the same task it started in, so a
498
+ // deferred reveal would be cancelled by `complete()` before it ever ran and the bar
499
+ // would be unreachable for anyone who opted out of the grace period.
500
+ if (this.config.delay <= 0) {
501
+ this.reveal();
502
+ return;
503
+ }
504
+ this.schedule('reveal', this.config.delay, () => this.reveal());
505
+ }
506
+ /**
507
+ * Retires one caller. Once none are left the bar runs to 100% and fades away — or, if
508
+ * the grace period swallowed the whole operation, disappears without ever having been
509
+ * seen.
510
+ */
511
+ complete() {
512
+ this.pending.update((count) => Math.max(0, count - 1));
513
+ if (untracked(this.pending) === 0) {
514
+ this.finish();
515
+ }
516
+ }
517
+ /** Drops every pending caller and completes the bar immediately. */
518
+ completeAll() {
519
+ this.pending.set(0);
520
+ this.finish();
521
+ }
522
+ /**
523
+ * Moves the bar to an exact value and reveals it, bypassing the grace period.
524
+ *
525
+ * For work whose real percentage is known — a file upload, a paged import. The value
526
+ * is not capped at `max`, because a caller reporting a true 100 is not guessing;
527
+ * finishing the cycle is still {@link complete}'s job.
528
+ *
529
+ * @param value - Target fill, 0–100. Values outside the range are clamped.
530
+ */
531
+ set(value) {
532
+ this.reveal();
533
+ this._progress.set(Math.min(100, Math.max(0, value)));
534
+ }
535
+ /**
536
+ * Advances the bar and reveals it, bypassing the grace period.
537
+ *
538
+ * @param amount - Step to add. Omitted, the configured trickle curve decides, which is
539
+ * what makes an unknown wait keep moving without ever arriving.
540
+ */
541
+ inc(amount) {
542
+ this.reveal();
543
+ this.advance(amount);
544
+ }
545
+ /**
546
+ * Cancels everything at once: no completion animation, no pending callers, nothing on
547
+ * screen. For an error handler that wants the bar gone rather than finished.
548
+ */
549
+ reset() {
550
+ this.clearAllTimers();
551
+ this.pending.set(0);
552
+ this._visible.set(false);
553
+ this._progress.set(0);
554
+ }
555
+ /** Paints the bar and starts the trickle, if a caller is waiting on one. */
556
+ reveal() {
557
+ this.clearTimer('reveal');
558
+ this._visible.set(true);
559
+ this.startTrickling();
560
+ }
561
+ /**
562
+ * Runs the completion, shared by {@link complete} and {@link completeAll}.
563
+ *
564
+ * The fill is deliberately left at 100 once the bar is hidden. `start()` rewinds it
565
+ * while the bar is invisible, where the stylesheet suppresses the transition, so the
566
+ * next cycle begins from `min` without the previous one being seen to unwind.
567
+ */
568
+ finish() {
569
+ this.clearTimer('reveal');
570
+ this.clearTimer('trickle');
571
+ // Untracked for the same reason as `pending`: `complete()` and `completeAll()` are
572
+ // public and reachable from an effect, and a tracked read here would wake that effect
573
+ // on every later cycle, completing a bar somebody else had just started.
574
+ if (!untracked(this._visible)) {
575
+ this._progress.set(0);
576
+ return;
577
+ }
578
+ this._progress.set(100);
579
+ this.schedule('complete', this.config.completeDelay, () => this._visible.set(false));
580
+ }
581
+ /** Starts the trickle interval, unless it is disabled, already running or unneeded. */
582
+ startTrickling() {
583
+ if (!this.config.trickle || this.timers.trickle !== null || untracked(this.pending) === 0) {
584
+ return;
585
+ }
586
+ if (!isPlatformBrowser(this.platformId)) {
587
+ return;
588
+ }
589
+ this.zone.runOutsideAngular(() => {
590
+ this.timers.trickle = setInterval(() => this.advance(), this.config.trickleSpeed);
591
+ });
592
+ }
593
+ /** Adds one step, capped at `max` so the trickle can never claim to be finished. */
594
+ advance(amount) {
595
+ // Untracked for the same reason as `pending`: `inc()` is public, so this read can happen
596
+ // inside a caller's effect, and each step would then re-run it — a bar that advances
597
+ // itself for as long as the guard allows.
598
+ const current = untracked(this._progress);
599
+ const step = amount ?? this.config.trickleFn(current);
600
+ this._progress.set(Math.min(this.config.max, Math.max(0, current + step)));
601
+ }
602
+ /** Schedules a one-shot timer outside the Angular zone, replacing any previous one. */
603
+ schedule(name, delay, action) {
604
+ this.clearTimer(name);
605
+ this.zone.runOutsideAngular(() => {
606
+ this.timers[name] = setTimeout(() => {
607
+ this.timers[name] = null;
608
+ action();
609
+ }, delay);
610
+ });
611
+ }
612
+ /** Cancels one timer if it is running. Safe to call for a timer that is not. */
613
+ clearTimer(name) {
614
+ const handle = this.timers[name];
615
+ if (handle === null) {
616
+ return;
617
+ }
618
+ this.timers[name] = null;
619
+ // `setInterval` and `setTimeout` share a handle space in both runtimes, so one
620
+ // clear covers the repeating trickle and the two one-shots alike.
621
+ clearTimeout(handle);
622
+ clearInterval(handle);
623
+ }
624
+ /** Cancels every timer; used by `reset()` and on injector teardown. */
625
+ clearAllTimers() {
626
+ this.clearTimer('reveal');
627
+ this.clearTimer('trickle');
628
+ this.clearTimer('complete');
629
+ }
630
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
631
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarService, providedIn: 'root' });
632
+ }
633
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarService, decorators: [{
634
+ type: Injectable,
635
+ args: [{ providedIn: 'root' }]
636
+ }], ctorParameters: () => [] });
637
+
638
+ /**
639
+ * The thin strip that reports page-level progress — the bar under the navbar.
640
+ *
641
+ * By default the component draws whatever {@link HubLoadingBarService} is doing, which is
642
+ * what makes a single `<hub-loading-bar />` in the shell enough for the whole
643
+ * application: the router integration and the HTTP interceptor drive the service, and
644
+ * this element follows. Bind `progress` to take it over instead, for a bar reporting one
645
+ * known quantity — an upload, an import — independently of everything else.
646
+ *
647
+ * The `progress` input therefore has three meanings, and the difference matters:
648
+ * unbound (`undefined`) follows the service; a number drives the bar directly; `null`
649
+ * hides it. This mirrors how `HubLoadingService` already reads `undefined` as "leave it
650
+ * alone" and `null` as "clear it".
651
+ *
652
+ * Note the deliberate gap in the accessibility contract. While the service is trickling,
653
+ * the number on screen is invented — nothing knows the real percentage of a page load —
654
+ * so `aria-valuenow` is withheld, which is exactly how ARIA spells an indeterminate
655
+ * progressbar. Announcing a made-up "43%" would be worse than announcing nothing. The
656
+ * value is published only when a caller has bound a real one.
657
+ *
658
+ * Styles are unencapsulated, like the rest of the library, so a consumer can retheme the
659
+ * bar from a global stylesheet.
660
+ *
661
+ * @example
662
+ * ```html
663
+ * <!-- Hanging off the bottom edge of a navbar, driven by the service -->
664
+ * <nav class="navbar position-relative">
665
+ * …
666
+ * <hub-loading-bar mode="overlay" placement="bottom" />
667
+ * </nav>
668
+ *
669
+ * <!-- Reporting one known quantity -->
670
+ * <hub-loading-bar [progress]="uploaded()" color="success" />
671
+ * ```
672
+ */
673
+ class HubLoadingBarComponent {
674
+ /** Application-wide defaults; also the source of every input's default value. */
675
+ config = inject(HUB_LOADING_BAR_CONFIG);
676
+ /** The shared page-level state this bar renders unless `progress` is bound. */
677
+ service = inject(HubLoadingBarService);
678
+ /**
679
+ * Placement of the strip. `overlay` needs a positioned ancestor to attach to;
680
+ * `fixed` pins it to the viewport at `--hub-loading-bar-offset`.
681
+ */
682
+ mode = input('inline', /* @ts-ignore */
683
+ ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
684
+ /** Edge the `overlay` and `fixed` modes attach to; `inline` ignores it. */
685
+ placement = input('top', /* @ts-ignore */
686
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
687
+ /**
688
+ * Takes the bar over. Leave it unbound to follow {@link HubLoadingBarService}; bind a
689
+ * number (0–100) to drive it directly, or `null` to hide it.
690
+ */
691
+ progress = input(undefined, /* @ts-ignore */
692
+ ...(ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []));
693
+ /**
694
+ * Sweeps a fragment back and forth instead of filling.
695
+ *
696
+ * The honest choice when there is no percentage worth inventing — a long stream, a
697
+ * job with no reported stages.
698
+ */
699
+ indeterminate = input(false, { ...(ngDevMode ? { debugName: "indeterminate" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
700
+ /** Soft glow trailing the leading edge, which is what reads as movement. */
701
+ glow = input(this.config.glow, { ...(ngDevMode ? { debugName: "glow" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
702
+ /**
703
+ * Accent for the fill. Accepts a semantic name (`primary`), a CSS colour literal or a
704
+ * `var(...)` reference — normalised by `resolveHubAccent()` into the single
705
+ * `--hub-loading-bar-accent` slot.
706
+ */
707
+ color = input(this.config.color, /* @ts-ignore */
708
+ ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
709
+ /** Accessible name for the host's `role="progressbar"`. */
710
+ ariaLabel = input(this.config.ariaLabel, /* @ts-ignore */
711
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
712
+ /** Whether a caller has taken the bar over rather than following the service. */
713
+ _manual = computed(() => this.progress() !== undefined, /* @ts-ignore */
714
+ ...(ngDevMode ? [{ debugName: "_manual" }] : /* istanbul ignore next */ []));
715
+ /** Current fill, from whichever source is in charge. */
716
+ _value = computed(() => (this._manual() ? (this.progress() ?? 0) : this.service.progress()), /* @ts-ignore */
717
+ ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
718
+ /** Whether the strip is painted at all. */
719
+ _visible = computed(() => (this._manual() ? this.progress() !== null : this.service.isVisible()), /* @ts-ignore */
720
+ ...(ngDevMode ? [{ debugName: "_visible" }] : /* istanbul ignore next */ []));
721
+ /** Fill as a CSS length, consumed by the stylesheet's single runtime slot. */
722
+ _fill = computed(() => `${this._value()}%`, /* @ts-ignore */
723
+ ...(ngDevMode ? [{ debugName: "_fill" }] : /* istanbul ignore next */ []));
724
+ /**
725
+ * The value published to assistive technology: only ever a real one.
726
+ *
727
+ * `null` removes the attribute, which is how ARIA marks a progressbar indeterminate —
728
+ * the correct answer both while the service trickles an invented number and while the
729
+ * `indeterminate` sweep is running. A bar that is not painted publishes nothing either:
730
+ * the host is `aria-hidden` by then, so a stale value would only ever be misleading.
731
+ */
732
+ _announcedValue = computed(() => this._visible() && this._manual() && !this.indeterminate() ? this._value() : null, /* @ts-ignore */
733
+ ...(ngDevMode ? [{ debugName: "_announcedValue" }] : /* istanbul ignore next */ []));
734
+ /** Mode and placement modifiers; kept as one binding so neither can drop the other. */
735
+ _modifierClasses = computed(() => {
736
+ const classes = [`hub-loading-bar--${this.mode()}`];
737
+ if (this.mode() !== 'inline') {
738
+ classes.push(`hub-loading-bar--${this.placement()}`);
739
+ }
740
+ if (this.indeterminate()) {
741
+ classes.push('hub-loading-bar--indeterminate');
742
+ }
743
+ if (this.glow()) {
744
+ classes.push('hub-loading-bar--glow');
745
+ }
746
+ return classes.join(' ');
747
+ }, /* @ts-ignore */
748
+ ...(ngDevMode ? [{ debugName: "_modifierClasses" }] : /* istanbul ignore next */ []));
749
+ /**
750
+ * Single accent slot consumed by the stylesheet. `null` leaves the binding off
751
+ * entirely, so the token's own cascade default stays in effect.
752
+ */
753
+ _accent = computed(() => resolveHubAccent(this.color()), /* @ts-ignore */
754
+ ...(ngDevMode ? [{ debugName: "_accent" }] : /* istanbul ignore next */ []));
755
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
756
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: HubLoadingBarComponent, isStandalone: true, selector: "hub-loading-bar", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, progress: { classPropertyName: "progress", publicName: "progress", isSignal: true, isRequired: false, transformFunction: null }, indeterminate: { classPropertyName: "indeterminate", publicName: "indeterminate", isSignal: true, isRequired: false, transformFunction: null }, glow: { classPropertyName: "glow", publicName: "glow", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "progressbar", "aria-valuemin": "0", "aria-valuemax": "100" }, properties: { "class": "_modifierClasses()", "class.hub-loading-bar--visible": "_visible()", "attr.aria-label": "ariaLabel()", "attr.aria-valuenow": "_announcedValue()", "attr.aria-hidden": "_visible() ? null : \"true\"", "style.--hub-loading-bar-progress": "_fill()", "style.--hub-loading-bar-accent": "_accent()" }, classAttribute: "hub-loading-bar" }, ngImport: i0, template: "<span class=\"hub-loading-bar__indicator\"></span>\n", styles: [":where(.hub-loading-bar){--hub-loading-bar-accent: var(--hub-sys-color-primary, #0d6efd);--hub-loading-bar-height: 3px;--hub-loading-bar-track-bg: transparent;--hub-loading-bar-radius: 0;--hub-loading-bar-speed: .2s;--hub-loading-bar-fade: .3s;--hub-loading-bar-easing: linear;--hub-loading-bar-glow-color: var(--hub-loading-bar-accent);--hub-loading-bar-glow-blur: 10px;--hub-loading-bar-glow-spread: 1px;--hub-loading-bar-indeterminate-speed: 1.6s;--hub-loading-bar-offset: 0px;--hub-loading-bar-z-index: var(--hub-sys-zindex-sticky, 1020);--hub-loading-bar-progress: 0%;--hub-loading-bar-sweep-direction: 1}[dir=rtl] .hub-loading-bar,.hub-loading-bar[dir=rtl]{--hub-loading-bar-sweep-direction: -1}.hub-loading-bar{display:block;position:relative;overflow:hidden;inline-size:100%;block-size:var(--hub-loading-bar-height);background:var(--hub-loading-bar-track-bg);border-radius:var(--hub-loading-bar-radius);opacity:0;pointer-events:none;transition:opacity var(--hub-loading-bar-fade) ease}.hub-loading-bar--visible{opacity:1}.hub-loading-bar--overlay,.hub-loading-bar--fixed{inset-inline:0;z-index:var(--hub-loading-bar-z-index)}.hub-loading-bar--overlay{position:absolute}.hub-loading-bar--fixed{position:fixed}.hub-loading-bar--top{inset-block-start:var(--hub-loading-bar-offset)}.hub-loading-bar--bottom{inset-block-end:var(--hub-loading-bar-offset)}.hub-loading-bar__indicator{display:block;position:relative;inline-size:var(--hub-loading-bar-progress);block-size:100%;background:var(--hub-loading-bar-accent);border-radius:inherit}.hub-loading-bar--visible .hub-loading-bar__indicator{transition:inline-size var(--hub-loading-bar-speed) var(--hub-loading-bar-easing)}.hub-loading-bar--glow .hub-loading-bar__indicator:after{content:\"\";position:absolute;inset-block:0;inset-inline-end:0;inline-size:var(--hub-loading-bar-height);box-shadow:0 0 var(--hub-loading-bar-glow-blur) var(--hub-loading-bar-glow-spread) var(--hub-loading-bar-glow-color),0 0 calc(var(--hub-loading-bar-glow-blur) / 2) var(--hub-loading-bar-glow-color)}.hub-loading-bar--indeterminate .hub-loading-bar__indicator,.hub-loading-bar--visible.hub-loading-bar--indeterminate .hub-loading-bar__indicator{inline-size:33%;transition:none;animation:hub-loading-bar-sweep var(--hub-loading-bar-indeterminate-speed) ease-in-out infinite}@media(prefers-reduced-motion:reduce){.hub-loading-bar{--hub-loading-bar-indeterminate-speed: 3.2s}}@keyframes hub-loading-bar-sweep{0%{transform:translate(calc(-100% * var(--hub-loading-bar-sweep-direction)))}to{transform:translate(calc(300% * var(--hub-loading-bar-sweep-direction)))}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
757
+ }
758
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarComponent, decorators: [{
759
+ type: Component,
760
+ args: [{ selector: 'hub-loading-bar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
761
+ class: 'hub-loading-bar',
762
+ role: 'progressbar',
763
+ 'aria-valuemin': '0',
764
+ 'aria-valuemax': '100',
765
+ '[class]': '_modifierClasses()',
766
+ '[class.hub-loading-bar--visible]': '_visible()',
767
+ '[attr.aria-label]': 'ariaLabel()',
768
+ '[attr.aria-valuenow]': '_announcedValue()',
769
+ '[attr.aria-hidden]': '_visible() ? null : "true"',
770
+ '[style.--hub-loading-bar-progress]': '_fill()',
771
+ '[style.--hub-loading-bar-accent]': '_accent()'
772
+ }, template: "<span class=\"hub-loading-bar__indicator\"></span>\n", styles: [":where(.hub-loading-bar){--hub-loading-bar-accent: var(--hub-sys-color-primary, #0d6efd);--hub-loading-bar-height: 3px;--hub-loading-bar-track-bg: transparent;--hub-loading-bar-radius: 0;--hub-loading-bar-speed: .2s;--hub-loading-bar-fade: .3s;--hub-loading-bar-easing: linear;--hub-loading-bar-glow-color: var(--hub-loading-bar-accent);--hub-loading-bar-glow-blur: 10px;--hub-loading-bar-glow-spread: 1px;--hub-loading-bar-indeterminate-speed: 1.6s;--hub-loading-bar-offset: 0px;--hub-loading-bar-z-index: var(--hub-sys-zindex-sticky, 1020);--hub-loading-bar-progress: 0%;--hub-loading-bar-sweep-direction: 1}[dir=rtl] .hub-loading-bar,.hub-loading-bar[dir=rtl]{--hub-loading-bar-sweep-direction: -1}.hub-loading-bar{display:block;position:relative;overflow:hidden;inline-size:100%;block-size:var(--hub-loading-bar-height);background:var(--hub-loading-bar-track-bg);border-radius:var(--hub-loading-bar-radius);opacity:0;pointer-events:none;transition:opacity var(--hub-loading-bar-fade) ease}.hub-loading-bar--visible{opacity:1}.hub-loading-bar--overlay,.hub-loading-bar--fixed{inset-inline:0;z-index:var(--hub-loading-bar-z-index)}.hub-loading-bar--overlay{position:absolute}.hub-loading-bar--fixed{position:fixed}.hub-loading-bar--top{inset-block-start:var(--hub-loading-bar-offset)}.hub-loading-bar--bottom{inset-block-end:var(--hub-loading-bar-offset)}.hub-loading-bar__indicator{display:block;position:relative;inline-size:var(--hub-loading-bar-progress);block-size:100%;background:var(--hub-loading-bar-accent);border-radius:inherit}.hub-loading-bar--visible .hub-loading-bar__indicator{transition:inline-size var(--hub-loading-bar-speed) var(--hub-loading-bar-easing)}.hub-loading-bar--glow .hub-loading-bar__indicator:after{content:\"\";position:absolute;inset-block:0;inset-inline-end:0;inline-size:var(--hub-loading-bar-height);box-shadow:0 0 var(--hub-loading-bar-glow-blur) var(--hub-loading-bar-glow-spread) var(--hub-loading-bar-glow-color),0 0 calc(var(--hub-loading-bar-glow-blur) / 2) var(--hub-loading-bar-glow-color)}.hub-loading-bar--indeterminate .hub-loading-bar__indicator,.hub-loading-bar--visible.hub-loading-bar--indeterminate .hub-loading-bar__indicator{inline-size:33%;transition:none;animation:hub-loading-bar-sweep var(--hub-loading-bar-indeterminate-speed) ease-in-out infinite}@media(prefers-reduced-motion:reduce){.hub-loading-bar{--hub-loading-bar-indeterminate-speed: 3.2s}}@keyframes hub-loading-bar-sweep{0%{transform:translate(calc(-100% * var(--hub-loading-bar-sweep-direction)))}to{transform:translate(calc(300% * var(--hub-loading-bar-sweep-direction)))}}\n"] }]
773
+ }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], progress: [{ type: i0.Input, args: [{ isSignal: true, alias: "progress", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }], glow: [{ type: i0.Input, args: [{ isSignal: true, alias: "glow", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
774
+
775
+ /**
776
+ * Marks a request as invisible to the loading bar.
777
+ *
778
+ * The escape hatch is not a nicety. A poll on a timer, a heartbeat, an autosave — any
779
+ * request the reader did not ask for — would otherwise hold the bar open forever, and a
780
+ * progress bar that never finishes is worse than none.
781
+ */
782
+ const HUB_LOADING_BAR_SKIP = new HttpContextToken(() => false);
783
+ /**
784
+ * Builds the `HttpContext` that hides one request from the loading bar.
785
+ *
786
+ * @param context - Existing context to extend; a fresh one by default.
787
+ * @returns The context, with the skip flag set.
788
+ *
789
+ * @example
790
+ * ```typescript
791
+ * this.http.get('/api/notifications', { context: withoutHubLoadingBar() });
792
+ * ```
793
+ */
794
+ function withoutHubLoadingBar(context = new HttpContext()) {
795
+ return context.set(HUB_LOADING_BAR_SKIP, true);
796
+ }
797
+ /**
798
+ * Holds the loading bar open for the lifetime of every HTTP request.
799
+ *
800
+ * The bar's reference counter is what makes this safe to combine with the router
801
+ * integration and with hand-written `start()` calls: six parallel requests are six
802
+ * callers, and the bar completes when the last of them does, not the first.
803
+ *
804
+ * `finalize` is the balancing point rather than a `tap` on success, because it also fires
805
+ * when the request errors and when the caller unsubscribes — a cancelled typeahead is the
806
+ * commonest way a naive interceptor strands the bar at 90%.
807
+ *
808
+ * Opt a request out with {@link withoutHubLoadingBar}.
809
+ *
810
+ * @example
811
+ * ```typescript
812
+ * provideHttpClient(withInterceptors([hubLoadingBarInterceptor]))
813
+ * ```
814
+ */
815
+ const hubLoadingBarInterceptor = (req, next) => {
816
+ if (req.context.get(HUB_LOADING_BAR_SKIP)) {
817
+ return next(req);
818
+ }
819
+ const bar = inject(HubLoadingBarService);
820
+ bar.start();
821
+ return next(req).pipe(finalize(() => bar.complete()));
822
+ };
823
+
824
+ /** Every way a navigation can stop, successful or not. All four must release the bar. */
825
+ function isNavigationSettled(event) {
826
+ return (event instanceof NavigationEnd ||
827
+ event instanceof NavigationCancel ||
828
+ event instanceof NavigationError ||
829
+ event instanceof NavigationSkipped);
830
+ }
831
+ /**
832
+ * Drives the loading bar from router navigation, which is the "page is loading" the bar
833
+ * is named after: it starts when a navigation begins and completes when it settles —
834
+ * including when it is cancelled by a guard or fails, because a bar left running after a
835
+ * rejected navigation is a bar that never goes away.
836
+ *
837
+ * Navigations are tracked with a flag rather than by pairing events one-to-one. The
838
+ * router's event sequence varies with configuration (a blocking initial navigation, a
839
+ * redirect, a skipped same-URL navigation), and the flag guarantees exactly one
840
+ * `start()` / `complete()` pair per navigation whatever order the events arrive in — a
841
+ * missed `NavigationStart` during bootstrap can no longer leave an unmatched `complete()`
842
+ * decrementing somebody else's count.
843
+ *
844
+ * Combine with {@link hubLoadingBarInterceptor} when routes fetch their own data: the
845
+ * navigation settles as soon as the component is created, so without the interceptor the
846
+ * bar finishes while the page is still empty.
847
+ *
848
+ * @returns Environment providers for the application bootstrap.
849
+ *
850
+ * @example
851
+ * ```typescript
852
+ * bootstrapApplication(AppComponent, {
853
+ * providers: [provideRouter(routes), provideHubLoadingBarRouter()]
854
+ * });
855
+ * ```
856
+ */
857
+ function provideHubLoadingBarRouter() {
858
+ return makeEnvironmentProviders([
859
+ provideAppInitializer(() => {
860
+ const router = inject(Router);
861
+ const bar = inject(HubLoadingBarService);
862
+ let navigating = false;
863
+ const subscription = router.events.subscribe((event) => {
864
+ if (event instanceof NavigationStart) {
865
+ if (!navigating) {
866
+ navigating = true;
867
+ bar.start();
868
+ }
869
+ return;
870
+ }
871
+ if (isNavigationSettled(event) && navigating) {
872
+ navigating = false;
873
+ bar.complete();
874
+ }
875
+ });
876
+ inject(DestroyRef).onDestroy(() => subscription.unsubscribe());
877
+ })
878
+ ]);
879
+ }
880
+
305
881
  /** Public API surface of ng-hub-ui-loading. */
306
882
 
307
883
  /**
308
884
  * Generated bundle index. Do not edit.
309
885
  */
310
886
 
311
- export { HUB_LOADING_CONFIG, HUB_LOADING_DEFAULT_CONFIG, HubLoadingComponent, HubLoadingService, provideHubLoading };
887
+ export { HUB_LOADING_BAR_CONFIG, HUB_LOADING_BAR_DEFAULT_CONFIG, HUB_LOADING_BAR_SKIP, HUB_LOADING_CONFIG, HUB_LOADING_DEFAULT_CONFIG, HubLoadingBarComponent, HubLoadingBarService, HubLoadingComponent, HubLoadingService, hubLoadingBarInterceptor, hubLoadingBarTrickle, provideHubLoading, provideHubLoadingBar, provideHubLoadingBarRouter, withoutHubLoadingBar };
312
888
  //# sourceMappingURL=ng-hub-ui-loading.mjs.map