lightweight-router 1.0.10 → 1.0.14

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/README.md CHANGED
@@ -13,6 +13,7 @@ PageFlick is a minimal lightweight client-side router with intelligent prefetchi
13
13
  - 🎨 Built-in loading animations
14
14
  - 🕰️ Based on History API so you can use native browser navigation
15
15
  - 🤖 Automatic title change
16
+ - 📜 Scroll route sections with automatic URL/title updates
16
17
 
17
18
 
18
19
  ## Installation
@@ -164,6 +165,55 @@ It does exactly what we're talking about automatically by returning full html re
164
165
  - Implement server-side partial responses for better bandwidth usage
165
166
  - Consider using the `prefetch="onHover"` attribute for less important links
166
167
 
168
+ ## Scroll Routes
169
+
170
+ The `scroll` attribute on a `<route>` marks it as a scrollable section. Scroll routes are stacked vertically and always visible when in scroll-route context. Clicking a link to a scroll route smooth-scrolls to that section. As the user scrolls, the URL and page title update automatically to reflect the visible section.
171
+
172
+ Use an optional `<title>` tag inside a scroll route to set the document title when that section is in view.
173
+
174
+ ```html
175
+ <router>
176
+ <route path="/" scroll>
177
+ <title>Home | My Site</title>
178
+ <section style="min-height: 100vh">Hero content</section>
179
+ </route>
180
+ <route path="/features" scroll>
181
+ <title>Features | My Site</title>
182
+ <section style="min-height: 100vh">Features content</section>
183
+ </route>
184
+ <route path="/pricing" scroll>
185
+ <title>Pricing | My Site</title>
186
+ <section style="min-height: 100vh">Pricing content</section>
187
+ </route>
188
+ </router>
189
+ ```
190
+
191
+ You can mix scroll routes and regular routes. When navigating to a regular route, scroll routes are hidden. When navigating to a scroll route, regular routes are hidden and all scroll sections are visible.
192
+
193
+ ### Server configuration for direct URL access
194
+
195
+ When a user visits a scroll route URL directly (e.g. `/features`), the server must serve the page containing the scroll routes. Configure your server to serve the same HTML file for all scroll route paths:
196
+
197
+ **Nginx:**
198
+ ```nginx
199
+ location ~ ^/(features|pricing)$ {
200
+ try_files $uri /home.html;
201
+ }
202
+ ```
203
+
204
+ **Express / Node.js:**
205
+ ```javascript
206
+ app.get(["/", "/features", "/pricing"], (req, res) => {
207
+ res.sendFile(path.join(__dirname, "home.html"));
208
+ });
209
+ ```
210
+
211
+ **Vite / static dev servers:** Use a rewrite rule or `_redirects` file (Netlify/Vercel):
212
+ ```
213
+ /features /home.html 200
214
+ /pricing /home.html 200
215
+ ```
216
+
167
217
  ## Future Development
168
218
  - Delay router intialization on first link hover for better performances?
169
219
  - Implement html streaming for faster page load
@@ -1 +1 @@
1
- (()=>{let l={},n=!1,c=(...e)=>n&&console.log("🚦 Router:",...e),o=async()=>{c("Navigation triggered to:",globalThis.location.pathname),document.body.classList.add("loading");var e=globalThis.location.pathname.replace(/\/$/,""),t=document.querySelector("router");let r=t.querySelector(`route[path="${e}"]`);if(r||(c("Creating new route element for:",e),(r=document.createElement("route")).setAttribute("path",e),t.appendChild(r)),!r.innerHTML){c("Fetching content for:",globalThis.location.href);let e=l[globalThis.location.href];e||(e=await u(globalThis.location.href),l[globalThis.location.href]=e);var n,o=(new DOMParser).parseFromString(e,"text/html"),a=o.querySelector("title"),a=(a&&(c("Updating page title to:",a.textContent),document.title=a.textContent),r.innerHTML=o.body.innerHTML,Array.from(r.querySelectorAll("script")));c("Executing",a.length,"scripts from fetched content");for(n of a){var i=document.createElement("script");n.src?i.src=n.src:i.textContent=n.textContent,n.parentNode.replaceChild(i,n)}}t.querySelectorAll("route").forEach(e=>e.style.contentVisibility="hidden"),r.style.contentVisibility="visible",document.body.classList.remove("loading"),window.scrollTo(0,0),d&&d(e),c("Route change completed")},a=async e=>{l[e.href]||(c("Prefetching content for:",e.href),l[e.href]=await u(e.href))},i=(e,r)=>{c("🔍 Intersection Observer triggered for",e.length,"entries"),e.forEach(e=>{var t=e.target;c(`🎯 Link ${t.href} intersection:`,{isIntersecting:e.isIntersecting,intersectionRatio:e.intersectionRatio,alreadyCached:!!l[t.href]}),e.isIntersecting&&(l[t.href]?c("📦 Content already cached for:",t.href):(a(t),c("👁️ Unobserving link after prefetch initiated:",t.href),r.unobserve(t)))})},s=e=>{var t=e.target.closest("A");t&&t.href&&h(t.href)&&t.origin===location.origin?(c("Internal link clicked:",t.href),e.preventDefault(),globalThis.history.pushState(null,null,t.href),globalThis.dispatchEvent(new Event("popstate"))):c("Invalid link click:",t?.href)};function h(e){if(!e||e.startsWith("#")||e.startsWith("javascript:"))return!1;if(e.startsWith("/"))return!0;try{var t=new URL(e,window.location.origin),r=new URL(window.location.href);return r.hostname.replace(/^www\./,"")!==t.hostname.replace(/^www\./,"")?!1:r.pathname!==t.pathname||!t.hash}catch{return!1}}let d,u=async e=>{e=await t(e);return e.ok?e.text():"Couldn't fetch the route - HTTP error! status: "+e.status},t=async e=>{if(!f){var t=await fetch(e,{method:"POST",body:"onlyRoute"});if(t.ok)return t}return fetch(e)},f=!1,e=(e={})=>{var{onRouteChange:t,debug:r}=e,e=(n=r,c("Router starting...",e),t&&(r=t,d=r),document.createElement("style")),t=(e.textContent=".loading{animation:pulse 1s infinite alternate}@keyframes pulse{from{opacity:.6}to{opacity:.1}}route{content-visibility:auto}",document.head.appendChild(e),document.querySelector("router")),r=globalThis.location.pathname,r=(t||(c("Creating new router element"),t=document.createElement("router"),(e=document.createElement("route")).setAttribute("path",r),e.innerHTML=document.body.innerHTML,t.appendChild(e),document.body.innerHTML="",document.body.appendChild(t),f=!0),globalThis.addEventListener("popstate",o),document.addEventListener("click",s),document.body.addEventListener("mouseover",e=>{"A"===e.target.tagName&&"onHover"===e.target.getAttribute("prefetch")&&(async e=>{e=e.target;!l[e.href]&&h(e.href)&&await a(e)})(e)}),new IntersectionObserver(i,{root:null,threshold:.5}));c("🎭 Created Intersection Observer with config:",{root:"viewport",threshold:.5}),(r=>{let n=navigator.connection&&navigator.connection.saveData;var e=document.querySelectorAll("a");c("🔄 Starting link observation...",{totalLinks:e.length,saveDataMode:n}),e.forEach(e=>{var t="onHover"!==e.getAttribute("prefetch")&&!n&&h(e.href);c("🔗 Link evaluation:",{href:e.href,prefetchAttr:e.getAttribute("prefetch"),isInternal:h(e.href),willObserve:t}),t&&(r.observe(e),c("👀 Now observing link:",e.href))})})(r)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()})();
1
+ (()=>{let i={},a=!1,l=(...e)=>a&&console.log("🚦 Router:",...e),c=(e,t)=>{var e=e?.querySelector("title");return e?e.textContent:(e=t.replace(/\.html$/,"").split("/").filter(Boolean).pop())?e.charAt(0).toUpperCase()+e.slice(1):"Home"},s=e=>{e.querySelectorAll("route[scroll]").forEach(e=>e.style.removeProperty("display")),e.querySelectorAll("route:not([scroll])").forEach(e=>e.style.display="none")},h=async()=>{l("Navigation triggered to:",globalThis.location.pathname),document.body.classList.add("loading");var t=globalThis.location.pathname.replace(/\/$/,"")||"/",e=document.querySelector("router");let r=e.querySelector(`route[path="${t}"]`);if(r?.hasAttribute("scroll"))s(e),document.body.classList.remove("loading"),r.scrollIntoView({behavior:"smooth"}),p&&p(t),l("Scroll route navigation completed");else{if(r||(l("Creating new route element for:",t),(r=document.createElement("route")).setAttribute("path",t),e.appendChild(r)),!r.innerHTML){l("Fetching content for:",globalThis.location.href);let e=i[globalThis.location.href];e||(e=await m(globalThis.location.href),i[globalThis.location.href]=e);var o,n=(new DOMParser).parseFromString(e,"text/html"),n=(document.title=c(n,t),l("Updating page title to:",document.title),r.innerHTML=n.body.innerHTML,Array.from(r.querySelectorAll("script")));l("Executing",n.length,"scripts from fetched content");for(o of n){var a=document.createElement("script");o.src?a.src=o.src:a.textContent=o.textContent,o.parentNode.replaceChild(a,o)}}e.querySelectorAll("route").forEach(e=>e.style.display="none"),r.style.display="contents",document.body.classList.remove("loading"),window.scrollTo(0,0),p&&p(t),l("Route change completed")}},d=async e=>{i[e.href]||(l("Prefetching content for:",e.href),i[e.href]=await m(e.href))},u=(e,r)=>{l("🔍 Intersection Observer triggered for",e.length,"entries"),e.forEach(e=>{var t=e.target;l(`🎯 Link ${t.href} intersection:`,{isIntersecting:e.isIntersecting,intersectionRatio:e.intersectionRatio,alreadyCached:!!i[t.href]}),e.isIntersecting&&(i[t.href]?l("📦 Content already cached for:",t.href):(d(t),l("👁️ Unobserving link after prefetch initiated:",t.href),r.unobserve(t)))})},f=e=>{var t=e.target.closest("A");t&&t.href&&g(t.href)&&t.origin===location.origin?(l("Internal link clicked:",t.href),e.preventDefault(),globalThis.history.pushState(null,null,t.href),globalThis.dispatchEvent(new Event("popstate"))):l("Invalid link click:",t?.href)};function g(e){if(!e||e.startsWith("#")||e.startsWith("javascript:"))return!1;if(e.startsWith("/"))return!0;try{var t=new URL(e,window.location.origin),r=new URL(window.location.href);return r.hostname.replace(/^www\./,"")!==t.hostname.replace(/^www\./,"")?!1:r.pathname!==t.pathname||!t.hash}catch{return!1}}let p,m=async e=>{e=await t(e);return e.ok?e.text():"Couldn't fetch the route - HTTP error! status: "+e.status},t=async e=>{if(!v){var t=await fetch(e,{method:"POST",body:"onlyRoute"});if(t.ok)return t}return fetch(e)},v=!1,e=(e={})=>{let{onRouteChange:r,debug:t}=e;a=t,l("Router starting...",e),r&&(e=r,p=e);e=document.createElement("style");e.textContent=".loading{animation:pulse 1s infinite alternate}@keyframes pulse{from{opacity:.6}to{opacity:.1}}route{content-visibility:auto}",document.head.appendChild(e);let o=document.querySelector("router");var e=globalThis.location.pathname.replace(/\/$/,"")||"/",n=(o||(l("Creating new router element"),o=document.createElement("router"),(n=document.createElement("route")).setAttribute("path",e),n.innerHTML=document.body.innerHTML,o.appendChild(n),document.body.innerHTML="",document.body.appendChild(o),v=!0),globalThis.addEventListener("popstate",h),document.addEventListener("click",f),document.body.addEventListener("mouseover",e=>{"A"===e.target.tagName&&"onHover"===e.target.getAttribute("prefetch")&&(async e=>{e=e.target;!i[e.href]&&g(e.href)&&await d(e)})(e)}),new IntersectionObserver(u,{root:null,threshold:.5})),n=(l("🎭 Created Intersection Observer with config:",{root:"viewport",threshold:.5}),(r=>{let o=navigator.connection&&navigator.connection.saveData;var e=document.querySelectorAll("a");l("🔄 Starting link observation...",{totalLinks:e.length,saveDataMode:o}),e.forEach(e=>{var t="onHover"!==e.getAttribute("prefetch")&&!o&&g(e.href);l("🔗 Link evaluation:",{href:e.href,prefetchAttr:e.getAttribute("prefetch"),isInternal:g(e.href),willObserve:t}),t&&(r.observe(e),l("👀 Now observing link:",e.href))})})(n),o.querySelectorAll("route[scroll]"));if(n.length){e=o.querySelector(`route[path="${e}"][scroll]`);e&&(s(o),e.scrollIntoView({behavior:"instant"}));let t=new IntersectionObserver(e=>{e.forEach(e=>{var t;e.isIntersecting&&(t=e.target.getAttribute("path")||"/",globalThis.history.replaceState(null,null,t),document.title=c(e.target,t),r)&&r(t)})},{threshold:.6});n.forEach(e=>t.observe(e))}};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()})();
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "lightweight-router",
3
- "version": "1.0.10",
3
+ "version": "1.0.14",
4
4
  "main": "src/router.js",
5
5
  "scripts": {
6
6
  "dev": "npx http-server src -o",
7
7
  "build": "node build.js",
8
- "deploy": "npm run build && git publish && npm version patch && git publish && npm publish",
8
+ "deploy": "npm run build && git publish && npm version patch && git publish && npm login && npm publish",
9
9
  "test": "jest",
10
10
  "test:watch": "jest --watch"
11
11
  },
package/src/about.html CHANGED
@@ -2,22 +2,84 @@
2
2
  <head>
3
3
  <meta charset="utf-8" />
4
4
  <meta name="viewport" content="width=device-width,initial-scale=1" />
5
- <title>Router manual test About</title>
5
+ <title>About | PageFlick Demo</title>
6
+ <style>
7
+ * {
8
+ box-sizing: border-box;
9
+ margin: 0;
10
+ padding: 0;
11
+ }
12
+ body {
13
+ font-family: system-ui, sans-serif;
14
+ }
15
+ nav {
16
+ position: fixed;
17
+ top: 0;
18
+ width: 100%;
19
+ display: flex;
20
+ gap: 1rem;
21
+ padding: 1rem 2rem;
22
+ background: rgba(255, 255, 255, 0.9);
23
+ backdrop-filter: blur(8px);
24
+ z-index: 100;
25
+ border-bottom: 1px solid #eee;
26
+ }
27
+ nav a {
28
+ text-decoration: none;
29
+ color: #333;
30
+ font-weight: 500;
31
+ }
32
+ nav a:hover {
33
+ color: #000;
34
+ }
35
+ route[scroll] {
36
+ display: block;
37
+ }
38
+ section {
39
+ min-height: 100vh;
40
+ display: flex;
41
+ flex-direction: column;
42
+ justify-content: center;
43
+ align-items: center;
44
+ padding: 6rem 2rem 2rem;
45
+ gap: 1rem;
46
+ }
47
+ section h1 {
48
+ font-size: 3rem;
49
+ }
50
+ section p {
51
+ font-size: 1.25rem;
52
+ color: #555;
53
+ max-width: 600px;
54
+ text-align: center;
55
+ }
56
+ #hero {
57
+ background: #f0f4ff;
58
+ }
59
+ #features {
60
+ background: #f0fff4;
61
+ }
62
+ #pricing {
63
+ background: #fff0f4;
64
+ }
65
+ </style>
6
66
  </head>
7
- <body style="background-color: bisque">
8
- <menu class="flex m-0 p-0"
9
- ><a prefetch="onHover" href="/">Home</a><a href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
- ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a>
11
- </menu>
12
- ABOUT
67
+ <body>
68
+ <nav>
69
+ <a href="/">Home</a>
70
+ <a href="/features">Features</a>
71
+ <a href="/pricing">Pricing</a>
72
+ <a href="/about.html">About (regular route)</a>
73
+ </nav>
74
+ <section id="about">
75
+ <h1>About</h1>
76
+ <p>This is the about page.</p>
77
+ <a href="/">Back to home ↑</a>
78
+ </section>
13
79
  <script type="module">
14
80
  import { startRouter } from "./router.js";
15
- // Initialize the router with a simple onRouteChange callback to test functionality
16
81
  startRouter({
17
- onRouteChange: currentRoute => {
18
- console.log("Route changed:", currentRoute);
19
- currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
20
- },
82
+ onRouteChange: path => console.log("Route:", path),
21
83
  });
22
84
  </script>
23
85
  </body>
package/src/index.html CHANGED
@@ -2,22 +2,115 @@
2
2
  <head>
3
3
  <meta charset="utf-8" />
4
4
  <meta name="viewport" content="width=device-width,initial-scale=1" />
5
- <title>Router manual test Index</title>
5
+ <title>Home | PageFlick Demo</title>
6
+ <style>
7
+ * {
8
+ box-sizing: border-box;
9
+ margin: 0;
10
+ padding: 0;
11
+ }
12
+ body {
13
+ font-family: system-ui, sans-serif;
14
+ }
15
+ nav {
16
+ position: fixed;
17
+ top: 0;
18
+ width: 100%;
19
+ display: flex;
20
+ gap: 1rem;
21
+ padding: 1rem 2rem;
22
+ background: rgba(255, 255, 255, 0.9);
23
+ backdrop-filter: blur(8px);
24
+ z-index: 100;
25
+ border-bottom: 1px solid #eee;
26
+ }
27
+ nav a {
28
+ text-decoration: none;
29
+ color: #333;
30
+ font-weight: 500;
31
+ }
32
+ nav a:hover {
33
+ color: #000;
34
+ }
35
+ route[scroll] {
36
+ display: block;
37
+ }
38
+ section {
39
+ min-height: 100vh;
40
+ display: flex;
41
+ flex-direction: column;
42
+ justify-content: center;
43
+ align-items: center;
44
+ padding: 6rem 2rem 2rem;
45
+ gap: 1rem;
46
+ }
47
+ section h1 {
48
+ font-size: 3rem;
49
+ }
50
+ section p {
51
+ font-size: 1.25rem;
52
+ color: #555;
53
+ max-width: 600px;
54
+ text-align: center;
55
+ }
56
+ #hero {
57
+ background: #f0f4ff;
58
+ }
59
+ #features {
60
+ background: #f0fff4;
61
+ }
62
+ #pricing {
63
+ background: #fff0f4;
64
+ }
65
+ </style>
6
66
  </head>
7
67
  <body>
8
- <menu class="flex m-0 p-0"
9
- ><a prefetch="onHover" href="/">Home</a><a preFetch="onHover" href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
- ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a>
11
- </menu>
12
- HOME
68
+ <nav>
69
+ <a href="/">Home</a>
70
+ <a href="/features">Features</a>
71
+ <a href="/pricing">Pricing</a>
72
+ <a href="/about.html">About (regular route)</a>
73
+ </nav>
74
+
75
+ <router>
76
+ <route path="/" scroll>
77
+ <title>Home | PageFlick Demo</title>
78
+ <section id="hero">
79
+ <h1>PageFlick</h1>
80
+ <p>A minimal client-side router with scroll section support. Scroll down or click the nav links above.</p>
81
+ <a href="/features">See Features ↓</a>
82
+ </section>
83
+ </route>
84
+
85
+ <route path="/features" scroll>
86
+ <title>Features | PageFlick Demo</title>
87
+ <section id="features">
88
+ <h1>Features</h1>
89
+ <p>Scroll routes stack vertically and stay visible. The URL and title update as you scroll through sections.</p>
90
+ <ul style="text-align: left; color: #333">
91
+ <li>Smooth scroll navigation</li>
92
+ <li>Automatic URL updates on scroll</li>
93
+ <li>Direct URL access jumps to section</li>
94
+ <li>Mix with regular routes</li>
95
+ </ul>
96
+ <a href="/pricing">See Pricing ↓</a>
97
+ </section>
98
+ </route>
99
+
100
+ <route path="/pricing" scroll>
101
+ <title>Pricing | PageFlick Demo</title>
102
+ <section id="pricing">
103
+ <h1>Pricing</h1>
104
+ <p>Free and open source. Always.</p>
105
+ <a href="/">Back to top ↑</a>
106
+ </section>
107
+ </route>
108
+ </router>
109
+
13
110
  <script type="module">
14
111
  import { startRouter } from "./router.js";
15
- // Initialize the router with a simple onRouteChange callback to test functionality
16
112
  startRouter({
17
- onRouteChange: currentRoute => {
18
- console.log("Route changed:", currentRoute);
19
- currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
20
- },
113
+ onRouteChange: path => console.log("Route:", path),
21
114
  });
22
115
  </script>
23
116
  </body>
package/src/router.js CHANGED
@@ -1,14 +1,37 @@
1
1
  let linkData = {};
2
2
  let debugMode = false;
3
3
  const log = (...args) => debugMode && console.log("🚦 Router:", ...args);
4
+
5
+ const getTitle = (el, path) => {
6
+ const tag = el?.querySelector("title");
7
+ if (tag) return tag.textContent;
8
+ const segment = path.replace(/\.html$/, "").split("/").filter(Boolean).pop();
9
+ return segment ? segment.charAt(0).toUpperCase() + segment.slice(1) : "Home";
10
+ };
11
+
12
+ const showScrollRoutes = router => {
13
+ router.querySelectorAll("route[scroll]").forEach(r => r.style.removeProperty("display"));
14
+ router.querySelectorAll("route:not([scroll])").forEach(r => (r.style.display = "none"));
15
+ };
16
+
4
17
  const handlePopState = async () => {
5
18
  log("Navigation triggered to:", globalThis.location.pathname);
6
19
  document.body.classList.add("loading");
7
- const currentPath = globalThis.location.pathname.replace(/\/$/, ""); // Normalize path by removing trailing slash
20
+ const currentPath = globalThis.location.pathname.replace(/\/$/, "") || "/"; // Normalize path, preserve root "/"
8
21
  const router = document.querySelector("router");
9
22
 
10
23
  let currentRoute = router.querySelector(`route[path="${currentPath}"]`);
11
24
 
25
+ // Handle scroll route navigation — show all scroll routes and smooth-scroll to target
26
+ if (currentRoute?.hasAttribute("scroll")) {
27
+ showScrollRoutes(router);
28
+ document.body.classList.remove("loading");
29
+ currentRoute.scrollIntoView({ behavior: "smooth" });
30
+ if (onRouteChange) onRouteChange(currentPath);
31
+ log("Scroll route navigation completed");
32
+ return;
33
+ }
34
+
12
35
  // If the route doesn't exist in DOM, create and append it
13
36
  if (!currentRoute) {
14
37
  log("Creating new route element for:", currentPath);
@@ -32,11 +55,8 @@ const handlePopState = async () => {
32
55
  const doc = parser.parseFromString(content, "text/html");
33
56
 
34
57
  // Update the page title with the new content's title
35
- const newTitle = doc.querySelector("title");
36
- if (newTitle) {
37
- log("Updating page title to:", newTitle.textContent);
38
- document.title = newTitle.textContent;
39
- }
58
+ document.title = getTitle(doc, currentPath);
59
+ log("Updating page title to:", document.title);
40
60
 
41
61
  currentRoute.innerHTML = doc.body.innerHTML;
42
62
 
@@ -54,9 +74,9 @@ const handlePopState = async () => {
54
74
  }
55
75
  }
56
76
 
57
- // Display only the current route
58
- router.querySelectorAll("route").forEach(route => (route.style.contentVisibility = "hidden"));
59
- currentRoute.style.contentVisibility = "visible";
77
+ // Display only the current route (hides scroll routes too when on a regular route)
78
+ router.querySelectorAll("route").forEach(route => (route.style.display = "none"));
79
+ currentRoute.style.display = "contents";
60
80
 
61
81
  document.body.classList.remove("loading");
62
82
  window.scrollTo(0, 0);
@@ -211,7 +231,7 @@ const startRouter = (options = {}) => {
211
231
  document.head.appendChild(style);
212
232
 
213
233
  let router = document.querySelector("router");
214
- const currentPath = globalThis.location.pathname;
234
+ const currentPath = globalThis.location.pathname.replace(/\/$/, "") || "/";
215
235
 
216
236
  if (!router) {
217
237
  log("Creating new router element");
@@ -244,6 +264,34 @@ const startRouter = (options = {}) => {
244
264
  });
245
265
 
246
266
  observeLinks(observer);
267
+
268
+ // Initialize scroll routes
269
+ const scrollRoutes = router.querySelectorAll("route[scroll]");
270
+ if (scrollRoutes.length) {
271
+ const currentScrollRoute = router.querySelector(`route[path="${currentPath}"][scroll]`);
272
+ if (currentScrollRoute) {
273
+ // Direct URL visit to a scroll route — show all scroll sections and jump instantly
274
+ showScrollRoutes(router);
275
+ currentScrollRoute.scrollIntoView({ behavior: "instant" });
276
+ }
277
+
278
+ // Update URL and title as scroll routes enter the viewport
279
+ const scrollObserver = new IntersectionObserver(
280
+ entries => {
281
+ entries.forEach(entry => {
282
+ if (entry.isIntersecting) {
283
+ const path = entry.target.getAttribute("path") || "/";
284
+ globalThis.history.replaceState(null, null, path);
285
+ document.title = getTitle(entry.target, path);
286
+ if (onRouteChange) onRouteChange(path);
287
+ }
288
+ });
289
+ },
290
+ { threshold: 0.6 }
291
+ );
292
+
293
+ scrollRoutes.forEach(route => scrollObserver.observe(route));
294
+ }
247
295
  };
248
296
 
249
297
  export { startRouter };
package/src/router.html DELETED
@@ -1,25 +0,0 @@
1
- <html lang="en">
2
- <head>
3
- <meta charset="utf-8" />
4
- <meta name="viewport" content="width=device-width,initial-scale=1" />
5
- <title>Router manual test About</title>
6
- </head>
7
- <body style="background-color: bisque">
8
- <menu class="flex m-0 p-0"
9
- ><a prefetch="onHover" href="/">Home</a><a href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
- ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a> </menu
11
- ><router>
12
- <route path="/about.html" style="content-visibility: auto"><h1>About</h1></route><route path="/onlyServer" style="content-visibility: auto"></route
13
- ></router>
14
- <script type="module">
15
- import { startRouter } from "./router.js";
16
- // Initialize the router with a simple onRouteChange callback to test functionality
17
- startRouter({
18
- onRouteChange: currentRoute => {
19
- console.log("Route changed:", currentRoute);
20
- currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
21
- },
22
- });
23
- </script>
24
- </body>
25
- </html>