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 +67 -40
- package/dist/router.min.js +1 -1
- package/package.json +2 -2
- package/src/about.html +74 -12
- package/src/index.html +70 -130
- package/src/router.js +71 -113
- package/src/blog.html +0 -24
- package/src/home.html +0 -25
- package/src/pricing.html +0 -177
- package/src/router.html +0 -25
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
|
-
- π
|
|
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:
|
|
44
|
-
console.log("Route changed:",
|
|
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):
|
|
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:
|
|
92
|
-
console.log("Route changed:",
|
|
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
|
-
|
|
140
|
+
<-- Header menu and parts that don't change -->
|
|
168
141
|
<router>
|
|
169
142
|
<route path="/">home content</route>
|
|
170
143
|
</router>
|
|
171
|
-
|
|
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
|
-
##
|
|
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
|
+
|
package/dist/router.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{let
|
|
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.
|
|
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>
|
|
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
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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:
|
|
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>
|
|
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
|
-
|
|
31
|
-
position: sticky;
|
|
15
|
+
nav {
|
|
16
|
+
position: fixed;
|
|
32
17
|
top: 0;
|
|
33
|
-
|
|
18
|
+
width: 100%;
|
|
34
19
|
display: flex;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
font-
|
|
29
|
+
color: #333;
|
|
30
|
+
font-weight: 500;
|
|
50
31
|
}
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
101
|
-
font-size: clamp(1.75rem, 4vw, 2.25rem);
|
|
47
|
+
section h1 {
|
|
48
|
+
font-size: 3rem;
|
|
102
49
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
50
|
+
section p {
|
|
51
|
+
font-size: 1.25rem;
|
|
52
|
+
color: #555;
|
|
53
|
+
max-width: 600px;
|
|
54
|
+
text-align: center;
|
|
106
55
|
}
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
opacity: 0.85;
|
|
62
|
+
#pricing {
|
|
63
|
+
background: #fff0f4;
|
|
125
64
|
}
|
|
126
65
|
</style>
|
|
127
66
|
</head>
|
|
128
67
|
<body>
|
|
129
|
-
<nav
|
|
130
|
-
<
|
|
131
|
-
<a href="/">
|
|
132
|
-
<a href="/
|
|
133
|
-
<a href="/
|
|
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
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
<h1>
|
|
143
|
-
<p>
|
|
144
|
-
<
|
|
145
|
-
</
|
|
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
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
<
|
|
152
|
-
<p>
|
|
153
|
-
<
|
|
154
|
-
|
|
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
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
<
|
|
161
|
-
<p>
|
|
162
|
-
<
|
|
163
|
-
</
|
|
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
|
-
|
|
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
|
|
1
|
+
let cache = {};
|
|
2
2
|
let debugMode = false;
|
|
3
|
+
let globalTitle = "";
|
|
3
4
|
const log = (...args) => debugMode && console.log("π¦ Router:", ...args);
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
43
|
+
// Fetch and render on first visit
|
|
75
44
|
if (!currentRoute.innerHTML) {
|
|
76
45
|
log("Fetching content for:", globalThis.location.href);
|
|
77
|
-
let content =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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 (!
|
|
92
|
+
if (!cache[link.href]) {
|
|
153
93
|
log("Prefetching content for:", link.href);
|
|
154
|
-
|
|
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: !!
|
|
105
|
+
alreadyCached: !!cache[link.href],
|
|
166
106
|
});
|
|
167
107
|
|
|
168
108
|
if (entry.isIntersecting) {
|
|
169
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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><route scroll></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>
|