@ecopages/react-router 0.2.0-alpha.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +212 -0
  4. package/browser.d.ts +13 -0
  5. package/browser.js +11 -0
  6. package/browser.ts +17 -0
  7. package/package.json +42 -0
  8. package/src/adapter.d.ts +28 -0
  9. package/src/adapter.js +22 -0
  10. package/src/adapter.ts +48 -0
  11. package/src/context.d.ts +16 -0
  12. package/src/context.js +11 -0
  13. package/src/context.ts +25 -0
  14. package/src/head-morpher.d.ts +15 -0
  15. package/src/head-morpher.js +94 -0
  16. package/src/head-morpher.ts +170 -0
  17. package/src/index.d.ts +14 -0
  18. package/src/index.js +13 -0
  19. package/src/index.ts +21 -0
  20. package/src/manage-scroll.d.ts +17 -0
  21. package/src/manage-scroll.js +25 -0
  22. package/src/manage-scroll.ts +47 -0
  23. package/src/navigation.d.ts +65 -0
  24. package/src/navigation.js +120 -0
  25. package/src/navigation.ts +247 -0
  26. package/src/props-script.d.ts +11 -0
  27. package/src/props-script.js +11 -0
  28. package/src/props-script.ts +19 -0
  29. package/src/router.d.ts +73 -0
  30. package/src/router.js +225 -0
  31. package/src/router.ts +348 -0
  32. package/src/scroll-persist.d.ts +40 -0
  33. package/src/scroll-persist.js +57 -0
  34. package/src/scroll-persist.ts +96 -0
  35. package/src/styles.css +200 -0
  36. package/src/types.d.ts +49 -0
  37. package/src/types.js +12 -0
  38. package/src/types.ts +64 -0
  39. package/src/view-transition-manager.d.ts +5 -0
  40. package/src/view-transition-manager.js +16 -0
  41. package/src/view-transition-manager.ts +30 -0
  42. package/src/view-transition-utils.d.ts +13 -0
  43. package/src/view-transition-utils.js +60 -0
  44. package/src/view-transition-utils.ts +95 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@ecopages/react-router` are documented here.
4
+
5
+ > **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
6
+
7
+ ## [UNRELEASED] — TBD
8
+
9
+ ### Bug Fixes
10
+
11
+ - Published npm package metadata now includes validated declaration exports for generated dist entrypoints.
12
+
13
+ ### Refactoring
14
+
15
+ - Updated `package.json` dependencies to align with the new core adapter and esbuild build adapter versions.
16
+ - Internal peer dependency declarations updated for React 18+ and the new `@ecopages/core` API surface.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Andrea Zanenghi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # @ecopages/react-router
2
+
3
+ Client-side SPA router for EcoPages React applications. Enables single-page application navigation while preserving full SSR benefits.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @ecopages/react-router
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ Add the router adapter to your `eco.config.ts`:
14
+
15
+ ```typescript
16
+ import { ConfigBuilder } from '@ecopages/core';
17
+ import { reactPlugin } from '@ecopages/react';
18
+ import { ecoRouter } from '@ecopages/react-router';
19
+
20
+ const config = await new ConfigBuilder()
21
+ .setRootDir(import.meta.dir)
22
+ .setIntegrations([reactPlugin({ router: ecoRouter() })])
23
+ .build();
24
+
25
+ export default config;
26
+ ```
27
+
28
+ That's it! All pages now have SPA navigation enabled.
29
+
30
+ ## Features
31
+
32
+ - **Opt-in via config** - Single line enables SPA for all pages
33
+ - **SSR preserved** - Full server-side rendering on initial load
34
+ - **Layout persistence** - Layouts stay mounted, only page content swaps
35
+ - **Standard links** - Works with regular `<a>` tags
36
+ - **Head sync** - Automatically updates title, meta, and stylesheets
37
+ - **Pluggable** - Extensible adapter pattern
38
+
39
+ ## Usage
40
+
41
+ ### Layouts (Optional)
42
+
43
+ Use `config.layout` for persistent UI across navigations:
44
+
45
+ ```tsx
46
+ // src/layouts/base-layout.tsx
47
+ export const BaseLayout = ({ children }) => (
48
+ <html>
49
+ <body>
50
+ <header>My Site</header>
51
+ <main>{children}</main>
52
+ </body>
53
+ </html>
54
+ );
55
+
56
+ // src/pages/index.tsx
57
+ import { BaseLayout } from '../layouts/base-layout';
58
+
59
+ const HomePage = () => <h1>Welcome</h1>;
60
+
61
+ HomePage.config = { layout: BaseLayout };
62
+
63
+ export default HomePage;
64
+ ```
65
+
66
+ ### Links
67
+
68
+ ```tsx
69
+ // SPA navigation (intercepted)
70
+ <a href="/about">About</a>
71
+
72
+ // Force full reload
73
+ <a href="/external" data-eco-reload>External</a>
74
+ ```
75
+
76
+ ### Programmatic Navigation
77
+
78
+ ```tsx
79
+ import { useRouter } from '@ecopages/react-router';
80
+
81
+ const MyComponent = () => {
82
+ const { navigate, isPending } = useRouter();
83
+
84
+ return (
85
+ <button onClick={() => navigate('/about')} disabled={isPending}>
86
+ Go to About
87
+ </button>
88
+ );
89
+ };
90
+ ```
91
+
92
+ ### View Transitions
93
+
94
+ The router automatically supports the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for smooth page transitions.
95
+
96
+ #### Lifecycle
97
+
98
+ When a navigation occurs with View Transitions enabled:
99
+
100
+ 1. **Snapshot**: The browser captures the current state (screenshot) of the page.
101
+ 2. **Update**: React processes the state change and renders the new page.
102
+ 3. **Animate**: The browser animates from the old snapshot to the new live state.
103
+
104
+ The router uses a deferred promise mechanism to ensure React has fully finished rendering the new content before telling the browser to start the animation phase.
105
+
106
+ #### Shared Element Transitions
107
+
108
+ To animate elements between pages (e.g., a thumbnail becoming a hero image), use the `data-view-transition` attribute. Ensure the value is unique to the specific element being transitioned and matches on both pages.
109
+
110
+ ```tsx
111
+ // List Page (Source)
112
+ <img
113
+ src={post.image}
114
+ data-view-transition={`hero-${post.id}`}
115
+ />
116
+
117
+ // Detail Page (Destination)
118
+ <img
119
+ src={post.image}
120
+ data-view-transition={`hero-${post.id}`}
121
+ />
122
+ ```
123
+
124
+ By default, the router applies a **clean morph** animation (disabling the default cross-fade ghosting). If you prefer the standard browser cross-fade, you can opt-out:
125
+
126
+ ```tsx
127
+ <div data-view-transition="my-hero" data-view-transition-animate="fade">
128
+ ...
129
+ </div>
130
+ ```
131
+
132
+ #### Cross-Fade
133
+
134
+ By default, the router provides a smooth cross-fade for the root content. You can customize this by overriding the default view transition CSS:
135
+
136
+ ```css
137
+ ::view-transition-old(root),
138
+ ::view-transition-new(root) {
139
+ animation-duration: 0.5s;
140
+ }
141
+ ```
142
+
143
+ ## How It Works
144
+
145
+ The router uses an **HTML-First** navigation strategy to ensure consistency with Server-Side Rendering (SSR).
146
+
147
+ 1. **SSR**: Server renders full HTML for the initial page load.
148
+ 2. **Hydration**: Client hydrates, router attaches to the document.
149
+ 3. **Navigation**: On link click:
150
+ - **Fetch**: Requests the full HTML of the target page (just like a standard browser navigation).
151
+ - **Parse**: Extracts the page component URL and serialized props from the HTML.
152
+ - **Preload**: Dynamically imports the new page component.
153
+ - **Transition**:
154
+ - Calls `document.startViewTransition()`.
155
+ - Updates the document head (title, meta, styles).
156
+ - Updates the React state to render the new page component.
157
+ - Waits for React commit (useEffect).
158
+ - **Resolve**: View Transition finishes, browser plays the animation.
159
+
160
+ ## API
161
+
162
+ ### `ecoRouter()`
163
+
164
+ Creates a router adapter for the React plugin.
165
+
166
+ ```typescript
167
+ reactPlugin({ router: ecoRouter() });
168
+ ```
169
+
170
+ ### `useRouter()`
171
+
172
+ Hook for programmatic navigation.
173
+
174
+ ```typescript
175
+ const { navigate, isPending } = useRouter();
176
+ ```
177
+
178
+ ### Link Behavior
179
+
180
+ Links are **not** intercepted when:
181
+
182
+ - Modifier keys held (Ctrl, Cmd, Shift, Alt)
183
+ - Has `target="_blank"` or `download` attribute
184
+ - Has `data-eco-reload` attribute
185
+ - Points to different origin
186
+ - Starts with `#` or `javascript:`
187
+
188
+ ## Architecture
189
+
190
+ The router uses a pluggable adapter pattern:
191
+
192
+ ```typescript
193
+ interface ReactRouterAdapter {
194
+ name: string;
195
+ bundle: { importPath; outputName; externals };
196
+ importMapKey: string;
197
+ components: { router; pageContent };
198
+ getRouterProps(page, props): string;
199
+ }
200
+ ```
201
+
202
+ This allows custom router implementations while keeping integration simple.
203
+
204
+ ## Compatibility
205
+
206
+ - React 18.x or 19.x
207
+ - Modern browsers with ES modules
208
+ - EcoPages with React integration
209
+
210
+ ## License
211
+
212
+ MIT
package/browser.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Browser entry point for @ecopages/react-router.
3
+ * This file exports only the client-side components needed for hydration.
4
+ * @module
5
+ */
6
+ export { EcoRouter, PageContent } from './src/router.js';
7
+ export type { EcoRouterProps } from './src/router.js';
8
+ export { useRouter } from './src/context.js';
9
+ export type { RouterContextValue } from './src/context.js';
10
+ export { EcoPropsScript } from './src/props-script.js';
11
+ export type { EcoPropsScriptProps } from './src/props-script.js';
12
+ export { morphHead } from './src/head-morpher.js';
13
+ export type { PageState } from './src/navigation.js';
package/browser.js ADDED
@@ -0,0 +1,11 @@
1
+ import { EcoRouter, PageContent } from "./src/router.js";
2
+ import { useRouter } from "./src/context.js";
3
+ import { EcoPropsScript } from "./src/props-script.js";
4
+ import { morphHead } from "./src/head-morpher.js";
5
+ export {
6
+ EcoPropsScript,
7
+ EcoRouter,
8
+ PageContent,
9
+ morphHead,
10
+ useRouter
11
+ };
package/browser.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Browser entry point for @ecopages/react-router.
3
+ * This file exports only the client-side components needed for hydration.
4
+ * @module
5
+ */
6
+
7
+ export { EcoRouter, PageContent } from './src/router.ts';
8
+ export type { EcoRouterProps } from './src/router.ts';
9
+
10
+ export { useRouter } from './src/context.ts';
11
+ export type { RouterContextValue } from './src/context.ts';
12
+ export { EcoPropsScript } from './src/props-script.ts';
13
+ export type { EcoPropsScriptProps } from './src/props-script.ts';
14
+
15
+ export { morphHead } from './src/head-morpher.ts';
16
+
17
+ export type { PageState } from './src/navigation.ts';
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@ecopages/react-router",
3
+ "version": "0.2.0-alpha.1",
4
+ "description": "Client-side SPA router for EcoPages React applications",
5
+ "keywords": [
6
+ "ecopages",
7
+ "react",
8
+ "router",
9
+ "spa",
10
+ "navigation",
11
+ "ssr"
12
+ ],
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./src/index.d.ts",
18
+ "default": "./src/index.js"
19
+ },
20
+ "./browser": {
21
+ "types": "./browser.d.ts",
22
+ "default": "./browser.js"
23
+ },
24
+ "./browser.ts": {
25
+ "types": "./browser.d.ts",
26
+ "default": "./browser.js"
27
+ }
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/ecopages/ecopages.git",
32
+ "directory": "packages/react-router"
33
+ },
34
+ "peerDependencies": {
35
+ "@ecopages/react": "0.2.0-alpha.1"
36
+ },
37
+ "dependencies": {
38
+ "react": "^19",
39
+ "react-dom": "^19.2.4"
40
+ },
41
+ "types": "./src/index.d.ts"
42
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Router adapter for React integration.
3
+ * @module
4
+ */
5
+ import type { ReactRouterAdapter } from '@ecopages/react/router-adapter';
6
+ import type { EcoRouterOptions } from './types.js';
7
+ /**
8
+ * Creates a ReactRouterAdapter for EcoPages React Router.
9
+ * Use this with the React plugin to enable SPA navigation.
10
+ *
11
+ * @param options - Router configuration options
12
+ * @example
13
+ * ```ts
14
+ * import { reactPlugin } from '@ecopages/react';
15
+ * import { ecoRouter } from '@ecopages/react-router';
16
+ *
17
+ * export default {
18
+ * integrations: [reactPlugin({ router: ecoRouter() })],
19
+ * };
20
+ * ```
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // Disable view transitions
25
+ * reactPlugin({ router: ecoRouter({ viewTransitions: false }) })
26
+ * ```
27
+ */
28
+ export declare function ecoRouter(options?: EcoRouterOptions): ReactRouterAdapter;
package/src/adapter.js ADDED
@@ -0,0 +1,22 @@
1
+ function ecoRouter(options) {
2
+ return {
3
+ name: "eco-router",
4
+ bundle: {
5
+ importPath: "@ecopages/react-router/browser.ts",
6
+ outputName: "react-router-esm",
7
+ externals: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"]
8
+ },
9
+ importMapKey: "@ecopages/react-router",
10
+ components: {
11
+ router: "EcoRouter",
12
+ pageContent: "PageContent"
13
+ },
14
+ getRouterProps(page, props) {
15
+ const optionsStr = options ? `, options: ${JSON.stringify(options)}` : "";
16
+ return `{ page: ${page}, pageProps: ${props}${optionsStr} }`;
17
+ }
18
+ };
19
+ }
20
+ export {
21
+ ecoRouter
22
+ };
package/src/adapter.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Router adapter for React integration.
3
+ * @module
4
+ */
5
+
6
+ import type { ReactRouterAdapter } from '@ecopages/react/router-adapter';
7
+ import type { EcoRouterOptions } from './types.ts';
8
+
9
+ /**
10
+ * Creates a ReactRouterAdapter for EcoPages React Router.
11
+ * Use this with the React plugin to enable SPA navigation.
12
+ *
13
+ * @param options - Router configuration options
14
+ * @example
15
+ * ```ts
16
+ * import { reactPlugin } from '@ecopages/react';
17
+ * import { ecoRouter } from '@ecopages/react-router';
18
+ *
19
+ * export default {
20
+ * integrations: [reactPlugin({ router: ecoRouter() })],
21
+ * };
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // Disable view transitions
27
+ * reactPlugin({ router: ecoRouter({ viewTransitions: false }) })
28
+ * ```
29
+ */
30
+ export function ecoRouter(options?: EcoRouterOptions): ReactRouterAdapter {
31
+ return {
32
+ name: 'eco-router',
33
+ bundle: {
34
+ importPath: '@ecopages/react-router/browser.ts',
35
+ outputName: 'react-router-esm',
36
+ externals: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
37
+ },
38
+ importMapKey: '@ecopages/react-router',
39
+ components: {
40
+ router: 'EcoRouter',
41
+ pageContent: 'PageContent',
42
+ },
43
+ getRouterProps(page: string, props: string): string {
44
+ const optionsStr = options ? `, options: ${JSON.stringify(options)}` : '';
45
+ return `{ page: ${page}, pageProps: ${props}${optionsStr} }`;
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Router context and hook for accessing navigation state.
3
+ * @module
4
+ */
5
+ export type RouterContextValue = {
6
+ navigate: (url: string) => void;
7
+ isNavigating: boolean;
8
+ };
9
+ export declare const RouterContext: import("react").Context<RouterContextValue | null>;
10
+ /**
11
+ * Hook to access the router's navigate function and navigation state.
12
+ * Must be used within an EcoRouter.
13
+ *
14
+ * @throws Error if used outside of EcoRouter
15
+ */
16
+ export declare const useRouter: () => RouterContextValue;
package/src/context.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createContext, useContext } from "react";
2
+ const RouterContext = createContext(null);
3
+ const useRouter = () => {
4
+ const context = useContext(RouterContext);
5
+ if (!context) throw new Error("useRouter must be used within EcoRouter");
6
+ return context;
7
+ };
8
+ export {
9
+ RouterContext,
10
+ useRouter
11
+ };
package/src/context.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Router context and hook for accessing navigation state.
3
+ * @module
4
+ */
5
+
6
+ import { createContext, useContext } from 'react';
7
+
8
+ export type RouterContextValue = {
9
+ navigate: (url: string) => void;
10
+ isNavigating: boolean;
11
+ };
12
+
13
+ export const RouterContext = createContext<RouterContextValue | null>(null);
14
+
15
+ /**
16
+ * Hook to access the router's navigate function and navigation state.
17
+ * Must be used within an EcoRouter.
18
+ *
19
+ * @throws Error if used outside of EcoRouter
20
+ */
21
+ export const useRouter = (): RouterContextValue => {
22
+ const context = useContext(RouterContext);
23
+ if (!context) throw new Error('useRouter must be used within EcoRouter');
24
+ return context;
25
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Head morphing utilities for client-side navigation.
3
+ * Intelligently syncs head elements between pages using key-based diffing.
4
+ * @module
5
+ */
6
+ /**
7
+ * Morphs the current document head to match the new document's head.
8
+ * Now splits the process into adding new elements and returning a cleanup function
9
+ * to remove old ones. This is crucial for View Transitions to ensure styles
10
+ * don't disappear before the "old" snapshot is taken.
11
+ *
12
+ * @param newDocument - The parsed document from the navigation target
13
+ * @returns Promise that resolves to a cleanup function when new stylesheets have loaded
14
+ */
15
+ export declare function morphHead(newDocument: Document): Promise<() => void>;
@@ -0,0 +1,94 @@
1
+ const PRESERVE_SELECTORS = ['script[type="importmap"]', "meta[charset]", "[data-eco-persist]"];
2
+ function getHeadElementKey(el) {
3
+ const tag = el.tagName.toLowerCase();
4
+ switch (tag) {
5
+ case "title":
6
+ return "title";
7
+ case "meta": {
8
+ const name = el.getAttribute("name") || el.getAttribute("property") || el.getAttribute("http-equiv");
9
+ return name ? `meta:${name}` : null;
10
+ }
11
+ case "link": {
12
+ const rel = el.getAttribute("rel");
13
+ const href = el.getAttribute("href");
14
+ if (rel === "stylesheet" && href) return `stylesheet:${href}`;
15
+ if (rel === "icon" || rel === "shortcut icon") return "favicon";
16
+ if (rel === "canonical") return "canonical";
17
+ return href ? `link:${href}` : null;
18
+ }
19
+ case "script": {
20
+ if (el.getAttribute("type") === "importmap") return "importmap";
21
+ const src = el.src;
22
+ return src ? `script:${src}` : null;
23
+ }
24
+ case "style": {
25
+ const dataId = el.getAttribute("data-eco-style");
26
+ return dataId ? `style:${dataId}` : null;
27
+ }
28
+ default:
29
+ return null;
30
+ }
31
+ }
32
+ async function morphHead(newDocument) {
33
+ const currentHead = document.head;
34
+ const newHead = newDocument.head;
35
+ const currentElements = /* @__PURE__ */ new Map();
36
+ const newElements = /* @__PURE__ */ new Map();
37
+ const stylesheetPromises = [];
38
+ const elementsToRemove = [];
39
+ for (const el of Array.from(currentHead.children)) {
40
+ const key = getHeadElementKey(el);
41
+ if (key) currentElements.set(key, el);
42
+ }
43
+ for (const el of Array.from(newHead.children)) {
44
+ const key = getHeadElementKey(el);
45
+ if (key) newElements.set(key, el);
46
+ }
47
+ for (const [key, newEl] of newElements) {
48
+ const currentEl = currentElements.get(key);
49
+ if (!currentEl) {
50
+ const src = newEl.getAttribute("src");
51
+ if (newEl.tagName === "SCRIPT" && src && src.includes("hydration.js") && src.includes("ecopages-react")) {
52
+ continue;
53
+ }
54
+ const cloned = newEl.cloneNode(true);
55
+ if (cloned.tagName === "LINK" && cloned.rel === "stylesheet") {
56
+ const loadPromise = new Promise((resolve) => {
57
+ cloned.onload = () => resolve();
58
+ cloned.onerror = () => resolve();
59
+ });
60
+ stylesheetPromises.push(loadPromise);
61
+ }
62
+ currentHead.appendChild(cloned);
63
+ } else if (key === "title" && currentEl.textContent !== newEl.textContent) {
64
+ currentEl.textContent = newEl.textContent;
65
+ } else if (key.startsWith("style:") && currentEl.textContent !== newEl.textContent) {
66
+ currentEl.textContent = newEl.textContent;
67
+ }
68
+ }
69
+ for (const newEl of Array.from(newHead.children)) {
70
+ const key = getHeadElementKey(newEl);
71
+ if (!key) {
72
+ currentHead.appendChild(newEl.cloneNode(true));
73
+ }
74
+ }
75
+ if (stylesheetPromises.length > 0) {
76
+ await Promise.all(stylesheetPromises);
77
+ }
78
+ for (const [key, el] of currentElements) {
79
+ if (!newElements.has(key)) {
80
+ const shouldPreserve = PRESERVE_SELECTORS.some((sel) => el.matches(sel));
81
+ if (!shouldPreserve) {
82
+ elementsToRemove.push(el);
83
+ }
84
+ }
85
+ }
86
+ return () => {
87
+ for (const el of elementsToRemove) {
88
+ el.remove();
89
+ }
90
+ };
91
+ }
92
+ export {
93
+ morphHead
94
+ };