@gears-frontx/react 0.2.0-alpha.2 → 0.2.0-alpha.3

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.
package/dist/index.d.cts CHANGED
@@ -409,15 +409,23 @@ interface ApiStreamResult<TEvent> {
409
409
  * Manages the status lifecycle for a single SSE stream connection.
410
410
  *
411
411
  * **Status values**
412
- * - `'idle'` — no connection is being attempted. This is the initial status
413
- * before the first connect effect runs, and it is ordinarily the status
414
- * while `enabled` is `false`. Consumers must not assume `'idle'` means "never
412
+ * - `'idle'` — no connection is being attempted. This is the status whenever
413
+ * `enabled` is `false`, including on mount: a hook that mounts disabled
414
+ * never reports anything but `'idle'` until `enabled` flips to `true`. A
415
+ * hook that mounts enabled skips `'idle'` entirely — the status is set
416
+ * during render, so its very first render already reports `'connecting'`.
417
+ * Consumers must not assume `'idle'` means "never
415
418
  * connected" — a stream that is disabled after having connected returns to
416
419
  * `'idle'` too, and `data`/`events` from the earlier connection are not
417
420
  * cleared by that transition alone (they are cleared by mode/key changes,
418
421
  * see below).
419
- * - `'connecting'` — `descriptor.connect()` has been called and its promise
420
- * has not yet settled.
422
+ * - `'connecting'` — a connection attempt is committed to but has not settled.
423
+ * It is published during the render that turns the stream on, which is
424
+ * before `descriptor.connect()` runs: the call is made from an effect after
425
+ * that render commits. So `'connecting'` means "this stream is being
426
+ * opened", not "the connect promise is pending" — for one render the two
427
+ * differ, and a consumer that reads the status as proof a request is already
428
+ * in flight is reading it too strongly.
421
429
  * - `'connected'` — set both when `connect()` resolves (unless a disconnect
422
430
  * was requested while it was pending) and, independently, whenever the
423
431
  * `onEvent` callback delivers a new event. Because every event re-asserts
@@ -425,20 +433,33 @@ interface ApiStreamResult<TEvent> {
425
433
  * - `'disconnected'` — reached via any of: the descriptor's `onComplete`
426
434
  * callback firing (the stream ended normally), `connect()` rejecting while
427
435
  * a disconnect was requested concurrently, or the manual `disconnect()`
428
- * function being called. `disconnect()` has no `enabled` guard: called
429
- * while the stream is disabled it still sets `'disconnected'`, which
430
- * persists until the next connect-effect run.
436
+ * function being called. `disconnect()` has no `enabled` guard, but calling
437
+ * it while the stream is disabled is not observable: the stored status moves
438
+ * to `'disconnected'` while the published one stays `'idle'`, and flipping
439
+ * `enabled` back to `true` sets `'connecting'` during that render, so the
440
+ * stored `'disconnected'` is overwritten before it is ever returned.
431
441
  * - `'error'` — `connect()` rejected and no disconnect was requested while it
432
442
  * was pending. `error` holds the rejection, coerced to an `Error` if it
433
443
  * wasn't one already.
434
444
  *
435
445
  * **Transitions**
436
- * - Mount / `enabled` becomes `true` / `descriptor.key` or `mode` changes:
437
- * `data`, `events`, and `error` are reset, then status goes to
438
- * `'connecting'` and `connect()` is called.
439
- * - `enabled` becomes `false` (or starts `false`): status is forced to
440
- * `'idle'` and `connect()` is never called; no other field is reset by
441
- * this transition alone.
446
+ * - Mounting enabled, `enabled` becoming `true`, or `descriptor.key` / `mode`
447
+ * changing while `enabled` is `true`: `data`, `events`, and `error` are reset
448
+ * and status is set to `'connecting'` synchronously during render (not from
449
+ * an effect); `connect()` is then called from an effect that runs after that
450
+ * render commits.
451
+ * - `descriptor.key` or `mode` changing while `enabled` is `false`: `data`,
452
+ * `events`, and `error` are reset all the same, since that reset is not gated
453
+ * on `enabled`, but the status write is - so no `'connecting'` is written and
454
+ * the published status stays `'idle'`; `connect()` is not called. The skipped
455
+ * `'connecting'` is not owed later either: the change counts as handled when
456
+ * it happens, so what publishes `'connecting'` is the subsequent flip of
457
+ * `enabled` to `true`, once, for whichever key and mode are in force by then.
458
+ * - `enabled` becomes `false` (or starts `false`): the published status is
459
+ * `'idle'` for as long as `enabled` stays `false` — it is derived from
460
+ * `enabled` rather than written, so nothing a prior connection stored can
461
+ * surface through it — and `connect()` is never called; no other field is
462
+ * reset by this transition alone.
442
463
  * - Unmount, `descriptor.key` change, `mode` change, or `enabled` flipping to
443
464
  * `false` while connected or connecting: the previous connection is torn
444
465
  * down via `descriptor.disconnect()` once its `connect()` promise settles
@@ -755,13 +776,23 @@ declare function useRegisteredPackages(): string[];
755
776
  *
756
777
  * Styling strategy:
757
778
  * 1. adoptHostStylesIntoShadowRoot() clones all host <style> and <link> into the
758
- * shadow root, bringing the full compiled Tailwind CSS (including MFE utilities,
759
- * since the host's content paths cover src/mfe_packages/**).
779
+ * front of the shadow root, bringing the full compiled Tailwind CSS (including
780
+ * MFE utilities, since the host's content paths cover src/mfe_packages/**).
781
+ * Front, not back, so the MFE's own stylesheets outrank them - see the method.
760
782
  * 2. injectBaseResets() adds box-model resets and :host defaults that aren't part
761
- * of Tailwind's compiled output but are needed for consistent rendering.
783
+ * of Tailwind's compiled output but are needed for consistent rendering, and
784
+ * positions them at the end of the shell-owned head - after the adopted block
785
+ * and the isolation style, ahead of the MFE's own CSS.
762
786
  * 3. Subclasses may override initializeStyles() to inject additional CSS that is
763
787
  * not covered by the host stylesheet (e.g., MFE-specific @font-face rules).
764
788
  *
789
+ * Every mount therefore leaves the same sequence - adopted host block, shadow
790
+ * isolation, base resets, then the MFE's stylesheets - so the shell's CSS is
791
+ * context and the MFE wins every specificity tie against all three shell blocks.
792
+ * Steps 1 and 2 both position what they own rather than appending beside it,
793
+ * because a remounted container arrives with its shadow root intact and the MFE's
794
+ * own stylesheet removed and re-appended around the remount - see each method.
795
+ *
765
796
  * Theme CSS variables are delivered via CSS inheritance from :root (Shadow DOM)
766
797
  * or via MountManager injection (iframe). MFE lifecycles do NOT need to subscribe
767
798
  * to theme changes or call applyThemeToShadowRoot.
@@ -778,12 +809,83 @@ declare abstract class ThemeAwareReactLifecycle implements MfeEntryLifecycle<Chi
778
809
  /**
779
810
  * Copy all inline <style> and <link rel="stylesheet"> from the host document
780
811
  * into the shadow root so that Tailwind and component styles apply inside the MFE.
812
+ *
813
+ * The clones are inserted ahead of everything already in the shadow root, never
814
+ * appended, because adopted host styles are context rather than authority: where
815
+ * they and the MFE's own CSS declare the same property at equal specificity, the
816
+ * MFE has to win. Appending inverted that. `MfeHandlerMF` injects the MFE's
817
+ * compiled stylesheet into the shadow root before it calls mount, so an appended
818
+ * clone of the shell's Tailwind preflight - whose
819
+ * `button, [type='button'], [type='reset'], [type='submit']` rule sets
820
+ * `background-color: transparent` at specificity (0,1,0) - tied with
821
+ * `@gears-frontx/ui-kit`'s single-class button rule and won on document order,
822
+ * leaving every kit Button transparent until it was hovered.
823
+ *
824
+ * Inserting at the front rather than pushing MFE styles to the back is what makes
825
+ * the invariant hold for stylesheets that appear after mount too - a lazily
826
+ * imported component's CSS module, or whatever initializeStyles() adds - since
827
+ * those land behind the adopted block by construction.
828
+ *
829
+ * A cascade layer around the adopted CSS was the alternative and is rejected:
830
+ * layered rules lose to unlayered ones at *any* specificity, so a host utility
831
+ * class would stop overriding an MFE element selector. Document order keeps
832
+ * specificity in charge and only settles the ties.
833
+ *
834
+ * The block replaces its predecessor instead of stacking on it, the same
835
+ * upsert shape `injectStylesheet` and `injectCssVariables` use in
836
+ * `@gears-frontx/mfes`' shadow utilities, widened from one id-keyed node to a
837
+ * marked block. Replacement is required rather than tidy: `createShadowRoot`
838
+ * hands back an existing `element.shadowRoot` instead of attaching a new one,
839
+ * so remounting the same container runs this method again on a shadow root
840
+ * that already holds a full adopted block, and appending would grow it once
841
+ * per mount with no bound. It does not save re-resolving the cloned `<link>`s -
842
+ * every mount clones them afresh and the browser resolves each clone; what
843
+ * replacement buys is that the count stays at one block instead of one per
844
+ * mount.
845
+ *
846
+ * The block is rebuilt from the host head rather than left in place, because
847
+ * the head is not fixed: a lazily imported chunk adds stylesheets to it after
848
+ * the first mount, and the second mount has to pick those up.
849
+ *
850
+ * The fresh block goes in before the previous one comes out. Removing first
851
+ * leaves the shadow root with no host CSS at all, and everything between that
852
+ * removal and the insertion is synchronous work in this same task - so the gap
853
+ * never reaches a paint, but any code that reads computed styles from the tree
854
+ * in between sees an unstyled one. Inserting first costs one moment with two
855
+ * blocks present, which changes nothing: the old block is a clone of the same
856
+ * host head and loses every tie to the new one on document order anyway.
781
857
  */
782
858
  protected adoptHostStylesIntoShadowRoot(shadowRoot: ShadowRoot): void;
783
859
  /**
784
860
  * Box-model resets and :host defaults needed inside every shadow root.
785
861
  * These aren't part of Tailwind's compiled output but are required for
786
862
  * consistent rendering across browsers.
863
+ *
864
+ * Reuses the node a previous mount left behind, exactly as `injectStylesheet`
865
+ * does in `@gears-frontx/mfes`' shadow utilities: a remount runs against a
866
+ * shadow root that `createShadowRoot` handed back intact, and appending a
867
+ * second copy would add one node per mount forever.
868
+ *
869
+ * Reuse alone is not enough, which is why the node is also positioned on every
870
+ * mount rather than left where the previous one sat. These resets are context
871
+ * like the adopted block is - `border-color: currentColor` on `*` is a 0-0-0
872
+ * declaration that beats an MFE preflight's own 0-0-0 reset purely on document
873
+ * order - so they have to stay ahead of the MFE's CSS, and where they land is
874
+ * not stable on its own. `MfeHandlerMF.wrapLifecycleWithStylesheets` removes the
875
+ * MFE's stylesheet on unmount and appends a fresh one before the next mount, so
876
+ * a node merely left in place ends up ahead of that stylesheet on a remount and
877
+ * behind it on the first mount: two different cascades for one component.
878
+ * Anchoring it to the end of the shell-owned head gives every mount the same
879
+ * order - adopted block, isolation, resets, MFE - which is also the order the
880
+ * adoption method's invariant is written against.
881
+ *
882
+ * With no shell-owned node to anchor to, the resets go to the front. That is the
883
+ * plain-`Element` container: no adopted block, no isolation style, and the MFE
884
+ * stylesheet already appended by the handler, so the front is again the only
885
+ * position that leaves the MFE winning ties.
886
+ *
887
+ * Looked up by attribute-selector form rather than `getElementById`, which
888
+ * `ShadowRoot` has but `Element` does not, and this hook takes either.
787
889
  */
788
890
  private injectBaseResets;
789
891
  /**
package/dist/index.d.ts CHANGED
@@ -409,15 +409,23 @@ interface ApiStreamResult<TEvent> {
409
409
  * Manages the status lifecycle for a single SSE stream connection.
410
410
  *
411
411
  * **Status values**
412
- * - `'idle'` — no connection is being attempted. This is the initial status
413
- * before the first connect effect runs, and it is ordinarily the status
414
- * while `enabled` is `false`. Consumers must not assume `'idle'` means "never
412
+ * - `'idle'` — no connection is being attempted. This is the status whenever
413
+ * `enabled` is `false`, including on mount: a hook that mounts disabled
414
+ * never reports anything but `'idle'` until `enabled` flips to `true`. A
415
+ * hook that mounts enabled skips `'idle'` entirely — the status is set
416
+ * during render, so its very first render already reports `'connecting'`.
417
+ * Consumers must not assume `'idle'` means "never
415
418
  * connected" — a stream that is disabled after having connected returns to
416
419
  * `'idle'` too, and `data`/`events` from the earlier connection are not
417
420
  * cleared by that transition alone (they are cleared by mode/key changes,
418
421
  * see below).
419
- * - `'connecting'` — `descriptor.connect()` has been called and its promise
420
- * has not yet settled.
422
+ * - `'connecting'` — a connection attempt is committed to but has not settled.
423
+ * It is published during the render that turns the stream on, which is
424
+ * before `descriptor.connect()` runs: the call is made from an effect after
425
+ * that render commits. So `'connecting'` means "this stream is being
426
+ * opened", not "the connect promise is pending" — for one render the two
427
+ * differ, and a consumer that reads the status as proof a request is already
428
+ * in flight is reading it too strongly.
421
429
  * - `'connected'` — set both when `connect()` resolves (unless a disconnect
422
430
  * was requested while it was pending) and, independently, whenever the
423
431
  * `onEvent` callback delivers a new event. Because every event re-asserts
@@ -425,20 +433,33 @@ interface ApiStreamResult<TEvent> {
425
433
  * - `'disconnected'` — reached via any of: the descriptor's `onComplete`
426
434
  * callback firing (the stream ended normally), `connect()` rejecting while
427
435
  * a disconnect was requested concurrently, or the manual `disconnect()`
428
- * function being called. `disconnect()` has no `enabled` guard: called
429
- * while the stream is disabled it still sets `'disconnected'`, which
430
- * persists until the next connect-effect run.
436
+ * function being called. `disconnect()` has no `enabled` guard, but calling
437
+ * it while the stream is disabled is not observable: the stored status moves
438
+ * to `'disconnected'` while the published one stays `'idle'`, and flipping
439
+ * `enabled` back to `true` sets `'connecting'` during that render, so the
440
+ * stored `'disconnected'` is overwritten before it is ever returned.
431
441
  * - `'error'` — `connect()` rejected and no disconnect was requested while it
432
442
  * was pending. `error` holds the rejection, coerced to an `Error` if it
433
443
  * wasn't one already.
434
444
  *
435
445
  * **Transitions**
436
- * - Mount / `enabled` becomes `true` / `descriptor.key` or `mode` changes:
437
- * `data`, `events`, and `error` are reset, then status goes to
438
- * `'connecting'` and `connect()` is called.
439
- * - `enabled` becomes `false` (or starts `false`): status is forced to
440
- * `'idle'` and `connect()` is never called; no other field is reset by
441
- * this transition alone.
446
+ * - Mounting enabled, `enabled` becoming `true`, or `descriptor.key` / `mode`
447
+ * changing while `enabled` is `true`: `data`, `events`, and `error` are reset
448
+ * and status is set to `'connecting'` synchronously during render (not from
449
+ * an effect); `connect()` is then called from an effect that runs after that
450
+ * render commits.
451
+ * - `descriptor.key` or `mode` changing while `enabled` is `false`: `data`,
452
+ * `events`, and `error` are reset all the same, since that reset is not gated
453
+ * on `enabled`, but the status write is - so no `'connecting'` is written and
454
+ * the published status stays `'idle'`; `connect()` is not called. The skipped
455
+ * `'connecting'` is not owed later either: the change counts as handled when
456
+ * it happens, so what publishes `'connecting'` is the subsequent flip of
457
+ * `enabled` to `true`, once, for whichever key and mode are in force by then.
458
+ * - `enabled` becomes `false` (or starts `false`): the published status is
459
+ * `'idle'` for as long as `enabled` stays `false` — it is derived from
460
+ * `enabled` rather than written, so nothing a prior connection stored can
461
+ * surface through it — and `connect()` is never called; no other field is
462
+ * reset by this transition alone.
442
463
  * - Unmount, `descriptor.key` change, `mode` change, or `enabled` flipping to
443
464
  * `false` while connected or connecting: the previous connection is torn
444
465
  * down via `descriptor.disconnect()` once its `connect()` promise settles
@@ -755,13 +776,23 @@ declare function useRegisteredPackages(): string[];
755
776
  *
756
777
  * Styling strategy:
757
778
  * 1. adoptHostStylesIntoShadowRoot() clones all host <style> and <link> into the
758
- * shadow root, bringing the full compiled Tailwind CSS (including MFE utilities,
759
- * since the host's content paths cover src/mfe_packages/**).
779
+ * front of the shadow root, bringing the full compiled Tailwind CSS (including
780
+ * MFE utilities, since the host's content paths cover src/mfe_packages/**).
781
+ * Front, not back, so the MFE's own stylesheets outrank them - see the method.
760
782
  * 2. injectBaseResets() adds box-model resets and :host defaults that aren't part
761
- * of Tailwind's compiled output but are needed for consistent rendering.
783
+ * of Tailwind's compiled output but are needed for consistent rendering, and
784
+ * positions them at the end of the shell-owned head - after the adopted block
785
+ * and the isolation style, ahead of the MFE's own CSS.
762
786
  * 3. Subclasses may override initializeStyles() to inject additional CSS that is
763
787
  * not covered by the host stylesheet (e.g., MFE-specific @font-face rules).
764
788
  *
789
+ * Every mount therefore leaves the same sequence - adopted host block, shadow
790
+ * isolation, base resets, then the MFE's stylesheets - so the shell's CSS is
791
+ * context and the MFE wins every specificity tie against all three shell blocks.
792
+ * Steps 1 and 2 both position what they own rather than appending beside it,
793
+ * because a remounted container arrives with its shadow root intact and the MFE's
794
+ * own stylesheet removed and re-appended around the remount - see each method.
795
+ *
765
796
  * Theme CSS variables are delivered via CSS inheritance from :root (Shadow DOM)
766
797
  * or via MountManager injection (iframe). MFE lifecycles do NOT need to subscribe
767
798
  * to theme changes or call applyThemeToShadowRoot.
@@ -778,12 +809,83 @@ declare abstract class ThemeAwareReactLifecycle implements MfeEntryLifecycle<Chi
778
809
  /**
779
810
  * Copy all inline <style> and <link rel="stylesheet"> from the host document
780
811
  * into the shadow root so that Tailwind and component styles apply inside the MFE.
812
+ *
813
+ * The clones are inserted ahead of everything already in the shadow root, never
814
+ * appended, because adopted host styles are context rather than authority: where
815
+ * they and the MFE's own CSS declare the same property at equal specificity, the
816
+ * MFE has to win. Appending inverted that. `MfeHandlerMF` injects the MFE's
817
+ * compiled stylesheet into the shadow root before it calls mount, so an appended
818
+ * clone of the shell's Tailwind preflight - whose
819
+ * `button, [type='button'], [type='reset'], [type='submit']` rule sets
820
+ * `background-color: transparent` at specificity (0,1,0) - tied with
821
+ * `@gears-frontx/ui-kit`'s single-class button rule and won on document order,
822
+ * leaving every kit Button transparent until it was hovered.
823
+ *
824
+ * Inserting at the front rather than pushing MFE styles to the back is what makes
825
+ * the invariant hold for stylesheets that appear after mount too - a lazily
826
+ * imported component's CSS module, or whatever initializeStyles() adds - since
827
+ * those land behind the adopted block by construction.
828
+ *
829
+ * A cascade layer around the adopted CSS was the alternative and is rejected:
830
+ * layered rules lose to unlayered ones at *any* specificity, so a host utility
831
+ * class would stop overriding an MFE element selector. Document order keeps
832
+ * specificity in charge and only settles the ties.
833
+ *
834
+ * The block replaces its predecessor instead of stacking on it, the same
835
+ * upsert shape `injectStylesheet` and `injectCssVariables` use in
836
+ * `@gears-frontx/mfes`' shadow utilities, widened from one id-keyed node to a
837
+ * marked block. Replacement is required rather than tidy: `createShadowRoot`
838
+ * hands back an existing `element.shadowRoot` instead of attaching a new one,
839
+ * so remounting the same container runs this method again on a shadow root
840
+ * that already holds a full adopted block, and appending would grow it once
841
+ * per mount with no bound. It does not save re-resolving the cloned `<link>`s -
842
+ * every mount clones them afresh and the browser resolves each clone; what
843
+ * replacement buys is that the count stays at one block instead of one per
844
+ * mount.
845
+ *
846
+ * The block is rebuilt from the host head rather than left in place, because
847
+ * the head is not fixed: a lazily imported chunk adds stylesheets to it after
848
+ * the first mount, and the second mount has to pick those up.
849
+ *
850
+ * The fresh block goes in before the previous one comes out. Removing first
851
+ * leaves the shadow root with no host CSS at all, and everything between that
852
+ * removal and the insertion is synchronous work in this same task - so the gap
853
+ * never reaches a paint, but any code that reads computed styles from the tree
854
+ * in between sees an unstyled one. Inserting first costs one moment with two
855
+ * blocks present, which changes nothing: the old block is a clone of the same
856
+ * host head and loses every tie to the new one on document order anyway.
781
857
  */
782
858
  protected adoptHostStylesIntoShadowRoot(shadowRoot: ShadowRoot): void;
783
859
  /**
784
860
  * Box-model resets and :host defaults needed inside every shadow root.
785
861
  * These aren't part of Tailwind's compiled output but are required for
786
862
  * consistent rendering across browsers.
863
+ *
864
+ * Reuses the node a previous mount left behind, exactly as `injectStylesheet`
865
+ * does in `@gears-frontx/mfes`' shadow utilities: a remount runs against a
866
+ * shadow root that `createShadowRoot` handed back intact, and appending a
867
+ * second copy would add one node per mount forever.
868
+ *
869
+ * Reuse alone is not enough, which is why the node is also positioned on every
870
+ * mount rather than left where the previous one sat. These resets are context
871
+ * like the adopted block is - `border-color: currentColor` on `*` is a 0-0-0
872
+ * declaration that beats an MFE preflight's own 0-0-0 reset purely on document
873
+ * order - so they have to stay ahead of the MFE's CSS, and where they land is
874
+ * not stable on its own. `MfeHandlerMF.wrapLifecycleWithStylesheets` removes the
875
+ * MFE's stylesheet on unmount and appends a fresh one before the next mount, so
876
+ * a node merely left in place ends up ahead of that stylesheet on a remount and
877
+ * behind it on the first mount: two different cascades for one component.
878
+ * Anchoring it to the end of the shell-owned head gives every mount the same
879
+ * order - adopted block, isolation, resets, MFE - which is also the order the
880
+ * adoption method's invariant is written against.
881
+ *
882
+ * With no shell-owned node to anchor to, the resets go to the front. That is the
883
+ * plain-`Element` container: no adopted block, no isolation style, and the MFE
884
+ * stylesheet already appended by the handler, so the front is again the only
885
+ * position that leaves the MFE winning ties.
886
+ *
887
+ * Looked up by attribute-selector form rather than `getElementById`, which
888
+ * `ShadowRoot` has but `Element` does not, and this hook takes either.
787
889
  */
788
890
  private injectBaseResets;
789
891
  /**
package/dist/index.js CHANGED
@@ -1089,6 +1089,9 @@ function useRegisteredPackages() {
1089
1089
  // src/mfe/ThemeAwareReactLifecycle.tsx
1090
1090
  import { createRoot } from "react-dom/client";
1091
1091
  import { jsx as jsx4 } from "react/jsx-runtime";
1092
+ var ADOPTED_HOST_STYLE_ATTR = "data-frontx-adopted-host-style";
1093
+ var BASE_RESETS_STYLE_ID = "__frontx-base-resets__";
1094
+ var SHELL_OWNED_SHADOW_STYLE_IDS = ["__frontx-shadow-isolation__", "__frontx-css-variables__"];
1092
1095
  function resolveProviderMountOptions(app, bridge, mountContext) {
1093
1096
  const extensionId = mountContext?.extensionId;
1094
1097
  const domainId = mountContext?.domainId;
@@ -1143,27 +1146,108 @@ var ThemeAwareReactLifecycle = class {
1143
1146
  /**
1144
1147
  * Copy all inline <style> and <link rel="stylesheet"> from the host document
1145
1148
  * into the shadow root so that Tailwind and component styles apply inside the MFE.
1149
+ *
1150
+ * The clones are inserted ahead of everything already in the shadow root, never
1151
+ * appended, because adopted host styles are context rather than authority: where
1152
+ * they and the MFE's own CSS declare the same property at equal specificity, the
1153
+ * MFE has to win. Appending inverted that. `MfeHandlerMF` injects the MFE's
1154
+ * compiled stylesheet into the shadow root before it calls mount, so an appended
1155
+ * clone of the shell's Tailwind preflight - whose
1156
+ * `button, [type='button'], [type='reset'], [type='submit']` rule sets
1157
+ * `background-color: transparent` at specificity (0,1,0) - tied with
1158
+ * `@gears-frontx/ui-kit`'s single-class button rule and won on document order,
1159
+ * leaving every kit Button transparent until it was hovered.
1160
+ *
1161
+ * Inserting at the front rather than pushing MFE styles to the back is what makes
1162
+ * the invariant hold for stylesheets that appear after mount too - a lazily
1163
+ * imported component's CSS module, or whatever initializeStyles() adds - since
1164
+ * those land behind the adopted block by construction.
1165
+ *
1166
+ * A cascade layer around the adopted CSS was the alternative and is rejected:
1167
+ * layered rules lose to unlayered ones at *any* specificity, so a host utility
1168
+ * class would stop overriding an MFE element selector. Document order keeps
1169
+ * specificity in charge and only settles the ties.
1170
+ *
1171
+ * The block replaces its predecessor instead of stacking on it, the same
1172
+ * upsert shape `injectStylesheet` and `injectCssVariables` use in
1173
+ * `@gears-frontx/mfes`' shadow utilities, widened from one id-keyed node to a
1174
+ * marked block. Replacement is required rather than tidy: `createShadowRoot`
1175
+ * hands back an existing `element.shadowRoot` instead of attaching a new one,
1176
+ * so remounting the same container runs this method again on a shadow root
1177
+ * that already holds a full adopted block, and appending would grow it once
1178
+ * per mount with no bound. It does not save re-resolving the cloned `<link>`s -
1179
+ * every mount clones them afresh and the browser resolves each clone; what
1180
+ * replacement buys is that the count stays at one block instead of one per
1181
+ * mount.
1182
+ *
1183
+ * The block is rebuilt from the host head rather than left in place, because
1184
+ * the head is not fixed: a lazily imported chunk adds stylesheets to it after
1185
+ * the first mount, and the second mount has to pick those up.
1186
+ *
1187
+ * The fresh block goes in before the previous one comes out. Removing first
1188
+ * leaves the shadow root with no host CSS at all, and everything between that
1189
+ * removal and the insertion is synchronous work in this same task - so the gap
1190
+ * never reaches a paint, but any code that reads computed styles from the tree
1191
+ * in between sees an unstyled one. Inserting first costs one moment with two
1192
+ * blocks present, which changes nothing: the old block is a clone of the same
1193
+ * host head and loses every tie to the new one on document order anyway.
1146
1194
  */
1147
1195
  adoptHostStylesIntoShadowRoot(shadowRoot) {
1148
- const styleElements = document.head.querySelectorAll("style");
1149
- styleElements.forEach((el) => {
1150
- const clone = document.createElement("style");
1151
- clone.textContent = el.textContent ?? "";
1152
- shadowRoot.appendChild(clone);
1153
- });
1154
- const linkElements = document.head.querySelectorAll('link[rel="stylesheet"]');
1155
- linkElements.forEach((el) => {
1156
- const clone = el.cloneNode(true);
1157
- shadowRoot.appendChild(clone);
1196
+ const supersededStyles = Array.from(shadowRoot.querySelectorAll(`[${ADOPTED_HOST_STYLE_ATTR}]`));
1197
+ const adoptedStyles = document.createDocumentFragment();
1198
+ const hostStyleNodes = document.head.querySelectorAll('style, link[rel="stylesheet"]');
1199
+ hostStyleNodes.forEach((el) => {
1200
+ if (el instanceof HTMLStyleElement) {
1201
+ const clone = document.createElement("style");
1202
+ clone.textContent = el.textContent ?? "";
1203
+ clone.setAttribute(ADOPTED_HOST_STYLE_ATTR, "");
1204
+ adoptedStyles.appendChild(clone);
1205
+ return;
1206
+ }
1207
+ const linkClone = el.cloneNode(true);
1208
+ linkClone.setAttribute(ADOPTED_HOST_STYLE_ATTR, "");
1209
+ adoptedStyles.appendChild(linkClone);
1158
1210
  });
1211
+ shadowRoot.insertBefore(adoptedStyles, shadowRoot.firstChild);
1212
+ supersededStyles.forEach((el) => el.remove());
1159
1213
  }
1160
1214
  /**
1161
1215
  * Box-model resets and :host defaults needed inside every shadow root.
1162
1216
  * These aren't part of Tailwind's compiled output but are required for
1163
1217
  * consistent rendering across browsers.
1218
+ *
1219
+ * Reuses the node a previous mount left behind, exactly as `injectStylesheet`
1220
+ * does in `@gears-frontx/mfes`' shadow utilities: a remount runs against a
1221
+ * shadow root that `createShadowRoot` handed back intact, and appending a
1222
+ * second copy would add one node per mount forever.
1223
+ *
1224
+ * Reuse alone is not enough, which is why the node is also positioned on every
1225
+ * mount rather than left where the previous one sat. These resets are context
1226
+ * like the adopted block is - `border-color: currentColor` on `*` is a 0-0-0
1227
+ * declaration that beats an MFE preflight's own 0-0-0 reset purely on document
1228
+ * order - so they have to stay ahead of the MFE's CSS, and where they land is
1229
+ * not stable on its own. `MfeHandlerMF.wrapLifecycleWithStylesheets` removes the
1230
+ * MFE's stylesheet on unmount and appends a fresh one before the next mount, so
1231
+ * a node merely left in place ends up ahead of that stylesheet on a remount and
1232
+ * behind it on the first mount: two different cascades for one component.
1233
+ * Anchoring it to the end of the shell-owned head gives every mount the same
1234
+ * order - adopted block, isolation, resets, MFE - which is also the order the
1235
+ * adoption method's invariant is written against.
1236
+ *
1237
+ * With no shell-owned node to anchor to, the resets go to the front. That is the
1238
+ * plain-`Element` container: no adopted block, no isolation style, and the MFE
1239
+ * stylesheet already appended by the handler, so the front is again the only
1240
+ * position that leaves the MFE winning ties.
1241
+ *
1242
+ * Looked up by attribute-selector form rather than `getElementById`, which
1243
+ * `ShadowRoot` has but `Element` does not, and this hook takes either.
1164
1244
  */
1165
1245
  injectBaseResets(container) {
1166
- const style = document.createElement("style");
1246
+ const existing = container.querySelector(
1247
+ `style[id="${BASE_RESETS_STYLE_ID}"]`
1248
+ );
1249
+ const style = existing ?? document.createElement("style");
1250
+ style.id = BASE_RESETS_STYLE_ID;
1167
1251
  style.textContent = `
1168
1252
  *, *::before, *::after {
1169
1253
  box-sizing: border-box;
@@ -1181,7 +1265,17 @@ var ThemeAwareReactLifecycle = class {
1181
1265
  background-color: hsl(var(--background));
1182
1266
  }
1183
1267
  `;
1184
- container.appendChild(style);
1268
+ const shellOwnedHead = container.querySelectorAll(
1269
+ [
1270
+ `[${ADOPTED_HOST_STYLE_ATTR}]`,
1271
+ ...SHELL_OWNED_SHADOW_STYLE_IDS.map((id) => `[id="${id}"]`)
1272
+ ].join(", ")
1273
+ );
1274
+ const lastShellOwnedNode = shellOwnedHead[shellOwnedHead.length - 1];
1275
+ container.insertBefore(
1276
+ style,
1277
+ lastShellOwnedNode ? lastShellOwnedNode.nextSibling : container.firstChild
1278
+ );
1185
1279
  }
1186
1280
  /**
1187
1281
  * Hook for subclasses to inject additional CSS not covered by the adopted host