@seip/blue-bird 0.3.8 → 0.4.0

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/core/template.js CHANGED
@@ -91,6 +91,9 @@ class Template {
91
91
  *
92
92
  * @param {string} [options.metaTags.langMeta]
93
93
  * Alternative language metadata value.
94
+ *
95
+ * @param {boolean} [options.skeleton=true]
96
+ * Enables skeleton loading.
94
97
  *
95
98
  * @returns {void}
96
99
  * Sends a complete HTML response to the client.
@@ -120,7 +123,8 @@ class Template {
120
123
  * keywordsMeta: "express, react, framework",
121
124
  * authorMeta: "Blue Bird",
122
125
  * langMeta: "en"
123
- * }
126
+ * },
127
+ * skeleton: true
124
128
  * };
125
129
  *
126
130
  * Template.renderReact(res, "App", { title: "Hello World" }, options);
@@ -135,7 +139,8 @@ class Template {
135
139
  scriptsInHead = [],
136
140
  scriptsInBody = [],
137
141
  cache = true,
138
- metaTags
142
+ metaTags,
143
+ skeleton = true
139
144
  } = options;
140
145
  const metaTagsDefault = {
141
146
  titleMeta: props.titleMeta,
@@ -177,12 +182,20 @@ class Template {
177
182
  .map(item => `<script src="${item.src}"></script>`)
178
183
  .join("");
179
184
 
180
- const scriptsBodyTags = scriptsInBody
185
+ let scriptsBodyTags = scriptsInBody
181
186
  .map(item => `<script src="${item.src}"></script>`)
182
187
  .join("");
183
188
 
184
189
  const propsJson = JSON.stringify(componentProps).replace(/'/g, "&#39;");
185
190
 
191
+ const stylesSkeleton = skeleton
192
+ ? `<style>${this.skeletonStyles()}</style>`
193
+ : "";
194
+
195
+ const skeletonHtml = skeleton
196
+ ? this.skeletonHtml()
197
+ : "";
198
+
186
199
  let html = BASE_TEMPLATE
187
200
  .replace(/__LANG__/g, this.escapeHtml(langHtml))
188
201
  .replace(/__TITLE__/g, title)
@@ -196,7 +209,9 @@ class Template {
196
209
  .replace(/__COMPONENT__/g, component)
197
210
  .replace(/__PROPS__/g, propsJson)
198
211
  .replace(/__VITE_ASSETS__/g, this.vite_assets())
199
- .replace(/__SCRIPTS_BODY__/g, scriptsBodyTags);
212
+ .replace(/__SCRIPTS_BODY__/g, scriptsBodyTags)
213
+ .replace(/__STYLES_SKELETON__/g, stylesSkeleton)
214
+ .replace(/__SKELETON__/g, skeletonHtml);
200
215
 
201
216
  html = this.minifyHtml(html);
202
217
  CACHE_TEMPLATE[cacheKey] = html;
@@ -287,6 +302,182 @@ window.__vite_plugin_react_preamble_installed__ = true;
287
302
  .replace(/"/g, "&quot;")
288
303
  .replace(/'/g, "&#39;");
289
304
  }
305
+ static skeletonStyles() {
306
+ return `
307
+ @keyframes sk-pulse {
308
+ 0%, 100% { opacity: 1; }
309
+ 50% { opacity: 0.5; }
310
+ }
311
+ .sk-animate-pulse {
312
+ animation: sk-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
313
+ }
314
+ .sk-container {
315
+ min-height: 100vh;
316
+ width: 100%;
317
+ background-color: #f9fafb; /* bg-gray-50 */
318
+ padding: 1rem;
319
+ box-sizing: border-box;
320
+ }
321
+ .sk-inner {
322
+ display: flex;
323
+ flex-direction: column;
324
+ gap: 1.5rem;
325
+ }
326
+ .sk-header {
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: space-between;
330
+ width: 100%;
331
+ margin-bottom: 1rem;
332
+ }
333
+ .sk-btn-text {
334
+ height: 2.5rem;
335
+ width: 8rem;
336
+ background-color: #d1d5db; /* bg-gray-300 */
337
+ border-radius: 0.5rem;
338
+ }
339
+ .sk-header-actions {
340
+ display: flex;
341
+ gap: 1rem;
342
+ }
343
+ .sk-avatar {
344
+ height: 2.5rem;
345
+ width: 2.5rem;
346
+ background-color: #d1d5db;
347
+ border-radius: 9999px;
348
+ }
349
+ .sk-btn {
350
+ height: 2.5rem;
351
+ width: 6rem;
352
+ background-color: #d1d5db;
353
+ border-radius: 0.5rem;
354
+ }
355
+ .sk-hero {
356
+ height: 12rem;
357
+ width: 100%;
358
+ background-color: #d1d5db;
359
+ border-radius: 1rem;
360
+ }
361
+ .sk-grid {
362
+ display: grid;
363
+ grid-template-columns: repeat(1, minmax(0, 1fr));
364
+ gap: 1.5rem;
365
+ }
366
+ .sk-card {
367
+ display: flex;
368
+ flex-direction: column;
369
+ gap: 0.75rem;
370
+ }
371
+ .sk-card-img {
372
+ height: 10rem;
373
+ width: 100%;
374
+ background-color: #d1d5db;
375
+ border-radius: 0.75rem;
376
+ }
377
+ .sk-text-34 {
378
+ height: 1rem;
379
+ width: 75%;
380
+ background-color: #d1d5db;
381
+ border-radius: 0.25rem;
382
+ }
383
+ .sk-text-12 {
384
+ height: 1rem;
385
+ width: 50%;
386
+ background-color: #d1d5db;
387
+ border-radius: 0.25rem;
388
+ }
389
+ .sk-footer {
390
+ display: flex;
391
+ flex-direction: column;
392
+ gap: 0.5rem;
393
+ margin-top: 1rem;
394
+ }
395
+ .sk-text-full {
396
+ height: 1rem;
397
+ width: 100%;
398
+ background-color: #e5e7eb; /* bg-gray-200 */
399
+ border-radius: 0.25rem;
400
+ }
401
+ .sk-text-23 {
402
+ height: 1rem;
403
+ width: 66.666667%;
404
+ background-color: #e5e7eb;
405
+ border-radius: 0.25rem;
406
+ }
407
+
408
+ @media (min-width: 768px) {
409
+ .sk-container {
410
+ padding: 2rem;
411
+ }
412
+ .sk-hero {
413
+ height: 16rem;
414
+ }
415
+ .sk-grid {
416
+ grid-template-columns: repeat(3, minmax(0, 1fr));
417
+ }
418
+ }
419
+
420
+ html.dark .sk-container {
421
+ background-color: #0b0f19; /* bg-gray-950/900 */
422
+ }
423
+ html.dark .sk-btn-text,
424
+ html.dark .sk-avatar,
425
+ html.dark .sk-btn,
426
+ html.dark .sk-hero,
427
+ html.dark .sk-card-img,
428
+ html.dark .sk-text-34,
429
+ html.dark .sk-text-12 {
430
+ background-color: #374151; /* bg-gray-700 */
431
+ }
432
+ html.dark .sk-text-full,
433
+ html.dark .sk-text-23 {
434
+ background-color: #1f2937; /* bg-gray-800 */
435
+ }
436
+ `;
437
+ }
438
+
439
+ static skeletonHtml() {
440
+ return `
441
+ <div class="sk-container">
442
+ <div class="sk-inner sk-animate-pulse">
443
+ <div class="sk-header">
444
+ <div class="sk-btn-text"></div>
445
+ <div class="sk-header-actions">
446
+ <div class="sk-avatar"></div>
447
+ <div class="sk-btn"></div>
448
+ </div>
449
+ </div>
450
+
451
+ <div class="sk-hero"></div>
452
+
453
+ <div class="sk-grid">
454
+ <div class="sk-card">
455
+ <div class="sk-card-img"></div>
456
+ <div class="sk-text-34"></div>
457
+ <div class="sk-text-12"></div>
458
+ </div>
459
+ <div class="sk-card">
460
+ <div class="sk-card-img"></div>
461
+ <div class="sk-text-34"></div>
462
+ <div class="sk-text-12"></div>
463
+ </div>
464
+ <div class="sk-card">
465
+ <div class="sk-card-img"></div>
466
+ <div class="sk-text-34"></div>
467
+ <div class="sk-text-12"></div>
468
+ </div>
469
+ </div>
470
+
471
+ <div class="sk-footer">
472
+ <div class="sk-text-full"></div>
473
+ <div class="sk-text-full"></div>
474
+ <div class="sk-text-23"></div>
475
+ </div>
476
+ </div>
477
+ </div>
478
+ `;
479
+ }
480
+
290
481
  }
291
482
 
292
483
  export default Template;
@@ -13,16 +13,13 @@
13
13
  __LINK_STYLES__
14
14
  __SCRIPTS_HEAD__
15
15
  __VITE_ASSETS__
16
- <script src="https://cdn.tailwindcss.com"></script>
17
- <script>
18
- tailwind.config = {
19
- darkMode: 'class'
20
- }
21
- </script>
16
+ __STYLES_SKELETON__
22
17
  </head>
23
18
 
24
19
  <body class="__CLASS_BODY__">
25
- <div id="root" data-react-component="__COMPONENT__" data-props='__PROPS__'></div>
20
+ <div id="root" data-react-component="__COMPONENT__" data-props='__PROPS__'>
21
+ __SKELETON__
22
+ </div>
26
23
  __SCRIPTS_BODY__
27
24
  </body>
28
25
 
@@ -0,0 +1,2 @@
1
+ @import "tailwindcss";
2
+ @custom-variant dark (&:where(.dark, .dark *));
@@ -1,8 +1,11 @@
1
- import React from 'react';
1
+ import React, { lazy, Suspense } from 'react';
2
2
  import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
3
- import Home from './pages/Home';
4
- import About from './pages/About';
5
3
  import { ThemeProvider } from './blue-bird/contexts/ThemeContext.jsx';
4
+ import Skeleton from './blue-bird/components/Skeleton.jsx';
5
+ import { LanguageProvider } from './blue-bird/contexts/LanguageContext.jsx'
6
+
7
+ const Home = lazy(() => import('./pages/Home'));
8
+ const About = lazy(() => import('./pages/About'));
6
9
 
7
10
  export default function App(_props) {
8
11
  const {
@@ -16,12 +19,16 @@ export default function App(_props) {
16
19
 
17
20
  return (
18
21
  <ThemeProvider>
19
- <Router>
20
- <Routes>
21
- <Route path="/" element={<Home />} />
22
- <Route path="/about" element={<About />} />
23
- </Routes>
24
- </Router>
22
+ <LanguageProvider>
23
+ <Router>
24
+ <Suspense fallback={<Skeleton />}>
25
+ <Routes>
26
+ <Route path="/" element={<Home />} />
27
+ <Route path="/about" element={<About />} />
28
+ </Routes>
29
+ </Suspense>
30
+ </Router>
31
+ </LanguageProvider>
25
32
  </ThemeProvider>
26
33
  );
27
34
  }
@@ -1,18 +1,19 @@
1
1
  import React from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
  import App from './App';
4
+ import "../css/tailwind.css"
4
5
 
5
6
 
6
7
  document.addEventListener('DOMContentLoaded', () => {
7
- document.querySelectorAll('[data-react-component]').forEach(el => {
8
- const component = {
9
- component:el.dataset.reactComponent
10
- };
11
- const props = JSON.parse(el.dataset.props || '{}');
12
- const allProps={
13
- ...props,
14
- ...component
15
- }
16
- createRoot(el).render(<App {...allProps} />);
17
- });
8
+ document.querySelectorAll('[data-react-component]').forEach(el => {
9
+ const component = {
10
+ component: el.dataset.reactComponent
11
+ };
12
+ const props = JSON.parse(el.dataset.props || '{}');
13
+ const allProps = {
14
+ ...props,
15
+ ...component
16
+ }
17
+ createRoot(el).render(<App {...allProps} />);
18
+ });
18
19
  });
@@ -1,12 +1,13 @@
1
1
  import React from 'react';
2
+ import Typography from './Typography';
2
3
 
3
4
  export default function Card({ children, className = '', title, description, border = true, shadow = true }) {
4
5
  return (
5
6
  <div className={`rounded-lg ${border ? "border border-slate-200 dark:border-slate-800" : ""} ${shadow ? "shadow-sm" : ""} bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 ${className}`}>
6
7
  {(title || description) && (
7
8
  <div className="flex flex-col space-y-1.5 p-6">
8
- {title && <h3 className="font-semibold leading-none tracking-tight">{title}</h3>}
9
- {description && <p className="text-sm text-slate-500 dark:text-slate-400">{description}</p>}
9
+ {title && <Typography variant='h5' className="font-semibold leading-none tracking-tight">{title}</Typography>}
10
+ {description && <Typography variant='p' className="text-sm text-slate-500 dark:text-slate-400">{description}</Typography>}
10
11
  </div>
11
12
  )}
12
13
  <div className={`p-6 ${title || description ? 'pt-0' : ''}`}>
@@ -0,0 +1,45 @@
1
+ export default function Skeleton() {
2
+ return (
3
+ <div className="min-h-screen w-full bg-gray-50 p-4 md:p-8">
4
+ <div className="animate-pulse flex flex-col gap-6">
5
+
6
+ <div className="flex items-center justify-between w-full mb-4">
7
+ <div className="h-10 w-32 bg-gray-300 rounded-lg"></div>
8
+ <div className="flex space-x-4">
9
+ <div className="h-10 w-10 bg-gray-300 rounded-full"></div>
10
+ <div className="h-10 w-24 bg-gray-300 rounded-lg"></div>
11
+ </div>
12
+ </div>
13
+
14
+ <div className="h-48 md:h-64 w-full bg-gray-300 rounded-2xl"></div>
15
+
16
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
17
+ <div className="space-y-3">
18
+ <div className="h-40 w-full bg-gray-300 rounded-xl"></div>
19
+ <div className="h-4 w-3/4 bg-gray-300 rounded"></div>
20
+ <div className="h-4 w-1/2 bg-gray-300 rounded"></div>
21
+ </div>
22
+
23
+ <div className="space-y-3">
24
+ <div className="h-40 w-full bg-gray-300 rounded-xl"></div>
25
+ <div className="h-4 w-3/4 bg-gray-300 rounded"></div>
26
+ <div className="h-4 w-1/2 bg-gray-300 rounded"></div>
27
+ </div>
28
+
29
+ <div className="space-y-3">
30
+ <div className="h-40 w-full bg-gray-300 rounded-xl"></div>
31
+ <div className="h-4 w-3/4 bg-gray-300 rounded"></div>
32
+ <div className="h-4 w-1/2 bg-gray-300 rounded"></div>
33
+ </div>
34
+ </div>
35
+
36
+ <div className="space-y-2 mt-4">
37
+ <div className="h-4 w-full bg-gray-200 rounded"></div>
38
+ <div className="h-4 w-full bg-gray-200 rounded"></div>
39
+ <div className="h-4 w-2/3 bg-gray-200 rounded"></div>
40
+ </div>
41
+
42
+ </div>
43
+ </div>
44
+ )
45
+ }
@@ -17,7 +17,18 @@ export const LanguageProvider = ({ children }) => {
17
17
  * @param {string} key - the string config key
18
18
  * @returns {string} The translated text
19
19
  */
20
- const t = (key) => translations[lang][key] || key;
20
+ const t = (key) => {
21
+ const keys = key.split('.');
22
+ let value = translations[lang];
23
+ for (const k of keys) {
24
+ if (value && typeof value === 'object' && value !== null && k in value) {
25
+ value = value[k];
26
+ } else {
27
+ return key;
28
+ }
29
+ }
30
+ return value !== undefined ? value : key;
31
+ };
21
32
 
22
33
  return (
23
34
  <LanguageContext.Provider value={{ lang, setLang, t }}>
@@ -1,4 +1,22 @@
1
1
  {
2
+ "home_page": {
3
+ "title": "Welcome to Blue Bird",
4
+ "description": "The elegant, fast, and weightless framework for modern web development.",
5
+ "lightweight": "Lightweight",
6
+ "lightweightDescription": "Built with performance and simplicity in mind.",
7
+ "reactPowered": "React Powered",
8
+ "reactPoweredDescription": "Full React + Vite integration.",
9
+ "expressBackend": "Express Backend",
10
+ "expressBackendDescription": "Robust and scalable backend architecture."
11
+ },
12
+ "about_page": {
13
+ "title": "About Blue Bird",
14
+ "description": "Blue Bird is a modern framework designed to bridge the gap between backend routing and frontend interactivity.",
15
+ "description2": "It provides a seamless developer experience for building fast, reactive web applications.",
16
+ "check_your_console": "Check your console JS"
17
+ },
18
+ "home": "Home",
19
+ "about": "About",
2
20
  "login": "Login",
3
21
  "register": "Register",
4
22
  "email": "Email address",
@@ -1,4 +1,22 @@
1
1
  {
2
+ "home_page": {
3
+ "title": "Bienvenido a Blue Bird",
4
+ "description": "El framework elegante, rápido y ligero para el desarrollo web moderno.",
5
+ "lightweight": "Ligero",
6
+ "lightweightDescription": "Construido pensando en el rendimiento y la simplicidad.",
7
+ "reactPowered": "Impulsado por React",
8
+ "reactPoweredDescription": "Integración completa con React + Vite.",
9
+ "expressBackend": "Backend con Express",
10
+ "expressBackendDescription": "Arquitectura de backend robusta y escalable."
11
+ },
12
+ "about_page": {
13
+ "title": "Acerca de Blue Bird",
14
+ "description": "Blue Bird es un framework moderno diseñado para cerrar la brecha entre el enrutamiento de backend y la interactividad de frontend.",
15
+ "description2": "Proporciona una experiencia de desarrollador perfecta para crear aplicaciones web rápidas y reactivas.",
16
+ "check_your_console": "Revisa tu consola JS"
17
+ },
18
+ "home": "Inicio",
19
+ "about": "Acerca de",
2
20
  "login": "Iniciar Sesión",
3
21
  "register": "Registrarse",
4
22
  "email": "Correo Electrónico",
@@ -0,0 +1,53 @@
1
+ import { Link } from "react-router-dom";
2
+ import { useState } from "react";
3
+ import Button from "../blue-bird/components/Button";
4
+ import { useLanguage } from "../blue-bird/contexts/LanguageContext";
5
+ import { useTheme } from "../blue-bird/contexts/ThemeContext";
6
+
7
+ export default function Header() {
8
+ const { t, setLang } = useLanguage();
9
+ const { changeTheme } = useTheme();
10
+
11
+ const [emojiTheme, setEmojiTheme] = useState("🌞");
12
+
13
+ const changeThemeEmoji = () => {
14
+ if (emojiTheme === "🌞") {
15
+ setEmojiTheme("🌙");
16
+ changeTheme("dark");
17
+ } else {
18
+ setEmojiTheme("🌞");
19
+ changeTheme("light");
20
+ }
21
+ }
22
+
23
+ return (
24
+ <header>
25
+ <nav
26
+ className='bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-slate-800 px-4 py-4 flex justify-between items-center gap-4 sticky top-0 z-10'
27
+ >
28
+ <div className='font-bold text-xl text-slate-900 dark:text-slate-100'>
29
+ Blue Bird
30
+ </div>
31
+ <div className='flex justify-between items-center gap-4'>
32
+ <div className="flex justify-between items-center gap-4">
33
+ <Button variant="outline" size="sm" onClick={() => setLang("es")} >
34
+ ES
35
+ </Button>
36
+ <Button variant="outline" size="sm" onClick={() => setLang("en")} >
37
+ EN
38
+ </Button>
39
+ <Button variant="ghost" size="icon" onClick={changeThemeEmoji}>
40
+ {emojiTheme}
41
+ </Button>
42
+ </div>
43
+ <div className="flex justify-between items-center gap-4">
44
+ <Link to="/" className='text-sm font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 transition-colors'>{t("home")}</Link>
45
+ <Link to="/about" className='text-sm font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 transition-colors'>{t("about")}</Link>
46
+ </div>
47
+
48
+ </div>
49
+ </nav>
50
+ </header>
51
+
52
+ )
53
+ }
@@ -1,33 +1,31 @@
1
1
  import React from 'react';
2
- import { Link } from 'react-router-dom';
2
+ import Header from '../components/Header';
3
+ import { useLanguage } from '../blue-bird/contexts/LanguageContext';
4
+
5
+ import Card from '../blue-bird/components/Card';
6
+ import Typography from '../blue-bird/components/Typography'
3
7
 
4
8
  export default function About() {
9
+ const { t } = useLanguage();
5
10
  return (
6
11
  <div
7
12
  className="bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 min-h-screen"
8
13
  >
9
- <nav
10
- className='bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-slate-800 px-4 py-4 flex justify-between items-center gap-4 sticky top-0 z-10'
11
- >
12
- <div className='font-bold text-xl text-blue-600 dark:text-blue-400'>
13
- Blue Bird
14
- </div>
15
- <div className='flex justify-between items-center gap-4'>
16
- <Link to="/" className='text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'>Home</Link>
17
- <Link to="/about" className='text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'>About</Link>
18
- </div>
19
- </nav>
20
- <main className='max-w-7xl mx-auto'>
21
- <div className='p-4'>
22
- <h1 className='text-xl font-bold text-gray-900 dark:text-gray-100 mb-4'>About Blue Bird</h1>
23
- <p className='text-gray-500 dark:text-gray-400 leading-1.6'>
24
- Blue Bird is a modern framework designed to bridge the gap between backend routing and frontend interactivity.
25
- It provides a seamless developer experience for building fast, reactive web applications.
26
- </p>
27
- <p className='text-red-500 text-xl mt-8 '>
28
- Check your console JS
29
- </p>
30
- </div>
14
+ <Header />
15
+ <main className='max-w-3xl mx-auto mt-8 p-4'>
16
+ <Card>
17
+ <Typography variant='h1' className='text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-100 mb-4'>
18
+ {t("about_page.title")}
19
+ </Typography>
20
+ <Typography className='text-slate-500 dark:text-slate-400 leading-7'>
21
+ {t("about_page.description")}
22
+ </Typography>
23
+ <div className='mt-8 pt-4 border-t border-slate-200 dark:border-slate-800'>
24
+ <Typography className='text-sm text-red-500 font-medium'>
25
+ {t("about_page.check_your_console")}
26
+ </Typography>
27
+ </div>
28
+ </Card>
31
29
  </main>
32
30
  </div>
33
31
  );