@seip/blue-bird 0.6.1 → 0.6.2

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.
@@ -11,6 +11,7 @@
11
11
  <meta name="author" content="{{author}}">
12
12
  <link rel="icon" href="/favicon.ico" />
13
13
  <script src="/js/tailwind.js"></script>
14
+ <script src="/js/blue-bird.js"></script>
14
15
  </head>
15
16
 
16
17
  <body class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white">
@@ -35,11 +36,12 @@
35
36
  <nav class="flex space-x-8 text-sm font-medium text-slate-300">
36
37
  <a href="/" class="hover:text-blue-400 transition-colors">Home</a>
37
38
  <a href="/about" class="hover:text-blue-400 transition-colors">About</a>
39
+ <a href="/preact_example" class="hover:text-blue-400 transition-colors">Preact Example</a>
38
40
  </nav>
39
41
  </div>
40
42
  </header>
41
43
 
42
- <main class="max-w-4xl mx-auto px-4 py-16">
44
+ <main id="blueBird-spa-content" class="max-w-4xl mx-auto px-4 py-16">
43
45
  <div class="space-y-12">
44
46
  <div>
45
47
  <a href="/"
@@ -11,6 +11,8 @@
11
11
  <meta name="author" content="{{author}}">
12
12
  <link rel="icon" href="/favicon.ico" />
13
13
  <script src="/js/tailwind.js"></script>
14
+ <script src="/js/blue-bird.js"></script>
15
+
14
16
  </head>
15
17
 
16
18
  <body class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white">
@@ -33,11 +35,12 @@
33
35
  <nav class="flex space-x-8 text-sm font-medium text-slate-300">
34
36
  <a href="/" class="hover:text-blue-400 transition-colors">Home</a>
35
37
  <a href="/about" class="hover:text-blue-400 transition-colors">About</a>
38
+ <a href="/preact_example" class="hover:text-blue-400 transition-colors">Preact Example</a>
36
39
  </nav>
37
40
  </div>
38
41
  </header>
39
42
 
40
- <main class="max-w-7xl mx-auto px-4 pt-12 pb-24">
43
+ <main id="blueBird-spa-content" class="max-w-7xl mx-auto px-4 pt-12 pb-24">
41
44
 
42
45
  <div
43
46
  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 +73,7 @@
70
73
  GitHub
71
74
  </a>
72
75
  </div>
76
+
73
77
  </div>
74
78
  </div>
75
79
 
@@ -0,0 +1,78 @@
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
+ <script src="/js/blue-bird.js"></script>
15
+
16
+ </head>
17
+
18
+ <body class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white">
19
+ <header class="sticky top-0 z-50 px-4 py-3">
20
+ <div
21
+ 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">
22
+ <div class="flex items-center space-x-3">
23
+ <div
24
+ 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">
25
+ <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
27
+ d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
28
+ </svg>
29
+ </div>
30
+ <span
31
+ 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
32
+ Bird</span>
33
+ </div>
34
+
35
+ <nav class="flex space-x-8 text-sm font-medium text-slate-300">
36
+ <a href="/" class="hover:text-blue-400 transition-colors">Home</a>
37
+ <a href="/about" class="hover:text-blue-400 transition-colors">About</a>
38
+ <a href="/preact_example" class="hover:text-blue-400 transition-colors">Preact Example</a>
39
+ </nav>
40
+ </div>
41
+ </header>
42
+
43
+
44
+ <!-- Inside main container #blueBird-spa-content -->
45
+ <main id="blueBird-spa-content">
46
+ <div id="preact-app"></div>
47
+ <script type="importmap">
48
+ {
49
+ "imports": {
50
+ "preact": "https://esm.sh/preact@10.19.2",
51
+ "preact/hooks": "https://esm.sh/preact@10.19.2/hooks",
52
+ "htm/preact": "https://esm.sh/htm@3.1.1/preact"
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <script type="module">
58
+ import { render } from 'preact';
59
+ import { useState } from 'preact/hooks';
60
+ import { html } from 'htm/preact';
61
+
62
+ function App() {
63
+ const [count, setCount] = useState(0);
64
+ return html`
65
+ <div class="p-6 bg-slate-900 rounded-2xl">
66
+ <h2 class="text-lg text-white">Preact Counter</h2>
67
+ <p class="text-2xl text-blue-400 font-extrabold">${count}</p>
68
+ <button class="p-2 bg-blue-600 rounded-lg text-white font-semibold" onClick=${() => setCount(count + 1)}>Increment</button>
69
+ </div>
70
+ `;
71
+ }
72
+
73
+ render(html`<${App} />`, document.getElementById('preact-app'));
74
+ </script>
75
+ </main>
76
+ </body>
77
+
78
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seip/blue-bird",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Express opinionated framework with HTML rendering, API architecture, built-in JWT auth, validation, caching, and SEO",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- })();