@flexireact/core 1.0.2 → 2.0.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.
Files changed (37) hide show
  1. package/README.md +112 -112
  2. package/bin/flexireact.js +23 -0
  3. package/cli/index.ts +9 -21
  4. package/core/cli/{logger.js → logger.ts} +8 -2
  5. package/core/client/{hydration.js → hydration.ts} +10 -0
  6. package/core/client/{islands.js → islands.ts} +6 -1
  7. package/core/client/{navigation.js → navigation.ts} +10 -2
  8. package/core/client/{runtime.js → runtime.ts} +16 -0
  9. package/core/{index.js → index.ts} +2 -1
  10. package/core/islands/{index.js → index.ts} +16 -4
  11. package/core/{logger.js → logger.ts} +1 -1
  12. package/core/middleware/{index.js → index.ts} +32 -9
  13. package/core/plugins/{index.js → index.ts} +9 -6
  14. package/core/render/index.ts +1069 -0
  15. package/core/{render.js → render.ts} +7 -5
  16. package/core/router/index.ts +543 -0
  17. package/core/rsc/{index.js → index.ts} +6 -5
  18. package/core/server/{index.js → index.ts} +25 -6
  19. package/core/{server.js → server.ts} +8 -2
  20. package/core/ssg/{index.js → index.ts} +30 -5
  21. package/core/start-dev.ts +6 -0
  22. package/core/start-prod.ts +6 -0
  23. package/core/tsconfig.json +28 -0
  24. package/core/types.ts +239 -0
  25. package/package.json +19 -14
  26. package/cli/index.js +0 -992
  27. package/core/render/index.js +0 -773
  28. package/core/router/index.js +0 -296
  29. /package/core/{api.js → api.ts} +0 -0
  30. /package/core/build/{index.js → index.ts} +0 -0
  31. /package/core/client/{index.js → index.ts} +0 -0
  32. /package/core/{config.js → config.ts} +0 -0
  33. /package/core/{context.js → context.ts} +0 -0
  34. /package/core/{dev.js → dev.ts} +0 -0
  35. /package/core/{loader.js → loader.ts} +0 -0
  36. /package/core/{router.js → router.ts} +0 -0
  37. /package/core/{utils.js → utils.ts} +0 -0
@@ -1,773 +0,0 @@
1
- /**
2
- * FlexiReact Render System v2
3
- * SSR with layouts, loading states, error boundaries, and islands
4
- */
5
-
6
- import React from 'react';
7
- import { renderToString, renderToPipeableStream } from 'react-dom/server';
8
- import { escapeHtml } from '../utils.js';
9
-
10
- /**
11
- * Renders a page with all its layouts and wrappers
12
- */
13
- export async function renderPage(options) {
14
- const {
15
- Component,
16
- props = {},
17
- layouts = [],
18
- loading = null,
19
- error = null,
20
- islands = [],
21
- title = 'FlexiReact App',
22
- meta = {},
23
- scripts = [],
24
- styles = [],
25
- favicon = null,
26
- isSSG = false,
27
- route = '/',
28
- needsHydration = false
29
- } = options;
30
-
31
- const renderStart = Date.now();
32
-
33
- try {
34
- // Build the component tree - start with the page component
35
- let element = React.createElement(Component, props);
36
-
37
- // Wrap with layouts (innermost to outermost)
38
- // Each layout receives children as a prop
39
- for (const layout of [...layouts].reverse()) {
40
- if (layout.Component) {
41
- const LayoutComponent = layout.Component;
42
- element = React.createElement(LayoutComponent, {
43
- ...layout.props
44
- }, element);
45
- }
46
- }
47
-
48
- // Render to string
49
- const content = renderToString(element);
50
-
51
- // Calculate render time
52
- const renderTime = Date.now() - renderStart;
53
-
54
- // Generate island hydration scripts
55
- const islandScripts = generateIslandScripts(islands);
56
-
57
- // Build full HTML document
58
- return buildHtmlDocument({
59
- content,
60
- title,
61
- meta,
62
- scripts: [...scripts, ...islandScripts],
63
- styles,
64
- favicon,
65
- props,
66
- isSSG,
67
- renderTime,
68
- route,
69
- isClientComponent: needsHydration
70
- });
71
-
72
- } catch (err) {
73
- console.error('Render Error:', err);
74
- throw err;
75
- }
76
- }
77
-
78
- /**
79
- * Error Boundary Wrapper for SSR
80
- */
81
- class ErrorBoundaryWrapper extends React.Component {
82
- constructor(props) {
83
- super(props);
84
- this.state = { hasError: false, error: null };
85
- }
86
-
87
- static getDerivedStateFromError(error) {
88
- return { hasError: true, error };
89
- }
90
-
91
- render() {
92
- if (this.state.hasError) {
93
- const FallbackComponent = this.props.fallback;
94
- return React.createElement(FallbackComponent, { error: this.state.error });
95
- }
96
- return this.props.children;
97
- }
98
- }
99
-
100
- /**
101
- * Generates hydration scripts for islands
102
- */
103
- function generateIslandScripts(islands) {
104
- if (!islands.length) return [];
105
-
106
- const scripts = [];
107
-
108
- for (const island of islands) {
109
- scripts.push({
110
- type: 'module',
111
- content: `
112
- import { hydrateIsland } from '/_flexi/client.js';
113
- import ${island.name} from '${island.clientPath}';
114
- hydrateIsland('${island.id}', ${island.name}, ${JSON.stringify(island.props)});
115
- `
116
- });
117
- }
118
-
119
- return scripts;
120
- }
121
-
122
- /**
123
- * Generates the Dev Toolbar HTML (Premium Next.js/Vercel style)
124
- */
125
- function generateDevToolbar(options = {}) {
126
- const {
127
- renderTime = 0,
128
- pageType = 'SSR',
129
- route = '/',
130
- hasError = false,
131
- isHydrated = false
132
- } = options;
133
-
134
- return `
135
- <!-- FlexiReact Dev Toolbar -->
136
- <div id="flexi-dev-toolbar">
137
- <style>
138
- #flexi-dev-toolbar {
139
- position: fixed;
140
- bottom: 20px;
141
- left: 20px;
142
- z-index: 99999;
143
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
144
- font-size: 13px;
145
- }
146
- .flexi-dev-btn {
147
- display: flex;
148
- align-items: center;
149
- gap: 10px;
150
- padding: 10px 16px;
151
- background: linear-gradient(135deg, rgba(15, 23, 42, 0.95), rgba(30, 41, 59, 0.95));
152
- backdrop-filter: blur(12px);
153
- border: 1px solid rgba(16, 185, 129, 0.3);
154
- border-radius: 12px;
155
- color: #fff;
156
- cursor: pointer;
157
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
158
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(16, 185, 129, 0.1);
159
- }
160
- .flexi-dev-btn:hover {
161
- border-color: rgba(16, 185, 129, 0.6);
162
- transform: translateY(-3px);
163
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(16, 185, 129, 0.2);
164
- }
165
- .flexi-dev-logo {
166
- width: 22px;
167
- height: 22px;
168
- background: linear-gradient(135deg, #10b981, #06b6d4);
169
- border-radius: 6px;
170
- display: flex;
171
- align-items: center;
172
- justify-content: center;
173
- font-weight: 800;
174
- font-size: 12px;
175
- color: #000;
176
- }
177
- .flexi-dev-panel {
178
- position: absolute;
179
- bottom: 100%;
180
- left: 0;
181
- margin-bottom: 12px;
182
- min-width: 320px;
183
- background: linear-gradient(135deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.98));
184
- backdrop-filter: blur(16px);
185
- border: 1px solid rgba(16, 185, 129, 0.2);
186
- border-radius: 16px;
187
- padding: 0;
188
- opacity: 0;
189
- visibility: hidden;
190
- transform: translateY(10px) scale(0.95);
191
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
192
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(16, 185, 129, 0.1);
193
- overflow: hidden;
194
- }
195
- .flexi-dev-btn:hover + .flexi-dev-panel,
196
- .flexi-dev-panel:hover {
197
- opacity: 1;
198
- visibility: visible;
199
- transform: translateY(0) scale(1);
200
- }
201
- .flexi-dev-header {
202
- display: flex;
203
- align-items: center;
204
- gap: 10px;
205
- padding: 16px 20px;
206
- background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(6, 182, 212, 0.1));
207
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
208
- }
209
- .flexi-dev-header-logo {
210
- width: 28px;
211
- height: 28px;
212
- background: linear-gradient(135deg, #10b981, #06b6d4);
213
- border-radius: 8px;
214
- display: flex;
215
- align-items: center;
216
- justify-content: center;
217
- font-weight: 800;
218
- font-size: 14px;
219
- color: #000;
220
- }
221
- .flexi-dev-header-text {
222
- font-weight: 700;
223
- font-size: 15px;
224
- background: linear-gradient(90deg, #10b981, #06b6d4);
225
- -webkit-background-clip: text;
226
- -webkit-text-fill-color: transparent;
227
- }
228
- .flexi-dev-header-version {
229
- margin-left: auto;
230
- font-size: 11px;
231
- color: #64748b;
232
- background: rgba(255, 255, 255, 0.05);
233
- padding: 3px 8px;
234
- border-radius: 6px;
235
- }
236
- .flexi-dev-row {
237
- display: flex;
238
- justify-content: space-between;
239
- align-items: center;
240
- padding: 12px 20px;
241
- border-bottom: 1px solid rgba(255, 255, 255, 0.03);
242
- }
243
- .flexi-dev-row:last-child {
244
- border-bottom: none;
245
- }
246
- .flexi-dev-label {
247
- color: #64748b;
248
- font-size: 12px;
249
- }
250
- .flexi-dev-value {
251
- font-weight: 600;
252
- color: #f1f5f9;
253
- }
254
- .flexi-dev-badge {
255
- display: inline-flex;
256
- align-items: center;
257
- gap: 6px;
258
- padding: 4px 10px;
259
- border-radius: 6px;
260
- font-size: 11px;
261
- font-weight: 700;
262
- letter-spacing: 0.5px;
263
- }
264
- .flexi-dev-badge.ssr {
265
- background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(245, 158, 11, 0.1));
266
- color: #fbbf24;
267
- border: 1px solid rgba(251, 191, 36, 0.3);
268
- }
269
- .flexi-dev-badge.ssg {
270
- background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.1));
271
- color: #22c55e;
272
- border: 1px solid rgba(34, 197, 94, 0.3);
273
- }
274
- .flexi-dev-badge.isr {
275
- background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.1));
276
- color: #818cf8;
277
- border: 1px solid rgba(99, 102, 241, 0.3);
278
- }
279
- .flexi-dev-badge.csr {
280
- background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(14, 165, 233, 0.1));
281
- color: #06b6d4;
282
- border: 1px solid rgba(6, 182, 212, 0.3);
283
- }
284
- .flexi-dev-badge.api {
285
- background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(99, 102, 241, 0.1));
286
- color: #60a5fa;
287
- border: 1px solid rgba(59, 130, 246, 0.3);
288
- }
289
- .flexi-dev-time {
290
- font-weight: 700;
291
- color: ${renderTime < 100 ? '#10b981' : renderTime < 500 ? '#fbbf24' : '#ef4444'};
292
- }
293
- .flexi-dev-status {
294
- display: flex;
295
- align-items: center;
296
- gap: 8px;
297
- }
298
- .flexi-dev-dot {
299
- width: 10px;
300
- height: 10px;
301
- border-radius: 50%;
302
- background: linear-gradient(135deg, #10b981, #06b6d4);
303
- box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
304
- animation: pulse 2s infinite;
305
- }
306
- .flexi-dev-dot.error {
307
- background: linear-gradient(135deg, #ef4444, #f87171);
308
- box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
309
- }
310
- @keyframes pulse {
311
- 0%, 100% { opacity: 1; transform: scale(1); }
312
- 50% { opacity: 0.7; transform: scale(0.9); }
313
- }
314
- .flexi-dev-links {
315
- display: flex;
316
- gap: 8px;
317
- padding: 16px 20px;
318
- background: rgba(0, 0, 0, 0.2);
319
- }
320
- .flexi-dev-link {
321
- flex: 1;
322
- padding: 8px 12px;
323
- background: rgba(255, 255, 255, 0.03);
324
- border: 1px solid rgba(255, 255, 255, 0.08);
325
- border-radius: 8px;
326
- color: #94a3b8;
327
- text-decoration: none;
328
- font-size: 11px;
329
- font-weight: 500;
330
- text-align: center;
331
- transition: all 0.2s;
332
- }
333
- .flexi-dev-link:hover {
334
- background: rgba(16, 185, 129, 0.15);
335
- border-color: rgba(16, 185, 129, 0.3);
336
- color: #10b981;
337
- }
338
- </style>
339
-
340
- <button class="flexi-dev-btn" title="FlexiReact Dev Tools">
341
- <div class="flexi-dev-logo">⚡</div>
342
- <span class="flexi-dev-badge ${pageType.toLowerCase()}">${pageType}</span>
343
- </button>
344
-
345
- <div class="flexi-dev-panel">
346
- <div class="flexi-dev-header">
347
- <div class="flexi-dev-header-logo">⚡</div>
348
- <span class="flexi-dev-header-text">FlexiReact</span>
349
- <span class="flexi-dev-header-version">v1.0.0</span>
350
- </div>
351
-
352
- <div class="flexi-dev-row">
353
- <span class="flexi-dev-label">Status</span>
354
- <div class="flexi-dev-status">
355
- <div class="flexi-dev-dot ${hasError ? 'error' : ''}"></div>
356
- <span class="flexi-dev-value">${hasError ? 'Error' : 'Ready'}</span>
357
- </div>
358
- </div>
359
-
360
- <div class="flexi-dev-row">
361
- <span class="flexi-dev-label">Route</span>
362
- <span class="flexi-dev-value">${route}</span>
363
- </div>
364
-
365
- <div class="flexi-dev-row">
366
- <span class="flexi-dev-label">Page Type</span>
367
- <span class="flexi-dev-badge ${pageType.toLowerCase()}">${pageType}</span>
368
- </div>
369
-
370
- <div class="flexi-dev-row">
371
- <span class="flexi-dev-label">Render Time</span>
372
- <span class="flexi-dev-value flexi-dev-time">${renderTime}ms</span>
373
- </div>
374
-
375
- <div class="flexi-dev-row">
376
- <span class="flexi-dev-label">Hydration</span>
377
- <span class="flexi-dev-value">${isHydrated ? '✓ Client' : '○ Server'}</span>
378
- </div>
379
-
380
- <div class="flexi-dev-links">
381
- <a href="/_flexi/routes" class="flexi-dev-link">Routes</a>
382
- <a href="/api" class="flexi-dev-link">API</a>
383
- <a href="https://github.com/nicholasmusic/flexireact" target="_blank" class="flexi-dev-link">Docs ↗</a>
384
- </div>
385
- </div>
386
- </div>
387
-
388
- <script>
389
- // Track hydration
390
- window.__FLEXI_DEV__ = {
391
- renderTime: ${renderTime},
392
- pageType: '${pageType}',
393
- route: '${route}',
394
- hydrated: false
395
- };
396
-
397
- // Update hydration status when React hydrates
398
- const observer = new MutationObserver(() => {
399
- if (document.getElementById('root')?.children.length > 0) {
400
- window.__FLEXI_DEV__.hydrated = true;
401
- const hydrationEl = document.querySelector('.flexi-dev-row:nth-child(5) .flexi-dev-value');
402
- if (hydrationEl) hydrationEl.textContent = '✓ Client';
403
- }
404
- });
405
- observer.observe(document.getElementById('root'), { childList: true, subtree: true });
406
-
407
- // Log to console
408
- console.log('%c⚛ FlexiReact Dev', 'color: #61DAFB; font-weight: bold; font-size: 14px;');
409
- console.log('%cPage: ' + '${route}' + ' [${pageType}] - ${renderTime}ms', 'color: #94a3b8;');
410
- </script>
411
- `;
412
- }
413
-
414
- /**
415
- * Builds complete HTML document
416
- */
417
- function buildHtmlDocument(options) {
418
- const {
419
- content,
420
- title,
421
- meta = {},
422
- scripts = [],
423
- styles = [],
424
- props = {},
425
- isSSG = false,
426
- renderTime = 0,
427
- route = '/',
428
- isClientComponent = false,
429
- favicon = null
430
- } = options;
431
-
432
- const metaTags = Object.entries(meta)
433
- .map(([name, content]) => {
434
- if (name.startsWith('og:')) {
435
- return `<meta property="${escapeHtml(name)}" content="${escapeHtml(content)}">`;
436
- }
437
- return `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}">`;
438
- })
439
- .join('\n ');
440
-
441
- const styleTags = styles
442
- .map(style => {
443
- if (typeof style === 'string') {
444
- return `<link rel="stylesheet" href="${escapeHtml(style)}">`;
445
- }
446
- return `<style>${style.content}</style>`;
447
- })
448
- .join('\n ');
449
-
450
- const scriptTags = scripts
451
- .map(script => {
452
- if (typeof script === 'string') {
453
- return `<script src="${escapeHtml(script)}"></script>`;
454
- }
455
- const type = script.type ? ` type="${script.type}"` : '';
456
- if (script.src) {
457
- return `<script${type} src="${escapeHtml(script.src)}"></script>`;
458
- }
459
- return `<script${type}>${script.content}</script>`;
460
- })
461
- .join('\n ');
462
-
463
- // FlexiReact SVG favicon (properly encoded)
464
- const faviconSvg = encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#00FF9C"/><text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" fill="#000" font-family="system-ui" font-weight="bold" font-size="16">F</text></svg>`);
465
-
466
- // Generate Dev Toolbar for development mode
467
- const isDev = process.env.NODE_ENV !== 'production';
468
- const pageType = isSSG ? 'SSG' : isClientComponent ? 'CSR' : 'SSR';
469
- const devToolbar = isDev ? generateDevToolbar({
470
- renderTime,
471
- pageType,
472
- route,
473
- isHydrated: isClientComponent
474
- }) : '';
475
-
476
- // Determine favicon link
477
- const faviconLink = favicon
478
- ? `<link rel="icon" href="${escapeHtml(favicon)}">`
479
- : `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${faviconSvg}">`;
480
-
481
- return `<!DOCTYPE html>
482
- <html lang="en" class="dark">
483
- <head>
484
- <meta charset="UTF-8">
485
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
486
- <title>${escapeHtml(title)}</title>
487
- ${faviconLink}
488
- ${metaTags}
489
- <style>
490
- :root { --flexi-bg: #0f172a; --flexi-fg: #f8fafc; }
491
- html, body { background-color: #0f172a; color: #f8fafc; min-height: 100vh; margin: 0; }
492
- </style>
493
- ${styleTags}
494
- <script>
495
- (function() {
496
- var theme = localStorage.getItem('theme');
497
- if (theme === 'light') {
498
- document.documentElement.classList.remove('dark');
499
- document.documentElement.style.backgroundColor = '#ffffff';
500
- document.documentElement.style.color = '#0f172a';
501
- document.body.style.backgroundColor = '#ffffff';
502
- document.body.style.color = '#0f172a';
503
- }
504
- })();
505
- </script>
506
- </head>
507
- <body>
508
- <div id="root">${content}</div>
509
- <script>
510
- window.__FLEXI_DATA__ = ${JSON.stringify({ props, isSSG })};
511
- </script>
512
- ${scriptTags}
513
- ${devToolbar}
514
- </body>
515
- </html>`;
516
- }
517
-
518
- /**
519
- * Renders an error page with beautiful styling
520
- */
521
- export function renderError(statusCode, message, stack = null) {
522
- const showStack = process.env.NODE_ENV !== 'production' && stack;
523
- const isDev = process.env.NODE_ENV !== 'production';
524
-
525
- // Different messages for different status codes
526
- const errorMessages = {
527
- 404: { title: 'Page Not Found', emoji: '🔍', desc: 'The page you\'re looking for doesn\'t exist or has been moved.' },
528
- 500: { title: 'Server Error', emoji: '💥', desc: 'Something went wrong on our end. Please try again later.' },
529
- 403: { title: 'Forbidden', emoji: '🚫', desc: 'You don\'t have permission to access this resource.' },
530
- 401: { title: 'Unauthorized', emoji: '🔐', desc: 'Please log in to access this page.' },
531
- };
532
-
533
- const errorInfo = errorMessages[statusCode] || { title: 'Error', emoji: '⚠️', desc: message };
534
-
535
- return `<!DOCTYPE html>
536
- <html lang="en">
537
- <head>
538
- <meta charset="UTF-8">
539
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
540
- <title>${statusCode} - ${errorInfo.title} | FlexiReact</title>
541
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'><defs><linearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'><stop offset='0%25' stop-color='%2361DAFB'/><stop offset='100%25' stop-color='%2321A1F1'/></linearGradient></defs><circle cx='100' cy='100' r='12' fill='url(%23g)'/><ellipse cx='100' cy='100' rx='80' ry='30' fill='none' stroke='url(%23g)' stroke-width='6' transform='rotate(-30 100 100)'/><ellipse cx='100' cy='100' rx='80' ry='30' fill='none' stroke='url(%23g)' stroke-width='6' transform='rotate(30 100 100)'/><ellipse cx='100' cy='100' rx='80' ry='30' fill='none' stroke='url(%23g)' stroke-width='6' transform='rotate(90 100 100)'/></svg>">
542
- <style>
543
- * { margin: 0; padding: 0; box-sizing: border-box; }
544
- body {
545
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
546
- min-height: 100vh;
547
- display: flex;
548
- align-items: center;
549
- justify-content: center;
550
- background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%);
551
- color: #f8fafc;
552
- overflow: hidden;
553
- }
554
- .bg-grid {
555
- position: fixed;
556
- inset: 0;
557
- background-image:
558
- linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
559
- linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
560
- background-size: 50px 50px;
561
- pointer-events: none;
562
- }
563
- .bg-glow {
564
- position: fixed;
565
- top: 50%;
566
- left: 50%;
567
- transform: translate(-50%, -50%);
568
- width: 600px;
569
- height: 600px;
570
- background: radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, transparent 70%);
571
- pointer-events: none;
572
- }
573
- .container {
574
- position: relative;
575
- text-align: center;
576
- padding: 2rem;
577
- max-width: 600px;
578
- animation: fadeIn 0.5s ease-out;
579
- }
580
- @keyframes fadeIn {
581
- from { opacity: 0; transform: translateY(20px); }
582
- to { opacity: 1; transform: translateY(0); }
583
- }
584
- .logo {
585
- width: 80px;
586
- height: 80px;
587
- margin: 0 auto 2rem;
588
- opacity: 0.8;
589
- }
590
- .emoji {
591
- font-size: 4rem;
592
- margin-bottom: 1rem;
593
- }
594
- .error-code {
595
- font-size: 8rem;
596
- font-weight: 800;
597
- line-height: 1;
598
- background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #6366f1 100%);
599
- -webkit-background-clip: text;
600
- -webkit-text-fill-color: transparent;
601
- background-clip: text;
602
- text-shadow: 0 0 80px rgba(99, 102, 241, 0.5);
603
- }
604
- .error-title {
605
- font-size: 1.5rem;
606
- font-weight: 600;
607
- margin: 1rem 0 0.5rem;
608
- color: #e2e8f0;
609
- }
610
- .error-desc {
611
- font-size: 1rem;
612
- color: #94a3b8;
613
- margin-bottom: 2rem;
614
- line-height: 1.6;
615
- }
616
- .buttons {
617
- display: flex;
618
- gap: 1rem;
619
- justify-content: center;
620
- flex-wrap: wrap;
621
- }
622
- .btn {
623
- display: inline-flex;
624
- align-items: center;
625
- gap: 0.5rem;
626
- padding: 0.875rem 1.75rem;
627
- border-radius: 12px;
628
- font-weight: 600;
629
- text-decoration: none;
630
- transition: all 0.2s;
631
- font-size: 0.95rem;
632
- }
633
- .btn-primary {
634
- background: linear-gradient(135deg, #6366f1, #8b5cf6);
635
- color: white;
636
- box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
637
- }
638
- .btn-primary:hover {
639
- transform: translateY(-2px);
640
- box-shadow: 0 8px 30px rgba(99, 102, 241, 0.5);
641
- }
642
- .btn-secondary {
643
- background: rgba(255, 255, 255, 0.05);
644
- color: #e2e8f0;
645
- border: 1px solid rgba(255, 255, 255, 0.1);
646
- }
647
- .btn-secondary:hover {
648
- background: rgba(255, 255, 255, 0.1);
649
- border-color: rgba(255, 255, 255, 0.2);
650
- }
651
- .stack {
652
- text-align: left;
653
- background: rgba(0, 0, 0, 0.3);
654
- border: 1px solid rgba(239, 68, 68, 0.3);
655
- padding: 1rem;
656
- border-radius: 12px;
657
- font-family: 'SF Mono', Monaco, 'Courier New', monospace;
658
- font-size: 0.8rem;
659
- overflow-x: auto;
660
- white-space: pre-wrap;
661
- margin-top: 2rem;
662
- color: #fca5a5;
663
- max-height: 200px;
664
- overflow-y: auto;
665
- }
666
- .dev-badge {
667
- position: fixed;
668
- bottom: 1rem;
669
- right: 1rem;
670
- display: flex;
671
- align-items: center;
672
- gap: 0.5rem;
673
- padding: 0.5rem 1rem;
674
- background: rgba(0, 0, 0, 0.5);
675
- border: 1px solid rgba(255, 255, 255, 0.1);
676
- border-radius: 8px;
677
- font-size: 0.75rem;
678
- color: #94a3b8;
679
- }
680
- .dev-badge svg {
681
- width: 16px;
682
- height: 16px;
683
- }
684
- </style>
685
- </head>
686
- <body>
687
- <div class="bg-grid"></div>
688
- <div class="bg-glow"></div>
689
-
690
- <div class="container">
691
- <svg class="logo" viewBox="0 0 200 200" fill="none">
692
- <defs>
693
- <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
694
- <stop offset="0%" stop-color="#6366f1"/>
695
- <stop offset="100%" stop-color="#a855f7"/>
696
- </linearGradient>
697
- </defs>
698
- <circle cx="100" cy="100" r="12" fill="url(#g)"/>
699
- <ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" stroke-width="6" transform="rotate(-30 100 100)"/>
700
- <ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" stroke-width="6" transform="rotate(30 100 100)"/>
701
- <ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#g)" stroke-width="6" transform="rotate(90 100 100)"/>
702
- </svg>
703
-
704
- <div class="error-code">${statusCode}</div>
705
- <h1 class="error-title">${errorInfo.title}</h1>
706
- <p class="error-desc">${errorInfo.desc}</p>
707
-
708
- <div class="buttons">
709
- <a href="/" class="btn btn-primary">
710
- <span>←</span> Back to Home
711
- </a>
712
- <a href="javascript:history.back()" class="btn btn-secondary">
713
- Go Back
714
- </a>
715
- </div>
716
-
717
- ${showStack ? `<pre class="stack">${escapeHtml(stack)}</pre>` : ''}
718
- </div>
719
-
720
- ${isDev ? `
721
- <div class="dev-badge">
722
- <svg viewBox="0 0 200 200" fill="none">
723
- <defs><linearGradient id="db" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#61DAFB"/><stop offset="100%" stop-color="#21A1F1"/></linearGradient></defs>
724
- <circle cx="100" cy="100" r="12" fill="url(#db)"/>
725
- <ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#db)" stroke-width="6" transform="rotate(-30 100 100)"/>
726
- <ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#db)" stroke-width="6" transform="rotate(30 100 100)"/>
727
- <ellipse cx="100" cy="100" rx="80" ry="30" fill="none" stroke="url(#db)" stroke-width="6" transform="rotate(90 100 100)"/>
728
- </svg>
729
- FlexiReact Dev
730
- </div>
731
- ` : ''}
732
- </body>
733
- </html>`;
734
- }
735
-
736
- /**
737
- * Renders a loading state
738
- */
739
- export function renderLoading(LoadingComponent) {
740
- if (!LoadingComponent) {
741
- return `<div class="flexi-loading">
742
- <div class="flexi-spinner"></div>
743
- <style>
744
- .flexi-loading {
745
- display: flex;
746
- align-items: center;
747
- justify-content: center;
748
- min-height: 200px;
749
- }
750
- .flexi-spinner {
751
- width: 40px;
752
- height: 40px;
753
- border: 3px solid #f3f3f3;
754
- border-top: 3px solid #667eea;
755
- border-radius: 50%;
756
- animation: spin 1s linear infinite;
757
- }
758
- @keyframes spin {
759
- 0% { transform: rotate(0deg); }
760
- 100% { transform: rotate(360deg); }
761
- }
762
- </style>
763
- </div>`;
764
- }
765
-
766
- return renderToString(React.createElement(LoadingComponent));
767
- }
768
-
769
- export default {
770
- renderPage,
771
- renderError,
772
- renderLoading
773
- };