ng-hub-ui-loading 22.0.0 → 22.1.0

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