lightweight-router 1.0.12 β†’ 1.0.16

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,7 +13,8 @@ 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 routes** β€” multi-section landing pages: full-height `<route scroll>` blocks, smooth in-page navigation, and URL bar sync while scrolling
16
+ - πŸ“œ Scroll route sections with automatic URL/title updates
17
+
17
18
 
18
19
  ## Installation
19
20
 
@@ -35,15 +36,14 @@ Example:
35
36
  ```javascript
36
37
  import { startRouter } from "lightweight-router";
37
38
 
38
- startRouter();
39
+ startRouter()
39
40
 
40
41
  //or with your callback
41
42
 
42
43
  startRouter({
43
- onRouteChange: path => {
44
- console.log("Route changed:", path);
44
+ onRouteChange: currentRoute => {
45
+ console.log("Route changed:", currentRoute);
45
46
  },
46
- debug: true, // optional: log router activity to the console
47
47
  });
48
48
  ```
49
49
 
@@ -75,8 +75,7 @@ Initializes the router with the given options.
75
75
  #### Parameters
76
76
 
77
77
  - `options` (Object): Configuration options for the router.
78
- - `onRouteChange` (Function): Called with the **current path string** when the route changes (including when the URL is updated while scrolling through scroll routes).
79
- - `debug` (Boolean): When `true`, logs navigation and prefetch activity to the console.
78
+ - `onRouteChange` (Function): Callback function to be called when the route changes.
80
79
 
81
80
  ## Examples
82
81
 
@@ -88,20 +87,19 @@ Your website content
88
87
  import { startRouter } from "./router.js";
89
88
 
90
89
  startRouter({
91
- onRouteChange: path => {
92
- console.log("Route changed:", path);
90
+ onRouteChange: currentRoute => {
91
+ console.log("Route changed:", currentRoute);
93
92
  },
94
93
  });
95
94
  </script>
96
95
  ```
97
96
 
98
- See **`src/index.html`** in this repo for a full **scroll routes** landing example (`<route scroll>` sections and nav).
99
-
100
97
  ## Prefetching
101
98
 
102
99
  By default, links are prefetched when they get in the user's screen using an `IntersectionObserver`. This ensures that the content is loaded in the background before the user clicks on the link, providing a smoother navigation experience.
103
100
  This behaviour is automatically disabled if the user has data saving preferences.
104
101
 
102
+
105
103
  If you have too many links at once or too many requests, you can add the `prefetch="onHover"` attribute to your links or some of them (usually links to huge pages that are not often visited):
106
104
 
107
105
  ```html
@@ -113,31 +111,6 @@ The minified version was created with uglify-js, clean.css and then ultra minifi
113
111
  The size of the gzipped version was calculated with: https://dafrok.github.io/gzip-size-online/
114
112
  It's worth to note that nonetheless Terser give better results than uglify-js. The final uglify version packed by packjs.com was the smallest.
115
113
 
116
- ## Scroll routes (landing sections)
117
-
118
- Use this for a **single HTML page** that behaves like several β€œroutes”: tall sections that share one document, each with its own pathname. Mark sections with **`scroll`** on `<route>` and a **`path`** that matches the URL you want in the address bar.
119
-
120
- ```html
121
- <router>
122
- <route scroll path="/">
123
- <!-- hero -->
124
- </route>
125
- <route scroll path="/features">
126
- <!-- features -->
127
- </route>
128
- <route scroll path="/pricing.html">
129
- <!-- pricing -->
130
- </route>
131
- </router>
132
- ```
133
-
134
- **Behavior**
135
-
136
- - When the current URL matches a **`route[scroll]`** `path`, all scroll routes are shown (non-scroll routes inside `<router>` are hidden), and the matching section is scrolled into view. Internal links to another scroll route use **smooth** scrolling.
137
- - While the user scrolls, an **IntersectionObserver** picks the most visible section and updates the URL with **`history.replaceState`** (and fires `onRouteChange`) so the address bar tracks the section in view.
138
- - Empty scroll routes still **fetch** HTML from the server like normal routes (prefetching applies the same way).
139
- - **Deep links**: opening `/features` directly only works if your static server **falls back to this HTML** for unknown paths (for example `npx serve . --single`).
140
-
141
114
  ## Browser Support
142
115
 
143
116
  The router is intended for modern browsers. Required features:
@@ -164,11 +137,11 @@ This allows only the changing part of the document to be updated, improving perf
164
137
  Once you configured your server to respond to this type of request, wrap the part of your document that changes in a `router` tag. Inside the `router` tag, render the current initial route inside a `route` tag like this:
165
138
 
166
139
  ```html
167
- <!-- Header menu and parts that don't change -->
140
+ <-- Header menu and parts that don't change -->
168
141
  <router>
169
142
  <route path="/">home content</route>
170
143
  </router>
171
- <!-- footer etc.. -->
144
+ <-- footer etc.. -->
172
145
  ```
173
146
 
174
147
  You can also prerender most visited routes by rendering them inside the `router` tag in their appropriate `route` tags for faster loading times:
@@ -187,15 +160,69 @@ Right now errors are shown without styling as the content of the page.
187
160
  If you like to use Preact and Deno for easy server config consider using the server side router present in [Singularity](https://github.com/andreafuturi/Singularity/) framework.
188
161
  It does exactly what we're talking about automatically by returning full html renders on normal request (sorrounded by an Index.jsx) and returing only partial route html when route is requested from inside the page (with onlyRoute param).
189
162
 
190
- ## Performance Tips
191
163
 
164
+ ## Performance Tips
192
165
  - Implement server-side partial responses for better bandwidth usage
193
166
  - Consider using the `prefetch="onHover"` attribute for less important links
194
167
 
195
- ## Future Development
168
+ ## Scroll Routes
196
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
+
217
+ ## Future Development
197
218
  - Delay router intialization on first link hover for better performances?
198
219
  - Implement html streaming for faster page load
199
220
  - Cooler Error handling
200
221
  - Disable caching on certain links/routes (that needs to be always up to date)
201
222
  - Cache limiting
223
+
224
+
225
+
226
+
227
+
228
+
@@ -1 +1 @@
1
- (()=>{let c={},o=!1,s=(...e)=>o&&console.log("🚦 Router:",...e),h="auto",t=null,u=0,n=e=>(e??"").replace(/\/$/,""),d=(e,t)=>{var r,o=n(t);for(r of e.querySelectorAll("route[scroll]"))if(n(r.getAttribute("path"))===o)return r;return null},f=()=>{t?.disconnect(),t=null},g=e=>{f();e=[...e.querySelectorAll("route[scroll]")];e.length&&(t=new IntersectionObserver(e=>{var t,r;Date.now()<u||(e=e.filter(e=>e.isIntersecting)).length&&(e.sort((e,t)=>t.intersectionRatio-e.intersectionRatio),e=e[0].target.getAttribute("path"))&&(t=n(e))!==n(globalThis.location.pathname)&&((r=new URL(globalThis.location.href)).pathname=e.startsWith("/")?e:"/"+e,globalThis.history.replaceState(null,"",r.pathname+r.search+r.hash),s("πŸ“ Scroll URL sync:",t),y)&&y(t)},{root:null,rootMargin:"-38% 0px -38% 0px",threshold:[0,.1,.25,.5,.75,1]}),e.forEach(e=>t.observe(e)))},a=async()=>{s("Navigation triggered to:",globalThis.location.pathname),document.body.classList.add("loading");var e=globalThis.location.pathname.replace(/\/$/,"");let t=document.querySelector("router"),r=t.querySelector(`route[path="${e}"]`);if(r||(s("Creating new route element for:",e),(r=document.createElement("route")).setAttribute("path",e),t.appendChild(r)),!r.innerHTML){s("Fetching content for:",globalThis.location.href);let e=c[globalThis.location.href];e||(e=await v(globalThis.location.href),c[globalThis.location.href]=e);var o,n=(new DOMParser).parseFromString(e,"text/html"),a=n.querySelector("title"),a=(a&&(s("Updating page title to:",a.textContent),document.title=a.textContent),r.innerHTML=n.body.innerHTML,Array.from(r.querySelectorAll("script")));s("Executing",a.length,"scripts from fetched content");for(o of a){var l=document.createElement("script");o.src?l.src=o.src:l.textContent=o.textContent,o.parentNode.replaceChild(l,o)}}n=[...t.querySelectorAll("route[scroll]")],a=[...t.querySelectorAll("route:not([scroll])")];let i=d(t,e);if(Boolean(i)){n.forEach(e=>{e.style.display=""}),a.forEach(e=>{e.style.display="none"});let e=h;h="auto","smooth"===e&&(u=Date.now()+550),requestAnimationFrame(()=>{i.scrollIntoView({behavior:e,block:"start"}),requestAnimationFrame(()=>g(t))})}else f(),n.forEach(e=>{e.style.display="none"}),t.querySelectorAll("route").forEach(e=>e.style.display="none"),r.style.display="contents",window.scrollTo(0,0);document.body.classList.remove("loading"),y&&y(e),s("Route change completed")},l=async e=>{c[e.href]||(s("Prefetching content for:",e.href),c[e.href]=await v(e.href))},i=(e,r)=>{s("πŸ” Intersection Observer triggered for",e.length,"entries"),e.forEach(e=>{var t=e.target;s(`🎯 Link ${t.href} intersection:`,{isIntersecting:e.isIntersecting,intersectionRatio:e.intersectionRatio,alreadyCached:!!c[t.href]}),e.isIntersecting&&(c[t.href]?s("πŸ“¦ Content already cached for:",t.href):(l(t),s("πŸ‘οΈ Unobserving link after prefetch initiated:",t.href),r.unobserve(t)))})},p=e=>{var t,r,o=e.target.closest("A");o&&o.href&&m(o.href)&&o.origin===location.origin?(t=document.querySelector("router"),r=new URL(o.href).pathname.replace(/\/$/,""),t&&d(t,r)?(s("πŸ”— Scroll-route link β†’ smooth scroll:",o.href),e.preventDefault(),h="smooth"):(s("Internal link clicked:",o.href),e.preventDefault()),globalThis.history.pushState(null,null,o.href),globalThis.dispatchEvent(new Event("popstate"))):s("Invalid link click:",o?.href)};function m(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 y,v=async e=>{e=await r(e);return e.ok?e.text():"Couldn't fetch the route - HTTP error! status: "+e.status},r=async e=>{if(!b){var t=await fetch(e,{method:"POST",body:"onlyRoute"});if(t.ok)return t}return fetch(e)},b=!1,e=(e={})=>{var{onRouteChange:t,debug:r}=e,e=(o=r,s("Router starting...",e),t&&(r=t,y=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||(s("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),b=!0),globalThis.addEventListener("popstate",a),document.addEventListener("click",p),document.body.addEventListener("mouseover",e=>{"A"===e.target.tagName&&"onHover"===e.target.getAttribute("prefetch")&&(async e=>{e=e.target;!c[e.href]&&m(e.href)&&await l(e)})(e)}),new IntersectionObserver(i,{root:null,threshold:.5}));s("🎭 Created Intersection Observer with config:",{root:"viewport",threshold:.5}),(r=>{let o=navigator.connection&&navigator.connection.saveData;var e=document.querySelectorAll("a");s("πŸ”„ Starting link observation...",{totalLinks:e.length,saveDataMode:o}),e.forEach(e=>{var t="onHover"!==e.getAttribute("prefetch")&&!o&&m(e.href);s("πŸ”— Link evaluation:",{href:e.href,prefetchAttr:e.getAttribute("prefetch"),isInternal:m(e.href),willObserve:t}),t&&(r.observe(e),s("πŸ‘€ Now observing link:",e.href))})})(r),a()};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()})();
1
+ (()=>{let i={},a=!1,l="",c=(...e)=>a&&console.log("🚦 Router:",...e),s=(e,t)=>{var e=e?.querySelector("title");return e?e.textContent.split("|")[0].trim():(e=t.replace(/\.html$/,"").split("/").filter(Boolean).pop())?e.charAt(0).toUpperCase()+e.slice(1):"Home"},h=e=>{e.querySelectorAll("route[scroll]").forEach(e=>e.style.removeProperty("display")),e.querySelectorAll("route:not([scroll])").forEach(e=>e.style.display="none")},d=async()=>{c("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"))h(e),document.body.classList.remove("loading"),r.scrollIntoView({behavior:"smooth"}),m&&m(t),c("Scroll route navigation completed");else{if(r||((r=document.createElement("route")).setAttribute("path",t),e.appendChild(r),c("Created route element for:",t)),!r.innerHTML){c("Fetching content for:",globalThis.location.href);let e=i[globalThis.location.href];e||(e=await v(globalThis.location.href),i[globalThis.location.href]=e);var o,n=(new DOMParser).parseFromString(e,"text/html"),n=(r.dataset.routeTitle=s(n,t),r.innerHTML=n.body.innerHTML,Array.from(r.querySelectorAll("script")));c("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";n=r.dataset.routeTitle||s(r,t);document.title=l?n+" | "+l:n,document.body.classList.remove("loading"),window.scrollTo(0,0),m&&m(t),c("Route change completed")}},u=async e=>{i[e.href]||(c("Prefetching content for:",e.href),i[e.href]=await v(e.href))},f=(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:!!i[t.href]}),e.isIntersecting&&(i[t.href]?c("πŸ“¦ Content already cached for:",t.href):(u(t),c("πŸ‘οΈ Unobserving link after prefetch initiated:",t.href),r.unobserve(t)))})},p=e=>{var t=e.target.closest("A");t&&t.href&&g(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 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 m,v=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(!y){var t=await fetch(e,{method:"POST",body:"onlyRoute"});if(t.ok)return t}return fetch(e)},y=!1,e=(e={})=>{let{onRouteChange:r,debug:t}=e;a=t;var o=document.querySelector("title")?.textContent?.trim()??"",e=(l=o.includes("|")?o.split("|").at(-1).trim():o||"App",c("Router starting...",e),r&&(o=r,m=o),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 n=document.querySelector("router");o=globalThis.location.pathname.replace(/\/$/,"")||"/",n||(c("Creating new router element"),n=document.createElement("router"),(e=document.createElement("route")).setAttribute("path",o),e.innerHTML=document.body.innerHTML,n.appendChild(e),document.body.innerHTML="",document.body.appendChild(n),y=!0),globalThis.addEventListener("popstate",d),document.addEventListener("click",p),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 u(e)})(e)}),e=new IntersectionObserver(f,{root:null,threshold:.5}),c("🎭 Created Intersection Observer with config:",{root:"viewport",threshold:.5}),(r=>{let o=navigator.connection&&navigator.connection.saveData;var e=document.querySelectorAll("a");c("πŸ”„ Starting link observation...",{totalLinks:e.length,saveDataMode:o}),e.forEach(e=>{var t="onHover"!==e.getAttribute("prefetch")&&!o&&g(e.href);c("πŸ”— Link evaluation:",{href:e.href,prefetchAttr:e.getAttribute("prefetch"),isInternal:g(e.href),willObserve:t}),t&&(r.observe(e),c("πŸ‘€ Now observing link:",e.href))})})(e),e=n.querySelectorAll("route[scroll]");if(e.length){o=n.querySelector(`route[path="${o}"][scroll]`);o&&(h(n),o.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),e=s(e.target,t),document.title=l?e+" | "+l:e,r)&&r(t)})},{threshold:.6});e.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.12",
3
+ "version": "1.0.16",
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
@@ -1,176 +1,116 @@
1
- <!doctype html>
2
- <!-- Serve this file so the URL path is /landingpage.html (e.g. npx serve src). If your path is /src/landingpage.html, update route path="/landingpage.html" and the Hero nav href to match. -->
3
1
  <html lang="en">
4
2
  <head>
5
3
  <meta charset="utf-8" />
6
4
  <meta name="viewport" content="width=device-width,initial-scale=1" />
7
- <title>Scroll routes β€” landing demo</title>
5
+ <title>Home | PageFlick Demo</title>
8
6
  <style>
9
- :root {
10
- --bg: #0f1419;
11
- --surface: #1a2332;
12
- --text: #e8eef5;
13
- --muted: #8b9cb3;
14
- --accent: #3dd6c3;
15
- --nav-h: 3.25rem;
16
- }
17
-
18
7
  * {
19
8
  box-sizing: border-box;
9
+ margin: 0;
10
+ padding: 0;
20
11
  }
21
-
22
12
  body {
23
- margin: 0;
24
13
  font-family: system-ui, sans-serif;
25
- background: var(--bg);
26
- color: var(--text);
27
- line-height: 1.5;
28
14
  }
29
-
30
- .site-nav {
31
- position: sticky;
15
+ nav {
16
+ position: fixed;
32
17
  top: 0;
33
- z-index: 10;
18
+ width: 100%;
34
19
  display: flex;
35
- flex-wrap: wrap;
36
- align-items: center;
37
- gap: 0.5rem 1rem;
38
- min-height: var(--nav-h);
39
- padding: 0.5rem 1rem;
40
- background: color-mix(in srgb, var(--bg) 92%, transparent);
20
+ gap: 1rem;
21
+ padding: 1rem 2rem;
22
+ background: rgba(255, 255, 255, 0.9);
41
23
  backdrop-filter: blur(8px);
42
- border-bottom: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
24
+ z-index: 100;
25
+ border-bottom: 1px solid #eee;
43
26
  }
44
-
45
- .site-nav a {
46
- color: var(--accent);
27
+ nav a {
47
28
  text-decoration: none;
48
- font-weight: 600;
49
- font-size: 0.9rem;
29
+ color: #333;
30
+ font-weight: 500;
50
31
  }
51
-
52
- .site-nav a:hover,
53
- .site-nav a:focus-visible {
54
- text-decoration: underline;
55
- outline: none;
56
- }
57
-
58
- .site-nav .label {
59
- color: var(--muted);
60
- font-size: 0.75rem;
61
- margin-right: 0.25rem;
32
+ nav a:hover {
33
+ color: #000;
62
34
  }
63
-
64
35
  route[scroll] {
65
36
  display: block;
66
- min-height: 100vh;
67
- scroll-margin-top: calc(var(--nav-h) + 0.75rem);
68
- padding: 2rem 1.25rem 4rem;
69
- border-bottom: 1px solid color-mix(in srgb, var(--text) 8%, transparent);
70
- }
71
-
72
- route[scroll]:nth-of-type(odd) {
73
- background: linear-gradient(165deg, var(--surface) 0%, var(--bg) 100%);
74
37
  }
75
-
76
- route[scroll]:nth-of-type(even) {
77
- background: linear-gradient(-5deg, #121a24 0%, var(--surface) 100%);
78
- }
79
-
80
- .section-inner {
81
- max-width: 42rem;
82
- margin: 0 auto;
83
- }
84
-
85
- .eyebrow {
86
- font-size: 0.75rem;
87
- letter-spacing: 0.12em;
88
- text-transform: uppercase;
89
- color: var(--muted);
90
- margin: 0 0 0.5rem;
91
- }
92
-
93
- h1,
94
- h2 {
95
- margin: 0 0 1rem;
96
- font-weight: 700;
97
- letter-spacing: -0.02em;
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;
98
46
  }
99
-
100
- h1 {
101
- font-size: clamp(1.75rem, 4vw, 2.25rem);
47
+ section h1 {
48
+ font-size: 3rem;
102
49
  }
103
-
104
- h2 {
105
- font-size: clamp(1.35rem, 3vw, 1.75rem);
50
+ section p {
51
+ font-size: 1.25rem;
52
+ color: #555;
53
+ max-width: 600px;
54
+ text-align: center;
106
55
  }
107
-
108
- p {
109
- margin: 0 0 1rem;
110
- color: color-mix(in srgb, var(--text) 88%, var(--muted));
56
+ #hero {
57
+ background: #f0f4ff;
111
58
  }
112
-
113
- .pill {
114
- display: inline-block;
115
- padding: 0.2rem 0.6rem;
116
- border-radius: 999px;
117
- font-size: 0.7rem;
118
- background: color-mix(in srgb, var(--accent) 18%, transparent);
119
- color: var(--accent);
120
- margin-bottom: 1rem;
59
+ #features {
60
+ background: #f0fff4;
121
61
  }
122
-
123
- .loading body {
124
- opacity: 0.85;
62
+ #pricing {
63
+ background: #fff0f4;
125
64
  }
126
65
  </style>
127
66
  </head>
128
67
  <body>
129
- <nav class="site-nav" aria-label="Scroll sections">
130
- <span class="label">Jump</span>
131
- <a href="/">Hero</a>
132
- <a href="/landing-features">Features</a>
133
- <a href="/pricing.html">Pricing</a>
134
- <a href="/blog">Blog</a>
135
- <span class="label" style="margin-left: auto"><a href="index.html">← Index</a></span>
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>
136
73
  </nav>
137
74
 
138
75
  <router>
139
- <route scroll path="/">
140
- <div class="section-inner">
141
- <p class="eyebrow">Scroll mode</p>
142
- <h1>Landing hero</h1>
143
- <p>Each <code>&lt;route scroll&gt;</code> is a tall section. Internal links smooth-scroll; all sections stay in the DOM.</p>
144
- <p class="pill">min-height: 100vh</p>
145
- </div>
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>
146
83
  </route>
147
84
 
148
- <route scroll path="/landing-features">
149
- <div class="section-inner">
150
- <p class="eyebrow">Section 2</p>
151
- <h2>Features</h2>
152
- <p>Lazy content loads when this block is near the viewport (IntersectionObserver + 200px) or when you hover a linkβ€”same as the rest of the router.</p>
153
- <p class="pill">min-height: 100vh</p>
154
- </div>
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>
155
98
  </route>
156
99
 
157
- <route scroll path="/pricing.html">
158
- <div class="section-inner">
159
- <p class="eyebrow">Section 3</p>
160
- <h2>Pricing</h2>
161
- <p>Empty routes fetch HTML from the server when you land here; inline placeholders skip the network.</p>
162
- <p class="pill">min-height: 100vh</p>
163
- </div>
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>
164
107
  </route>
165
108
  </router>
166
109
 
167
110
  <script type="module">
168
111
  import { startRouter } from "./router.js";
169
112
  startRouter({
170
- debug: true,
171
- onRouteChange: path => {
172
- console.log(`πŸ“ Route (scroll demo): ${path}`);
173
- },
113
+ onRouteChange: path => console.log("Route:", path),
174
114
  });
175
115
  </script>
176
116
  </body>
package/src/router.js CHANGED
@@ -1,97 +1,60 @@
1
- let linkData = {};
1
+ let cache = {};
2
2
  let debugMode = false;
3
+ let globalTitle = "";
3
4
  const log = (...args) => debugMode && console.log("🚦 Router:", ...args);
4
5
 
5
- /** @type {'auto' | 'smooth'} */
6
- let pendingScrollBehavior = "auto";
7
- let scrollUrlObserver = null;
8
- let suppressScrollUrlSyncUntil = 0;
9
-
10
- const normalizePath = p => {
11
- const s = (p ?? "").replace(/\/$/, "");
12
- return s;
13
- };
14
-
15
- /** @param {Element} router */
16
- const pathToScrollRoute = (router, path) => {
17
- const n = normalizePath(path);
18
- for (const r of router.querySelectorAll("route[scroll]")) {
19
- if (normalizePath(r.getAttribute("path")) === n) return r;
20
- }
21
- return null;
22
- };
23
-
24
- const teardownScrollUrlObserver = () => {
25
- scrollUrlObserver?.disconnect();
26
- scrollUrlObserver = null;
6
+ const getTitle = (el, path) => {
7
+ const tag = el?.querySelector("title");
8
+ if (tag) return tag.textContent.split("|")[0].trim();
9
+ const segment = path.replace(/\.html$/, "").split("/").filter(Boolean).pop();
10
+ return segment ? segment.charAt(0).toUpperCase() + segment.slice(1) : "Home";
27
11
  };
28
12
 
29
- /** @param {Element} router */
30
- const setupScrollUrlObserver = router => {
31
- teardownScrollUrlObserver();
32
- const scrollRoutes = [...router.querySelectorAll("route[scroll]")];
33
- if (!scrollRoutes.length) return;
34
-
35
- scrollUrlObserver = new IntersectionObserver(
36
- entries => {
37
- if (Date.now() < suppressScrollUrlSyncUntil) return;
38
- const visible = entries.filter(e => e.isIntersecting);
39
- if (!visible.length) return;
40
- visible.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
41
- const best = visible[0].target;
42
- const pathAttr = best.getAttribute("path");
43
- if (!pathAttr) return;
44
- const nextPath = normalizePath(pathAttr);
45
- const here = normalizePath(globalThis.location.pathname);
46
- if (nextPath === here) return;
47
- const u = new URL(globalThis.location.href);
48
- u.pathname = pathAttr.startsWith("/") ? pathAttr : `/${pathAttr}`;
49
- globalThis.history.replaceState(null, "", u.pathname + u.search + u.hash);
50
- log("πŸ“ Scroll URL sync:", nextPath);
51
- if (onRouteChange) onRouteChange(nextPath);
52
- },
53
- { root: null, rootMargin: "-38% 0px -38% 0px", threshold: [0, 0.1, 0.25, 0.5, 0.75, 1] },
54
- );
55
- scrollRoutes.forEach(r => scrollUrlObserver.observe(r));
13
+ const showScrollRoutes = router => {
14
+ router.querySelectorAll("route[scroll]").forEach(r => r.style.removeProperty("display"));
15
+ router.querySelectorAll("route:not([scroll])").forEach(r => (r.style.display = "none"));
56
16
  };
57
17
 
58
18
  const handlePopState = async () => {
59
19
  log("Navigation triggered to:", globalThis.location.pathname);
60
20
  document.body.classList.add("loading");
61
- const currentPath = globalThis.location.pathname.replace(/\/$/, ""); // Normalize path by removing trailing slash
21
+ const currentPath = globalThis.location.pathname.replace(/\/$/, "") || "/"; // Normalize path, preserve root "/"
62
22
  const router = document.querySelector("router");
63
23
 
64
24
  let currentRoute = router.querySelector(`route[path="${currentPath}"]`);
65
25
 
66
- // If the route doesn't exist in DOM, create and append it
26
+ // Handle scroll route navigation β€” show all scroll routes and smooth-scroll to target
27
+ if (currentRoute?.hasAttribute("scroll")) {
28
+ showScrollRoutes(router);
29
+ document.body.classList.remove("loading");
30
+ currentRoute.scrollIntoView({ behavior: "smooth" });
31
+ if (onRouteChange) onRouteChange(currentPath);
32
+ log("Scroll route navigation completed");
33
+ return;
34
+ }
35
+
67
36
  if (!currentRoute) {
68
- log("Creating new route element for:", currentPath);
69
37
  currentRoute = document.createElement("route");
70
38
  currentRoute.setAttribute("path", currentPath);
71
39
  router.appendChild(currentRoute);
40
+ log("Created route element for:", currentPath);
72
41
  }
73
42
 
74
- // Only fetch and render content if the route is empty
43
+ // Fetch and render on first visit
75
44
  if (!currentRoute.innerHTML) {
76
45
  log("Fetching content for:", globalThis.location.href);
77
- let content = linkData[globalThis.location.href];
46
+ let content = cache[globalThis.location.href];
78
47
 
79
48
  // Fetch content if it's not already cached
80
49
  if (!content) {
81
50
  content = await fetchContent(globalThis.location.href);
82
- linkData[globalThis.location.href] = content;
51
+ cache[globalThis.location.href] = content;
83
52
  }
84
53
 
85
54
  const parser = new DOMParser();
86
55
  const doc = parser.parseFromString(content, "text/html");
87
56
 
88
- // Update the page title with the new content's title
89
- const newTitle = doc.querySelector("title");
90
- if (newTitle) {
91
- log("Updating page title to:", newTitle.textContent);
92
- document.title = newTitle.textContent;
93
- }
94
-
57
+ currentRoute.dataset.routeTitle = getTitle(doc, currentPath);
95
58
  currentRoute.innerHTML = doc.body.innerHTML;
96
59
 
97
60
  // Execute scripts from the fetched content
@@ -108,38 +71,15 @@ const handlePopState = async () => {
108
71
  }
109
72
  }
110
73
 
111
- const scrollRoutes = [...router.querySelectorAll("route[scroll]")];
112
- const nonScrollRoutes = [...router.querySelectorAll("route:not([scroll])")];
113
- const scrollTarget = pathToScrollRoute(router, currentPath);
114
- const inScrollMode = Boolean(scrollTarget);
74
+ // Show current route, hide the rest
75
+ router.querySelectorAll("route").forEach(route => (route.style.display = "none"));
76
+ currentRoute.style.display = "contents";
115
77
 
116
- if (inScrollMode) {
117
- scrollRoutes.forEach(r => {
118
- r.style.display = "";
119
- });
120
- nonScrollRoutes.forEach(r => {
121
- r.style.display = "none";
122
- });
123
- const behavior = pendingScrollBehavior;
124
- pendingScrollBehavior = "auto";
125
- if (behavior === "smooth") {
126
- suppressScrollUrlSyncUntil = Date.now() + 550;
127
- }
128
- requestAnimationFrame(() => {
129
- scrollTarget.scrollIntoView({ behavior, block: "start" });
130
- requestAnimationFrame(() => setupScrollUrlObserver(router));
131
- });
132
- } else {
133
- teardownScrollUrlObserver();
134
- scrollRoutes.forEach(r => {
135
- r.style.display = "none";
136
- });
137
- router.querySelectorAll("route").forEach(route => (route.style.display = "none"));
138
- currentRoute.style.display = "contents";
139
- window.scrollTo(0, 0);
140
- }
78
+ const routeLabel = currentRoute.dataset.routeTitle || getTitle(currentRoute, currentPath);
79
+ document.title = globalTitle ? `${routeLabel} | ${globalTitle}` : routeLabel;
141
80
 
142
81
  document.body.classList.remove("loading");
82
+ window.scrollTo(0, 0);
143
83
 
144
84
  // Call the route change handler if it's set
145
85
  if (onRouteChange) onRouteChange(currentPath);
@@ -149,9 +89,9 @@ const handlePopState = async () => {
149
89
  //link management
150
90
 
151
91
  const fetchAndSaveContent = async link => {
152
- if (!linkData[link.href]) {
92
+ if (!cache[link.href]) {
153
93
  log("Prefetching content for:", link.href);
154
- linkData[link.href] = await fetchContent(link.href);
94
+ cache[link.href] = await fetchContent(link.href);
155
95
  }
156
96
  };
157
97
 
@@ -162,11 +102,11 @@ const handleLinkIntersection = (entries, observer) => {
162
102
  log(`🎯 Link ${link.href} intersection:`, {
163
103
  isIntersecting: entry.isIntersecting,
164
104
  intersectionRatio: entry.intersectionRatio,
165
- alreadyCached: !!linkData[link.href],
105
+ alreadyCached: !!cache[link.href],
166
106
  });
167
107
 
168
108
  if (entry.isIntersecting) {
169
- if (!linkData[link.href]) {
109
+ if (!cache[link.href]) {
170
110
  fetchAndSaveContent(link);
171
111
  log("πŸ‘οΈ Unobserving link after prefetch initiated:", link.href);
172
112
  observer.unobserve(link);
@@ -179,7 +119,7 @@ const handleLinkIntersection = (entries, observer) => {
179
119
 
180
120
  const handleLinkHover = async event => {
181
121
  const link = event.target;
182
- if (!linkData[link.href] && isInternalLink(link.href)) {
122
+ if (!cache[link.href] && isInternalLink(link.href)) {
183
123
  await fetchAndSaveContent(link);
184
124
  }
185
125
  };
@@ -190,17 +130,6 @@ const handleLinkClick = e => {
190
130
  log("Invalid link click:", link?.href);
191
131
  return;
192
132
  }
193
- const router = document.querySelector("router");
194
- const url = new URL(link.href);
195
- const targetPath = url.pathname.replace(/\/$/, "");
196
- if (router && pathToScrollRoute(router, targetPath)) {
197
- log("πŸ”— Scroll-route link β†’ smooth scroll:", link.href);
198
- e.preventDefault();
199
- pendingScrollBehavior = "smooth";
200
- globalThis.history.pushState(null, null, link.href);
201
- globalThis.dispatchEvent(new Event("popstate"));
202
- return;
203
- }
204
133
  log("Internal link clicked:", link.href);
205
134
  e.preventDefault();
206
135
  globalThis.history.pushState(null, null, link.href);
@@ -273,17 +202,19 @@ const fetchContent = async url => {
273
202
 
274
203
  // Updated fetchWithFallback to check the flag
275
204
  const fetchWithFallback = async url => {
276
- if (!routerCreatedManually) {
205
+ if (!manualMode) {
277
206
  const res = await fetch(url, { method: "POST", body: "onlyRoute" });
278
207
  if (res.ok) return res;
279
208
  }
280
209
  return await fetch(url);
281
210
  };
282
211
 
283
- let routerCreatedManually = false;
212
+ let manualMode = false;
284
213
  const startRouter = (options = {}) => {
285
214
  const { onRouteChange, debug } = options;
286
215
  debugMode = debug;
216
+ const pageTitle = document.querySelector("title")?.textContent?.trim() ?? "";
217
+ globalTitle = pageTitle.includes("|") ? pageTitle.split("|").at(-1).trim() : pageTitle || "App";
287
218
  log("Router starting...", options);
288
219
  if (onRouteChange) setRouteChangeHandler(onRouteChange);
289
220
  const style = document.createElement("style");
@@ -302,7 +233,7 @@ const startRouter = (options = {}) => {
302
233
  document.head.appendChild(style);
303
234
 
304
235
  let router = document.querySelector("router");
305
- const currentPath = globalThis.location.pathname;
236
+ const currentPath = globalThis.location.pathname.replace(/\/$/, "") || "/";
306
237
 
307
238
  if (!router) {
308
239
  log("Creating new router element");
@@ -313,7 +244,7 @@ const startRouter = (options = {}) => {
313
244
  router.appendChild(route);
314
245
  document.body.innerHTML = "";
315
246
  document.body.appendChild(router);
316
- routerCreatedManually = true;
247
+ manualMode = true;
317
248
  }
318
249
 
319
250
  globalThis.addEventListener("popstate", handlePopState);
@@ -336,7 +267,34 @@ const startRouter = (options = {}) => {
336
267
 
337
268
  observeLinks(observer);
338
269
 
339
- void handlePopState();
270
+ // Initialize scroll routes
271
+ const scrollRoutes = router.querySelectorAll("route[scroll]");
272
+ if (scrollRoutes.length) {
273
+ const currentScrollRoute = router.querySelector(`route[path="${currentPath}"][scroll]`);
274
+ if (currentScrollRoute) {
275
+ // Direct URL visit to a scroll route β€” show all scroll sections and jump instantly
276
+ showScrollRoutes(router);
277
+ currentScrollRoute.scrollIntoView({ behavior: "instant" });
278
+ }
279
+
280
+ // Update URL and title as scroll routes enter the viewport
281
+ const scrollObserver = new IntersectionObserver(
282
+ entries => {
283
+ entries.forEach(entry => {
284
+ if (entry.isIntersecting) {
285
+ const path = entry.target.getAttribute("path") || "/";
286
+ globalThis.history.replaceState(null, null, path);
287
+ const label = getTitle(entry.target, path);
288
+ document.title = globalTitle ? `${label} | ${globalTitle}` : label;
289
+ if (onRouteChange) onRouteChange(path);
290
+ }
291
+ });
292
+ },
293
+ { threshold: 0.6 }
294
+ );
295
+
296
+ scrollRoutes.forEach(route => scrollObserver.observe(route));
297
+ }
340
298
  };
341
299
 
342
300
  export { startRouter };
package/src/blog.html DELETED
@@ -1,24 +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 Blog</title>
6
- </head>
7
- <body style="background-color: bisque">
8
- <menu class="flex m-0 p-0"></menu>
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
- BLOG
13
- <script type="module">
14
- import { startRouter } from "./router.js";
15
- // Initialize the router with a simple onRouteChange callback to test functionality
16
- 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
- },
21
- });
22
- </script>
23
- </body>
24
- </html>
package/src/home.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 Index</title>
6
- </head>
7
- <body>
8
- <menu class="flex m-0 p-0"></menu>
9
- <a prefetch="onHover" href="/home.html">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
- <a href="/">Landingpage</a>
12
- </menu>
13
- HOME
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
- document.querySelector(`route[path='${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>
package/src/pricing.html DELETED
@@ -1,177 +0,0 @@
1
- <!doctype html>
2
- <!-- Serve this file so the URL path is /landingpage.html (e.g. npx serve src). If your path is /src/landingpage.html, update route path="/landingpage.html" and the Hero nav href to match. -->
3
- <html lang="en">
4
- <head>
5
- <meta charset="utf-8" />
6
- <meta name="viewport" content="width=device-width,initial-scale=1" />
7
- <title>Pricing</title>
8
- <style>
9
- :root {
10
- --bg: #0f1419;
11
- --surface: #1a2332;
12
- --text: #e8eef5;
13
- --muted: #8b9cb3;
14
- --accent: #3dd6c3;
15
- --nav-h: 3.25rem;
16
- }
17
-
18
- * {
19
- box-sizing: border-box;
20
- }
21
-
22
- body {
23
- margin: 0;
24
- font-family: system-ui, sans-serif;
25
- background: var(--bg);
26
- color: var(--text);
27
- line-height: 1.5;
28
- }
29
-
30
- .site-nav {
31
- position: sticky;
32
- top: 0;
33
- z-index: 10;
34
- display: flex;
35
- flex-wrap: wrap;
36
- align-items: center;
37
- gap: 0.5rem 1rem;
38
- min-height: var(--nav-h);
39
- padding: 0.5rem 1rem;
40
- background: color-mix(in srgb, var(--bg) 92%, transparent);
41
- backdrop-filter: blur(8px);
42
- border-bottom: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
43
- }
44
-
45
- .site-nav a {
46
- color: var(--accent);
47
- text-decoration: none;
48
- font-weight: 600;
49
- font-size: 0.9rem;
50
- }
51
-
52
- .site-nav a:hover,
53
- .site-nav a:focus-visible {
54
- text-decoration: underline;
55
- outline: none;
56
- }
57
-
58
- .site-nav .label {
59
- color: var(--muted);
60
- font-size: 0.75rem;
61
- margin-right: 0.25rem;
62
- }
63
-
64
- route[scroll] {
65
- display: block;
66
- min-height: 100vh;
67
- scroll-margin-top: calc(var(--nav-h) + 0.75rem);
68
- padding: 2rem 1.25rem 4rem;
69
- border-bottom: 1px solid color-mix(in srgb, var(--text) 8%, transparent);
70
- }
71
-
72
- route[scroll]:nth-of-type(odd) {
73
- background: linear-gradient(165deg, var(--surface) 0%, var(--bg) 100%);
74
- }
75
-
76
- route[scroll]:nth-of-type(even) {
77
- background: linear-gradient(-5deg, #121a24 0%, var(--surface) 100%);
78
- }
79
-
80
- .section-inner {
81
- max-width: 42rem;
82
- margin: 0 auto;
83
- }
84
-
85
- .eyebrow {
86
- font-size: 0.75rem;
87
- letter-spacing: 0.12em;
88
- text-transform: uppercase;
89
- color: var(--muted);
90
- margin: 0 0 0.5rem;
91
- }
92
-
93
- h1,
94
- h2 {
95
- margin: 0 0 1rem;
96
- font-weight: 700;
97
- letter-spacing: -0.02em;
98
- }
99
-
100
- h1 {
101
- font-size: clamp(1.75rem, 4vw, 2.25rem);
102
- }
103
-
104
- h2 {
105
- font-size: clamp(1.35rem, 3vw, 1.75rem);
106
- }
107
-
108
- p {
109
- margin: 0 0 1rem;
110
- color: color-mix(in srgb, var(--text) 88%, var(--muted));
111
- }
112
-
113
- .pill {
114
- display: inline-block;
115
- padding: 0.2rem 0.6rem;
116
- border-radius: 999px;
117
- font-size: 0.7rem;
118
- background: color-mix(in srgb, var(--accent) 18%, transparent);
119
- color: var(--accent);
120
- margin-bottom: 1rem;
121
- }
122
-
123
- .loading body {
124
- opacity: 0.85;
125
- }
126
- </style>
127
- </head>
128
- <body>
129
- <nav class="site-nav" aria-label="Scroll sections">
130
- <span class="label">Jump</span>
131
- <a href="/">Hero</a>
132
- <a href="/landing-features">Features</a>
133
- <a href="/pricing.html">Pricing</a>
134
- <a href="/blog">Blog</a>
135
- <span class="label" style="margin-left: auto"><a href="index.html" data-router-ignore>← Index</a></span>
136
- </nav>
137
-
138
- <router>
139
- <route scroll path="/">
140
- <div class="section-inner">
141
- <p class="eyebrow">Scroll mode</p>
142
- <h1>Landing hero</h1>
143
- <p>Each <code>&lt;route scroll&gt;</code> is a tall section. Internal links smooth-scroll; all sections stay in the DOM.</p>
144
- <p class="pill">min-height: 100vh</p>
145
- </div>
146
- </route>
147
-
148
- <route scroll path="/landing-features">
149
- <div class="section-inner">
150
- <p class="eyebrow">Section 2</p>
151
- <h2>Features</h2>
152
- <p>Lazy content loads when this block is near the viewport (IntersectionObserver + 200px) or when you hover a linkβ€”same as the rest of the router.</p>
153
- <p class="pill">min-height: 100vh</p>
154
- </div>
155
- </route>
156
-
157
- <route scroll path="/pricing.html">
158
- <div class="section-inner">
159
- <p class="eyebrow">Section 3</p>
160
- <h2>Pricing</h2>
161
- <p>Empty routes fetch HTML from the server when you land here; inline placeholders skip the network.</p>
162
- <p class="pill">min-height: 100vh</p>
163
- </div>
164
- </route>
165
- </router>
166
-
167
- <script type="module">
168
- import { startRouter } from "./router.js";
169
- startRouter({
170
- debug: true,
171
- onRouteChange: path => {
172
- console.log(`πŸ“ Route (scroll demo): ${path}`);
173
- },
174
- });
175
- </script>
176
- </body>
177
- </html>
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>