@seip/blue-bird 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +20 -1
- package/README.md +61 -0
- package/backend/routes/frontend.js +14 -4
- package/core/template.js +73 -8
- package/docker-compose.yml +1 -1
- package/frontend/public/js/blue-bird.js +1465 -0
- package/frontend/templates/about.html +5 -1
- package/frontend/templates/index.html +7 -1
- package/frontend/templates/preact_example.html +80 -0
- package/package.json +1 -1
- package/frontend/public/js/spa.js +0 -183
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
<meta name="author" content="{{author}}">
|
|
12
12
|
<link rel="icon" href="/favicon.ico" />
|
|
13
13
|
<script src="/js/tailwind.js"></script>
|
|
14
|
+
<!-- Enable or disable SPA transitions globally (default: true) -->
|
|
15
|
+
<script>window.enabledAnime = true;</script>
|
|
16
|
+
<script src="/js/blue-bird.js"></script>
|
|
14
17
|
</head>
|
|
15
18
|
|
|
16
19
|
<body class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white">
|
|
@@ -35,11 +38,12 @@
|
|
|
35
38
|
<nav class="flex space-x-8 text-sm font-medium text-slate-300">
|
|
36
39
|
<a href="/" class="hover:text-blue-400 transition-colors">Home</a>
|
|
37
40
|
<a href="/about" class="hover:text-blue-400 transition-colors">About</a>
|
|
41
|
+
<a href="/preact_example" class="hover:text-blue-400 transition-colors">Preact Example</a>
|
|
38
42
|
</nav>
|
|
39
43
|
</div>
|
|
40
44
|
</header>
|
|
41
45
|
|
|
42
|
-
<main class="max-w-4xl mx-auto px-4 py-16">
|
|
46
|
+
<main id="blueBird-spa-content" class="max-w-4xl mx-auto px-4 py-16">
|
|
43
47
|
<div class="space-y-12">
|
|
44
48
|
<div>
|
|
45
49
|
<a href="/"
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
<meta name="author" content="{{author}}">
|
|
12
12
|
<link rel="icon" href="/favicon.ico" />
|
|
13
13
|
<script src="/js/tailwind.js"></script>
|
|
14
|
+
<!-- Enable or disable SPA transitions globally (default: true) -->
|
|
15
|
+
<script>window.enabledAnime = true;</script>
|
|
16
|
+
<script src="/js/blue-bird.js"></script>
|
|
17
|
+
|
|
14
18
|
</head>
|
|
15
19
|
|
|
16
20
|
<body class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white">
|
|
@@ -33,11 +37,12 @@
|
|
|
33
37
|
<nav class="flex space-x-8 text-sm font-medium text-slate-300">
|
|
34
38
|
<a href="/" class="hover:text-blue-400 transition-colors">Home</a>
|
|
35
39
|
<a href="/about" class="hover:text-blue-400 transition-colors">About</a>
|
|
40
|
+
<a href="/preact_example" class="hover:text-blue-400 transition-colors">Preact Example</a>
|
|
36
41
|
</nav>
|
|
37
42
|
</div>
|
|
38
43
|
</header>
|
|
39
44
|
|
|
40
|
-
<main class="max-w-7xl mx-auto px-4 pt-12 pb-24">
|
|
45
|
+
<main id="blueBird-spa-content" class="max-w-7xl mx-auto px-4 pt-12 pb-24">
|
|
41
46
|
|
|
42
47
|
<div
|
|
43
48
|
class="relative py-16 px-6 md:px-12 rounded-3xl overflow-hidden bg-gradient-to-b from-blue-900/20 to-slate-950 border border-white/5 shadow-2xl">
|
|
@@ -70,6 +75,7 @@
|
|
|
70
75
|
GitHub
|
|
71
76
|
</a>
|
|
72
77
|
</div>
|
|
78
|
+
|
|
73
79
|
</div>
|
|
74
80
|
</div>
|
|
75
81
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="{{lang}}">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>{{title}}</title>
|
|
8
|
+
<link rel="canonical" href="{{canonicalUrl}}" />
|
|
9
|
+
<meta name="description" content="{{description}}">
|
|
10
|
+
<meta name="keywords" content="{{keywords}}">
|
|
11
|
+
<meta name="author" content="{{author}}">
|
|
12
|
+
<link rel="icon" href="/favicon.ico" />
|
|
13
|
+
<script src="/js/tailwind.js"></script>
|
|
14
|
+
<!-- Enable or disable SPA transitions globally (default: true) -->
|
|
15
|
+
<script>window.enabledAnime = true;</script>
|
|
16
|
+
<script src="/js/blue-bird.js"></script>
|
|
17
|
+
|
|
18
|
+
</head>
|
|
19
|
+
|
|
20
|
+
<body class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white">
|
|
21
|
+
<header class="sticky top-0 z-50 px-4 py-3">
|
|
22
|
+
<div
|
|
23
|
+
class="max-w-7xl mx-auto flex items-center justify-between backdrop-blur-md bg-slate-900/60 border border-white/10 rounded-2xl px-6 py-3 shadow-lg">
|
|
24
|
+
<div class="flex items-center space-x-3">
|
|
25
|
+
<div
|
|
26
|
+
class="w-10 h-10 bg-gradient-to-tr from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-md shadow-blue-500/30">
|
|
27
|
+
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
28
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
29
|
+
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
|
30
|
+
</svg>
|
|
31
|
+
</div>
|
|
32
|
+
<span
|
|
33
|
+
class="text-xl font-bold tracking-tight bg-gradient-to-r from-blue-400 via-indigo-200 to-white bg-clip-text text-transparent">Blue
|
|
34
|
+
Bird</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<nav class="flex space-x-8 text-sm font-medium text-slate-300">
|
|
38
|
+
<a href="/" class="hover:text-blue-400 transition-colors">Home</a>
|
|
39
|
+
<a href="/about" class="hover:text-blue-400 transition-colors">About</a>
|
|
40
|
+
<a href="/preact_example" class="hover:text-blue-400 transition-colors">Preact Example</a>
|
|
41
|
+
</nav>
|
|
42
|
+
</div>
|
|
43
|
+
</header>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<!-- Inside main container #blueBird-spa-content -->
|
|
47
|
+
<main id="blueBird-spa-content">
|
|
48
|
+
<div id="preact-app"></div>
|
|
49
|
+
<script type="importmap">
|
|
50
|
+
{
|
|
51
|
+
"imports": {
|
|
52
|
+
"preact": "https://esm.sh/preact@10.19.2",
|
|
53
|
+
"preact/hooks": "https://esm.sh/preact@10.19.2/hooks",
|
|
54
|
+
"htm/preact": "https://esm.sh/htm@3.1.1/preact"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<script type="module">
|
|
60
|
+
import { render } from 'preact';
|
|
61
|
+
import { useState } from 'preact/hooks';
|
|
62
|
+
import { html } from 'htm/preact';
|
|
63
|
+
|
|
64
|
+
function App() {
|
|
65
|
+
const [count, setCount] = useState(0);
|
|
66
|
+
return html`
|
|
67
|
+
<div class="p-6 bg-slate-900 rounded-2xl">
|
|
68
|
+
<h2 class="text-lg text-white">Preact Counter</h2>
|
|
69
|
+
<p class="text-2xl text-blue-400 font-extrabold">${count}</p>
|
|
70
|
+
<button class="p-2 bg-blue-600 rounded-lg text-white font-semibold" onClick=${() => setCount(count + 1)}>Increment</button>
|
|
71
|
+
</div>
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
render(html`<${App} />`, document.getElementById('preact-app'));
|
|
76
|
+
</script>
|
|
77
|
+
</main>
|
|
78
|
+
</body>
|
|
79
|
+
|
|
80
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* blueBird-SPA Navigation Engine
|
|
3
|
-
* Provides Single Page Application behavior for Twig and React.
|
|
4
|
-
*/
|
|
5
|
-
(function () {
|
|
6
|
-
const CONTENT_ID = 'blueBird-spa-content';
|
|
7
|
-
const TIMEOUT_MS = 5000;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Navigates to a new page using SPA logic.
|
|
11
|
-
*
|
|
12
|
-
* @param {string} url - The target URL
|
|
13
|
-
* @param {boolean} [push=true] - Whether to push to browser history
|
|
14
|
-
* @returns {Promise<void>}
|
|
15
|
-
*/
|
|
16
|
-
async function navigate(url, push = true) {
|
|
17
|
-
const controller = new AbortController();
|
|
18
|
-
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
let requestUrl = url;
|
|
22
|
-
|
|
23
|
-
const separator = requestUrl.includes('?') ? '&' : '?';
|
|
24
|
-
const response = await fetch(`${requestUrl}${separator}source=frontend`, {
|
|
25
|
-
signal: controller.signal,
|
|
26
|
-
headers: {
|
|
27
|
-
'X-blueBird-SPA': 'true',
|
|
28
|
-
'Accept': 'application/json'
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
clearTimeout(timeoutId);
|
|
33
|
-
|
|
34
|
-
if (response.status === 401 || response.status === 403) {
|
|
35
|
-
window.location.href = url;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!response.ok) {
|
|
40
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const data = await response.json();
|
|
44
|
-
|
|
45
|
-
if (data.meta) {
|
|
46
|
-
document.title = data.meta.title || document.title;
|
|
47
|
-
['description', 'keywords', 'author'].forEach(name => {
|
|
48
|
-
const el = document.querySelector(`meta[name="${name}"]`);
|
|
49
|
-
if (el && data.meta[name]) el.setAttribute("content", data.meta[name]);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (data.css && Array.isArray(data.css)) {
|
|
54
|
-
data.css.forEach(href => {
|
|
55
|
-
if (!document.head.querySelector(`link[href="${href}"]`)) {
|
|
56
|
-
const link = document.createElement('link');
|
|
57
|
-
link.rel = 'stylesheet';
|
|
58
|
-
link.href = href;
|
|
59
|
-
document.head.appendChild(link);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const container = document.getElementById(CONTENT_ID);
|
|
65
|
-
if (container) {
|
|
66
|
-
container.innerHTML = data.body;
|
|
67
|
-
|
|
68
|
-
const partialScripts = container.querySelectorAll('script');
|
|
69
|
-
const partialLinks = container.querySelectorAll('link[rel="stylesheet"]');
|
|
70
|
-
|
|
71
|
-
partialLinks.forEach(link => {
|
|
72
|
-
if (!document.head.querySelector(`link[href="${link.href}"]`)) {
|
|
73
|
-
document.head.appendChild(link.cloneNode(true));
|
|
74
|
-
}
|
|
75
|
-
link.remove();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
partialScripts.forEach(oldScript => {
|
|
79
|
-
const newScript = document.createElement('script');
|
|
80
|
-
Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value));
|
|
81
|
-
newScript.textContent = oldScript.textContent;
|
|
82
|
-
|
|
83
|
-
if (oldScript.src) {
|
|
84
|
-
const existing = document.head.querySelector(`script[src="${oldScript.src}"]`);
|
|
85
|
-
if (existing && (oldScript.hasAttribute('data-spa-reload') || oldScript.src.includes('source=frontend'))) {
|
|
86
|
-
existing.remove();
|
|
87
|
-
}
|
|
88
|
-
if (!document.head.querySelector(`script[src="${oldScript.src}"]`)) {
|
|
89
|
-
document.head.appendChild(newScript);
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
const content = oldScript.textContent.trim();
|
|
93
|
-
if (content) {
|
|
94
|
-
Array.from(document.head.querySelectorAll('script:not([src])'))
|
|
95
|
-
.filter(s => s.textContent.trim() === content)
|
|
96
|
-
.forEach(s => s.remove());
|
|
97
|
-
document.head.appendChild(newScript);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
oldScript.remove();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
if (!url.includes('set-lang=true')) {
|
|
104
|
-
const hash = url.split('#')[1];
|
|
105
|
-
if (hash) {
|
|
106
|
-
const element = document.getElementById(hash);
|
|
107
|
-
if (element) {
|
|
108
|
-
element.scrollIntoView({ behavior: 'smooth' });
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (push) {
|
|
117
|
-
let finalUrl = response.url || url;
|
|
118
|
-
try {
|
|
119
|
-
const urlObj = new URL(finalUrl);
|
|
120
|
-
const hasSetLang = urlObj.searchParams.has('set-lang');
|
|
121
|
-
urlObj.searchParams.delete('source');
|
|
122
|
-
urlObj.searchParams.delete('set-lang');
|
|
123
|
-
if (hasSetLang) {
|
|
124
|
-
urlObj.searchParams.delete('lang');
|
|
125
|
-
}
|
|
126
|
-
finalUrl = urlObj.pathname + urlObj.search + urlObj.hash;
|
|
127
|
-
} catch (e) {
|
|
128
|
-
finalUrl = finalUrl.replace(/[?&]source=frontend/, '').replace(/[?&]set-lang=true/, '');
|
|
129
|
-
}
|
|
130
|
-
window.history.pushState({ url: finalUrl }, data.meta?.title || '', finalUrl);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
document.dispatchEvent(new CustomEvent('blueBird:navigation', {
|
|
134
|
-
detail: { url, data }
|
|
135
|
-
}));
|
|
136
|
-
document.dispatchEvent(new CustomEvent('blueBird:content-loaded', {
|
|
137
|
-
detail: { url, data }
|
|
138
|
-
}));
|
|
139
|
-
|
|
140
|
-
} catch (error) {
|
|
141
|
-
if (error.name === 'AbortError') {
|
|
142
|
-
console.warn('SPA Navigation timeout, redirecting...');
|
|
143
|
-
} else {
|
|
144
|
-
console.error('SPA Navigation failed:', error);
|
|
145
|
-
}
|
|
146
|
-
window.location.href = url;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
window.blueBirdNav = { navigate };
|
|
151
|
-
|
|
152
|
-
function initSPA() {
|
|
153
|
-
document.addEventListener('click', (e) => {
|
|
154
|
-
const link = e.target.closest('a');
|
|
155
|
-
|
|
156
|
-
if (link &&
|
|
157
|
-
link.href &&
|
|
158
|
-
link.href.startsWith(window.location.origin) &&
|
|
159
|
-
!link.hasAttribute('download') &&
|
|
160
|
-
!link.hasAttribute('data-no-spa') &&
|
|
161
|
-
link.target !== '_blank' &&
|
|
162
|
-
!e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
163
|
-
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
navigate(link.href);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
window.addEventListener('popstate', (e) => {
|
|
170
|
-
if (e.state && e.state.url) {
|
|
171
|
-
navigate(e.state.url, false);
|
|
172
|
-
} else {
|
|
173
|
-
window.location.reload();
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (document.readyState === 'loading') {
|
|
179
|
-
document.addEventListener('DOMContentLoaded', initSPA);
|
|
180
|
-
} else {
|
|
181
|
-
initSPA();
|
|
182
|
-
}
|
|
183
|
-
})();
|