@mohamedatia/fly-design-system 2.7.0 → 2.7.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,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, ChangeDetectionStrategy, Component, Pipe, DOCUMENT, ElementRef, input, output, HostListener, ViewChild, EventEmitter, DestroyRef, Output, Input, forwardRef, Injector, viewChild, effect, afterNextRender, ViewEncapsulation, model, Directive } from '@angular/core';
2
+ import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, DestroyRef, ChangeDetectionStrategy, Component, Pipe, DOCUMENT, ElementRef, input, output, HostListener, ViewChild, EventEmitter, Output, Input, forwardRef, Injector, viewChild, effect, afterNextRender, ViewEncapsulation, model, Directive } from '@angular/core';
3
3
  import * as i1$1 from '@angular/common';
4
4
  import { isPlatformBrowser, NgComponentOutlet, CommonModule, DOCUMENT as DOCUMENT$1 } from '@angular/common';
5
5
  import { Router, NavigationEnd } from '@angular/router';
6
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
7
  import { of, ReplaySubject, Subject } from 'rxjs';
7
8
  import * as i1 from '@angular/forms';
8
9
  import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
9
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
10
10
  import { switchMap, debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
11
11
  import { HttpClient, HttpEventType } from '@angular/common/http';
12
12
  import Cropper from 'cropperjs';
@@ -550,17 +550,40 @@ class FlyRemoteRouter {
550
550
  * ```
551
551
  */
552
552
  params = computed(() => this.matchedRoute()?.params ?? {}, ...(ngDevMode ? [{ debugName: "params" }] : /* istanbul ignore next */ []));
553
+ destroyRef = inject(DestroyRef);
553
554
  constructor() {
554
555
  if (!this.isEmbedded && this.router) {
555
- // Seed with the current standalone URL so consumers can read `segments()`
556
- // synchronously during initial render, before any NavigationEnd fires.
556
+ // Standalone: seed from Angular Router and track NavigationEnd events so
557
+ // browser back/forward (which the host Router handles) keep segments() current.
557
558
  this._url.set(this.router.url);
558
- this.router.events.subscribe(event => {
559
+ this.router.events
560
+ .pipe(takeUntilDestroyed(this.destroyRef))
561
+ .subscribe(event => {
559
562
  if (event instanceof NavigationEnd) {
560
563
  this._url.set(event.urlAfterRedirects);
561
564
  }
562
565
  });
563
566
  }
567
+ else if (this.isEmbedded) {
568
+ // Embedded: seed from current browser location on initial load.
569
+ if (typeof window !== 'undefined') {
570
+ this._url.set(window.location.pathname || '/');
571
+ // Listen for browser back/forward so segments() stays in sync with
572
+ // history entries pushed by navigate() / navigateByUrl().
573
+ const onPopState = (event) => {
574
+ const state = event.state;
575
+ // Prefer the URL we stashed in the history state; fall back to pathname.
576
+ const url = state?.flyRemoteUrl ?? window.location.pathname ?? '/';
577
+ this._url.set(url.startsWith('/') ? url : '/' + url);
578
+ };
579
+ window.addEventListener('popstate', onPopState);
580
+ // Defensive cleanup — the service is providedIn: 'root' so this fires
581
+ // only when the whole Angular app is destroyed, but it keeps things tidy.
582
+ this.destroyRef.onDestroy(() => {
583
+ window.removeEventListener('popstate', onPopState);
584
+ });
585
+ }
586
+ }
564
587
  }
565
588
  /**
566
589
  * Navigate to a route, identified by an array of commands as you would pass
@@ -569,9 +592,11 @@ class FlyRemoteRouter {
569
592
  */
570
593
  navigate(commands, extras) {
571
594
  if (!this.isEmbedded && this.router) {
595
+ // Standalone: Angular Router owns history — pushState semantics are the default.
572
596
  return this.router.navigate(commands, extras);
573
597
  }
574
- this._url.set(this.buildUrl(commands));
598
+ const url = this.buildUrl(commands);
599
+ this._pushEmbedded(url);
575
600
  return Promise.resolve(true);
576
601
  }
577
602
  /**
@@ -579,9 +604,10 @@ class FlyRemoteRouter {
579
604
  */
580
605
  navigateByUrl(url, extras) {
581
606
  if (!this.isEmbedded && this.router) {
607
+ // Standalone: Angular Router owns history — pushState semantics are the default.
582
608
  return this.router.navigateByUrl(url, extras);
583
609
  }
584
- this._url.set(url.startsWith('/') ? url : '/' + url);
610
+ this._pushEmbedded(url.startsWith('/') ? url : '/' + url);
585
611
  return Promise.resolve(true);
586
612
  }
587
613
  /**
@@ -590,16 +616,26 @@ class FlyRemoteRouter {
590
616
  * so the browser's back stack is respected.
591
617
  */
592
618
  back() {
593
- if (!this.isEmbedded) {
594
- if (typeof history !== 'undefined')
595
- history.back();
596
- return;
619
+ // Both modes: delegate to the browser's history stack. In embedded mode we
620
+ // now push real history entries in navigate() / navigateByUrl(), so
621
+ // history.back() triggers popstate and the listener updates segments().
622
+ if (typeof history !== 'undefined') {
623
+ history.back();
597
624
  }
598
- const segs = [...this.segments()];
599
- if (segs.length === 0)
600
- return;
601
- segs.pop();
602
- this._url.set('/' + segs.join('/'));
625
+ }
626
+ /**
627
+ * In embedded mode: push a real browser history entry (so back/forward work)
628
+ * and update the internal signal synchronously.
629
+ *
630
+ * The state object carries `flyRemoteUrl` so the popstate listener can
631
+ * restore the exact URL without relying on window.location.pathname (which
632
+ * could be the shell's route, not the remote's logical URL).
633
+ */
634
+ _pushEmbedded(url) {
635
+ if (typeof history !== 'undefined') {
636
+ history.pushState({ flyRemoteUrl: url }, '', url);
637
+ }
638
+ this._url.set(url);
603
639
  }
604
640
  buildUrl(commands) {
605
641
  const parts = commands
@@ -616,6 +652,161 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
616
652
  args: [{ providedIn: 'root' }]
617
653
  }], ctorParameters: () => [] });
618
654
 
655
+ /**
656
+ * fly-remote-styles — Shell-layer CSS loader for Native Federation remotes.
657
+ *
658
+ * Problem
659
+ * -------
660
+ * Federated remotes only ship JS chunks via Native Federation — their global
661
+ * `styles.css` (or its hashed equivalent) never loads in the shell, so any
662
+ * global CSS selector (e.g. `.circles-overlay`) silently breaks in embedded
663
+ * mode.
664
+ *
665
+ * Solution
666
+ * --------
667
+ * On first mount of each remote, the shell calls `loadRemoteStyles(appId,
668
+ * remoteBaseUrl)`. This function:
669
+ * 1. Derives the remote's `index.html` URL from `remoteBaseUrl`.
670
+ * 2. Fetches it (one network round-trip per remote, browser-cached thereafter).
671
+ * 3. Parses the HTML with DOMParser to extract the first
672
+ * `<link rel="stylesheet">` — the hashed production stylesheet
673
+ * (e.g. `styles-ABC123.css`). This is Strategy (b): index.html discovery.
674
+ * 4. Injects `<link rel="stylesheet" data-fly-app="<appId>">` into
675
+ * `document.head`. Idempotent: if an identical href is already present,
676
+ * the call is a no-op. If the href differs (hot upgrade), it is replaced.
677
+ *
678
+ * Why strategy (b)?
679
+ * -----------------
680
+ * (a) Unhashed `styles.css` — requires remote build config changes (out of scope).
681
+ * (b) Parse remote `index.html` — one tiny fetch per remote; browser caches it;
682
+ * works today without any remote changes.
683
+ * (c) `stylesUrl` in manifest/remoteEntry.json — cleanest long-term; requires
684
+ * protocol change to every remote's CI pipeline (out of scope for Wave A1).
685
+ *
686
+ * Unload decision
687
+ * ---------------
688
+ * `unloadRemoteStyles` is exported for symmetry but should NOT be called on
689
+ * normal window close. Keeping styles loaded across remounts avoids FOUC
690
+ * flicker when the user reopens the same app. The stylesheet is a few KB; the
691
+ * memory cost is negligible. Only call `unloadRemoteStyles` if you are certain
692
+ * the remote will never be opened again in this session (e.g. licence revoke).
693
+ *
694
+ * Caveats
695
+ * -------
696
+ * - CORS: the remote's dev/prod server must serve `index.html` with a
697
+ * permissive `Access-Control-Allow-Origin` header (or be same-origin via
698
+ * the YARP gateway). The fetch uses `credentials: 'omit'` to avoid
699
+ * credential-carrying preflights.
700
+ * - Race on rapid mount/unmount: if `loadRemoteStyles` is called a second time
701
+ * for the same appId while the first fetch is still in-flight, the second
702
+ * call joins the same in-flight Promise (idempotency guard at fetch level).
703
+ * - Angular hashing: Angular's production build hashes the stylesheet filename.
704
+ * DOMParser picks the first `<link rel="stylesheet">` in `<head>`, which is
705
+ * the single global stylesheet Angular emits. If a remote emits multiple
706
+ * stylesheets, only the first is loaded — acceptable for Wave A1.
707
+ */
708
+ /** Per-appId cache of the resolved stylesheet href (or `null` if not found). */
709
+ const _resolvedHref = new Map();
710
+ /** In-flight fetch promises keyed by appId — prevents duplicate fetches. */
711
+ const _inFlight = new Map();
712
+ /**
713
+ * Discovers the hashed stylesheet href from the remote's `index.html`.
714
+ * Returns `null` if no stylesheet `<link>` is found or the fetch fails.
715
+ */
716
+ async function _discoverStylesheetHref(remoteBaseUrl) {
717
+ const indexUrl = remoteBaseUrl.replace(/\/$/, '') + '/index.html';
718
+ try {
719
+ const res = await fetch(indexUrl, {
720
+ credentials: 'omit',
721
+ cache: 'default',
722
+ });
723
+ if (!res.ok)
724
+ return null;
725
+ const html = await res.text();
726
+ const doc = new DOMParser().parseFromString(html, 'text/html');
727
+ // Find the first stylesheet link in <head> — Angular emits exactly one.
728
+ const link = doc.head.querySelector('link[rel="stylesheet"]');
729
+ if (!link?.href)
730
+ return null;
731
+ // `link.href` from DOMParser is resolved relative to the parser's base,
732
+ // which defaults to `about:blank`. We get the raw `href` attribute instead.
733
+ const rawHref = link.getAttribute('href') ?? '';
734
+ if (!rawHref)
735
+ return null;
736
+ // If the remote emits an absolute URL, use it as-is; otherwise resolve
737
+ // against remoteBaseUrl.
738
+ if (rawHref.startsWith('http://') || rawHref.startsWith('https://') || rawHref.startsWith('//')) {
739
+ return rawHref;
740
+ }
741
+ return remoteBaseUrl.replace(/\/$/, '') + '/' + rawHref.replace(/^\//, '');
742
+ }
743
+ catch {
744
+ return null;
745
+ }
746
+ }
747
+ /**
748
+ * Injects the remote's stylesheet into `document.head`.
749
+ *
750
+ * @param appId - Stable identifier for the remote app (matches `DesktopApp.id`).
751
+ * @param remoteBaseUrl - Base URL of the remote, e.g. `https://circles.example.com`
752
+ * or `http://localhost:7202`. Must NOT include `/remoteEntry.json`.
753
+ *
754
+ * The call is idempotent:
755
+ * - If a `<link data-fly-app="appId">` with the same href already exists → no-op.
756
+ * - If the href differs (hot upgrade) → existing link is replaced.
757
+ * - If no stylesheet is found in `index.html` → no-op (logged as a warning).
758
+ */
759
+ async function loadRemoteStyles(appId, remoteBaseUrl) {
760
+ if (typeof document === 'undefined')
761
+ return; // SSR guard
762
+ // Kick off or join an in-flight fetch for this appId.
763
+ let fetchPromise = _inFlight.get(appId);
764
+ if (!fetchPromise) {
765
+ if (_resolvedHref.has(appId)) {
766
+ // Already resolved in a previous call — skip the fetch.
767
+ fetchPromise = Promise.resolve(_resolvedHref.get(appId) ?? null);
768
+ }
769
+ else {
770
+ fetchPromise = _discoverStylesheetHref(remoteBaseUrl);
771
+ _inFlight.set(appId, fetchPromise);
772
+ }
773
+ }
774
+ const href = await fetchPromise;
775
+ _inFlight.delete(appId);
776
+ _resolvedHref.set(appId, href);
777
+ if (!href) {
778
+ console.warn(`[FlyOS] loadRemoteStyles: no stylesheet found in ${remoteBaseUrl}/index.html for appId="${appId}"`);
779
+ return;
780
+ }
781
+ const existing = document.head.querySelector(`link[data-fly-app="${CSS.escape(appId)}"]`);
782
+ if (existing) {
783
+ if (existing.getAttribute('href') === href)
784
+ return; // identical — no-op
785
+ // Hot upgrade: replace href.
786
+ existing.setAttribute('href', href);
787
+ return;
788
+ }
789
+ const link = document.createElement('link');
790
+ link.rel = 'stylesheet';
791
+ link.setAttribute('data-fly-app', appId);
792
+ link.href = href;
793
+ document.head.appendChild(link);
794
+ }
795
+ /**
796
+ * Removes the injected stylesheet for `appId` from `document.head`.
797
+ *
798
+ * NOTE: Prefer NOT calling this on normal window close — keeping styles loaded
799
+ * prevents FOUC flicker when the user reopens the same app. Call only if you
800
+ * are certain the remote will not be reopened in this session.
801
+ */
802
+ function unloadRemoteStyles(appId) {
803
+ if (typeof document === 'undefined')
804
+ return; // SSR guard
805
+ const link = document.head.querySelector(`link[data-fly-app="${CSS.escape(appId)}"]`);
806
+ link?.parentNode?.removeChild(link);
807
+ _resolvedHref.delete(appId);
808
+ }
809
+
619
810
  /**
620
811
  * Outlet for FlyOS-embedded routing. Renders the component associated with the
621
812
  * first matching route in the consumer's `FLY_REMOTE_ROUTES` table, reacting to
@@ -4028,5 +4219,5 @@ const AUDIENCE_ERROR_CODES = {
4028
4219
  * Generated bundle index. Do not edit.
4029
4220
  */
4030
4221
 
4031
- export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentCommandRegistry, AgentDropRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DialogResult, FLY_LOCALE_CATALOG, FLY_REMOTE_ROUTES, FLY_THEME_MODE_IDS, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlyThemeService, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, matchFlyRoutePattern, normalizeFlyTheme, trimAgentPayload, trimAgentString, utf8ByteLength, validateAgentPayload };
4222
+ export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentCommandRegistry, AgentDropRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DialogResult, FLY_LOCALE_CATALOG, FLY_REMOTE_ROUTES, FLY_THEME_MODE_IDS, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlyThemeService, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, loadRemoteStyles, matchFlyRoutePattern, normalizeFlyTheme, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload };
4032
4223
  //# sourceMappingURL=mohamedatia-fly-design-system.mjs.map