@flexireact/core 3.0.1 → 3.0.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.
Files changed (55) hide show
  1. package/dist/cli/index.js +1514 -0
  2. package/dist/cli/index.js.map +1 -0
  3. package/dist/core/client/index.js +373 -0
  4. package/dist/core/client/index.js.map +1 -0
  5. package/dist/core/index.js +6415 -0
  6. package/dist/core/index.js.map +1 -0
  7. package/dist/core/server/index.js +3094 -0
  8. package/dist/core/server/index.js.map +1 -0
  9. package/package.json +80 -80
  10. package/bin/flexireact.js +0 -23
  11. package/cli/generators.ts +0 -616
  12. package/cli/index.ts +0 -1182
  13. package/core/actions/index.ts +0 -364
  14. package/core/api.ts +0 -143
  15. package/core/build/index.ts +0 -425
  16. package/core/cli/logger.ts +0 -353
  17. package/core/client/Link.tsx +0 -345
  18. package/core/client/hydration.ts +0 -147
  19. package/core/client/index.ts +0 -12
  20. package/core/client/islands.ts +0 -143
  21. package/core/client/navigation.ts +0 -212
  22. package/core/client/runtime.ts +0 -52
  23. package/core/config.ts +0 -116
  24. package/core/context.ts +0 -83
  25. package/core/dev.ts +0 -47
  26. package/core/devtools/index.ts +0 -644
  27. package/core/edge/cache.ts +0 -344
  28. package/core/edge/fetch-polyfill.ts +0 -247
  29. package/core/edge/handler.ts +0 -248
  30. package/core/edge/index.ts +0 -81
  31. package/core/edge/ppr.ts +0 -264
  32. package/core/edge/runtime.ts +0 -161
  33. package/core/font/index.ts +0 -306
  34. package/core/helpers.ts +0 -494
  35. package/core/image/index.ts +0 -413
  36. package/core/index.ts +0 -218
  37. package/core/islands/index.ts +0 -293
  38. package/core/loader.ts +0 -111
  39. package/core/logger.ts +0 -242
  40. package/core/metadata/index.ts +0 -622
  41. package/core/middleware/index.ts +0 -416
  42. package/core/plugins/index.ts +0 -373
  43. package/core/render/index.ts +0 -1243
  44. package/core/render.ts +0 -136
  45. package/core/router/index.ts +0 -551
  46. package/core/router.ts +0 -141
  47. package/core/rsc/index.ts +0 -199
  48. package/core/server/index.ts +0 -779
  49. package/core/server.ts +0 -203
  50. package/core/ssg/index.ts +0 -346
  51. package/core/start-dev.ts +0 -6
  52. package/core/start-prod.ts +0 -6
  53. package/core/tsconfig.json +0 -30
  54. package/core/types.ts +0 -239
  55. package/core/utils.ts +0 -176
@@ -1,353 +0,0 @@
1
- /**
2
- * FlexiReact CLI Logger - Premium Console Output
3
- * Beautiful, modern, and professional CLI aesthetics
4
- */
5
-
6
- import pc from 'picocolors';
7
-
8
- // ============================================================================
9
- // Color Palette
10
- // ============================================================================
11
-
12
- const colors = {
13
- primary: (text) => pc.green(text), // Neon emerald
14
- secondary: (text) => pc.cyan(text), // Cyan
15
- accent: (text) => pc.magenta(text), // Magenta accent
16
- success: (text) => pc.green(text), // Success green
17
- error: (text) => pc.red(text), // Error red
18
- warning: (text) => pc.yellow(text), // Warning yellow
19
- info: (text) => pc.blue(text), // Info blue
20
- muted: (text) => pc.dim(text), // Muted/dim
21
- bold: (text) => pc.bold(text), // Bold
22
- white: (text) => pc.white(text), // White
23
- };
24
-
25
- // ============================================================================
26
- // Icons & Symbols
27
- // ============================================================================
28
-
29
- const icons = {
30
- success: pc.green('✓'),
31
- error: pc.red('✗'),
32
- warning: pc.yellow('⚠'),
33
- info: pc.blue('ℹ'),
34
- arrow: pc.dim('→'),
35
- dot: pc.dim('•'),
36
- star: pc.yellow('★'),
37
- rocket: '🚀',
38
- lightning: '⚡',
39
- puzzle: '🧩',
40
- island: '🏝️',
41
- plug: '🔌',
42
- package: '📦',
43
- folder: '📁',
44
- file: '📄',
45
- globe: '🌐',
46
- check: pc.green('✔'),
47
- cross: pc.red('✘'),
48
- spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
49
- };
50
-
51
- // ============================================================================
52
- // Box Drawing
53
- // ============================================================================
54
-
55
- const box = {
56
- topLeft: '╭',
57
- topRight: '╮',
58
- bottomLeft: '╰',
59
- bottomRight: '╯',
60
- horizontal: '─',
61
- vertical: '│',
62
- horizontalDown: '┬',
63
- horizontalUp: '┴',
64
- verticalRight: '├',
65
- verticalLeft: '┤',
66
- };
67
-
68
- interface BoxOptions {
69
- padding?: number;
70
- borderColor?: (s: string) => string;
71
- width?: number;
72
- }
73
-
74
- function createBox(content: string, options: BoxOptions = {}) {
75
- const {
76
- padding = 1,
77
- borderColor = colors.primary,
78
- width = 50
79
- } = options;
80
-
81
- const lines = content.split('\n');
82
- const maxLen = Math.max(...lines.map(l => stripAnsi(l).length), width - 4);
83
- const innerWidth = maxLen + (padding * 2);
84
-
85
- const top = borderColor(box.topLeft + box.horizontal.repeat(innerWidth) + box.topRight);
86
- const bottom = borderColor(box.bottomLeft + box.horizontal.repeat(innerWidth) + box.bottomRight);
87
- const empty = borderColor(box.vertical) + ' '.repeat(innerWidth) + borderColor(box.vertical);
88
-
89
- const contentLines = lines.map(line => {
90
- const stripped = stripAnsi(line);
91
- const pad = innerWidth - stripped.length - padding;
92
- return borderColor(box.vertical) + ' '.repeat(padding) + line + ' '.repeat(Math.max(0, pad)) + borderColor(box.vertical);
93
- });
94
-
95
- const paddingLines = padding > 0 ? [empty] : [];
96
-
97
- return [top, ...paddingLines, ...contentLines, ...paddingLines, bottom].join('\n');
98
- }
99
-
100
- function stripAnsi(str) {
101
- return str.replace(/\x1B\[[0-9;]*m/g, '');
102
- }
103
-
104
- // ============================================================================
105
- // ASCII Art Banners
106
- // ============================================================================
107
-
108
- const banners = {
109
- // Main banner - Gradient style
110
- main: `
111
- ${pc.cyan(' ╭─────────────────────────────────────────╮')}
112
- ${pc.cyan(' │')} ${pc.cyan('│')}
113
- ${pc.cyan(' │')} ${pc.bold(pc.green('⚡'))} ${pc.bold(pc.white('F L E X I R E A C T'))} ${pc.cyan('│')}
114
- ${pc.cyan(' │')} ${pc.cyan('│')}
115
- ${pc.cyan(' │')} ${pc.dim('The Modern React Framework')} ${pc.cyan('│')}
116
- ${pc.cyan(' │')} ${pc.cyan('│')}
117
- ${pc.cyan(' ╰─────────────────────────────────────────╯')}
118
- `,
119
-
120
- // Compact banner
121
- compact: `
122
- ${pc.green('⚡')} ${pc.bold('FlexiReact')} ${pc.dim('v2.1.0')}
123
- `,
124
-
125
- // Build banner
126
- build: `
127
- ${pc.cyan(' ╭─────────────────────────────────────────╮')}
128
- ${pc.cyan(' │')} ${pc.bold(pc.green('⚡'))} ${pc.bold('FlexiReact Build')} ${pc.cyan('│')}
129
- ${pc.cyan(' ╰─────────────────────────────────────────╯')}
130
- `,
131
- };
132
-
133
- // ============================================================================
134
- // Server Status Panel
135
- // ============================================================================
136
-
137
- function serverPanel(config) {
138
- const {
139
- mode = 'development',
140
- port = 3000,
141
- host = 'localhost',
142
- pagesDir = './pages',
143
- islands = true,
144
- rsc = true,
145
- } = config;
146
-
147
- const modeColor = mode === 'development' ? colors.warning : colors.success;
148
- const url = `http://${host}:${port}`;
149
-
150
- console.log('');
151
- console.log(pc.dim(' ─────────────────────────────────────────'));
152
- console.log(` ${icons.arrow} ${pc.dim('Mode:')} ${modeColor(mode)}`);
153
- console.log(` ${icons.arrow} ${pc.dim('Local:')} ${pc.cyan(pc.underline(url))}`);
154
- console.log(` ${icons.arrow} ${pc.dim('Pages:')} ${pc.white(pagesDir)}`);
155
- console.log(` ${icons.arrow} ${pc.dim('Islands:')} ${islands ? pc.green('enabled') : pc.dim('disabled')}`);
156
- console.log(` ${icons.arrow} ${pc.dim('RSC:')} ${rsc ? pc.green('enabled') : pc.dim('disabled')}`);
157
- console.log(pc.dim(' ─────────────────────────────────────────'));
158
- console.log('');
159
- console.log(` ${icons.success} ${pc.green('Ready for requests...')}`);
160
- console.log('');
161
- }
162
-
163
- // ============================================================================
164
- // Plugin Loader
165
- // ============================================================================
166
-
167
- function pluginLoader(plugins = []) {
168
- console.log('');
169
- console.log(`${icons.package} ${pc.bold('Loading plugins...')}`);
170
- console.log('');
171
-
172
- if (plugins.length === 0) {
173
- console.log(` ${pc.dim('No plugins configured')}`);
174
- } else {
175
- plugins.forEach((plugin, i) => {
176
- console.log(` ${icons.check} ${pc.white(plugin.name)} ${pc.dim(`v${plugin.version || '1.0.0'}`)}`);
177
- });
178
- }
179
-
180
- console.log('');
181
- console.log(` ${pc.dim('Total plugins:')} ${pc.white(plugins.length)}`);
182
- console.log('');
183
- }
184
-
185
- // ============================================================================
186
- // HTTP Request Logger
187
- // ============================================================================
188
-
189
- function request(method: string, path: string, statusCode: number, duration: number, options: { type?: string } = {}) {
190
- const { type = 'dynamic' } = options;
191
-
192
- // Method colors
193
- const methodColors = {
194
- GET: pc.green,
195
- POST: pc.blue,
196
- PUT: pc.yellow,
197
- DELETE: pc.red,
198
- PATCH: pc.magenta,
199
- };
200
-
201
- // Type badges
202
- const typeBadges = {
203
- ssr: `${pc.yellow('[SSR]')}`,
204
- ssg: `${pc.green('[SSG]')}`,
205
- rsc: `${pc.magenta('[RSC]')}`,
206
- island: `${pc.cyan('[ISL]')}`,
207
- api: `${pc.blue('[API]')}`,
208
- asset: `${pc.dim('[AST]')}`,
209
- dynamic: `${pc.yellow('[SSR]')}`,
210
- };
211
-
212
- // Status colors
213
- const statusColor = statusCode >= 500 ? pc.red :
214
- statusCode >= 400 ? pc.yellow :
215
- statusCode >= 300 ? pc.cyan :
216
- pc.green;
217
-
218
- const methodColor = methodColors[method] || pc.white;
219
- const badge = typeBadges[type] || typeBadges.dynamic;
220
- const durationStr = duration < 100 ? pc.green(`${duration}ms`) :
221
- duration < 500 ? pc.yellow(`${duration}ms`) :
222
- pc.red(`${duration}ms`);
223
-
224
- console.log(` ${methodColor(method.padEnd(6))} ${pc.white(path)}`);
225
- console.log(` ${pc.dim('└─')} ${badge} ${statusColor(statusCode)} ${pc.dim('(')}${durationStr}${pc.dim(')')}`);
226
- }
227
-
228
- // ============================================================================
229
- // Error & Warning Formatters
230
- // ============================================================================
231
-
232
- function error(title, message, stack = null, suggestions = []) {
233
- console.log('');
234
- console.log(pc.red(' ╭─────────────────────────────────────────────────────────╮'));
235
- console.log(pc.red(' │') + ` ${pc.red(pc.bold('✗ ERROR'))} ` + pc.red('│'));
236
- console.log(pc.red(' ├─────────────────────────────────────────────────────────┤'));
237
- console.log(pc.red(' │') + ` ${pc.white(title.substring(0, 55).padEnd(55))} ` + pc.red('│'));
238
- console.log(pc.red(' │') + ' '.repeat(57) + pc.red('│'));
239
-
240
- // Message lines
241
- const msgLines = message.split('\n').slice(0, 3);
242
- msgLines.forEach(line => {
243
- console.log(pc.red(' │') + ` ${pc.dim(line.substring(0, 55).padEnd(55))} ` + pc.red('│'));
244
- });
245
-
246
- if (suggestions.length > 0) {
247
- console.log(pc.red(' │') + ' '.repeat(57) + pc.red('│'));
248
- console.log(pc.red(' │') + ` ${pc.cyan('Suggestions:')} ` + pc.red('│'));
249
- suggestions.slice(0, 2).forEach(s => {
250
- console.log(pc.red(' │') + ` ${pc.dim('→')} ${pc.white(s.substring(0, 52).padEnd(52))} ` + pc.red('│'));
251
- });
252
- }
253
-
254
- console.log(pc.red(' ╰─────────────────────────────────────────────────────────╯'));
255
- console.log('');
256
- }
257
-
258
- function warning(title, message) {
259
- console.log('');
260
- console.log(pc.yellow(' ⚠ WARNING: ') + pc.white(title));
261
- console.log(pc.dim(` ${message}`));
262
- console.log('');
263
- }
264
-
265
- function info(message) {
266
- console.log(` ${icons.info} ${message}`);
267
- }
268
-
269
- function success(message) {
270
- console.log(` ${icons.success} ${pc.green(message)}`);
271
- }
272
-
273
- // ============================================================================
274
- // Build Logger
275
- // ============================================================================
276
-
277
- function buildStart() {
278
- console.log(banners.build);
279
- console.log(` ${icons.rocket} ${pc.bold('Starting production build...')}`);
280
- console.log('');
281
- }
282
-
283
- function buildStep(step, total, message) {
284
- console.log(` ${pc.dim(`[${step}/${total}]`)} ${message}`);
285
- }
286
-
287
- function buildComplete(stats) {
288
- const { duration, pages, size } = stats;
289
-
290
- console.log('');
291
- console.log(pc.green(' ╭─────────────────────────────────────────╮'));
292
- console.log(pc.green(' │') + ` ${pc.green(pc.bold('✓ Build completed successfully!'))} ` + pc.green('│'));
293
- console.log(pc.green(' ├─────────────────────────────────────────┤'));
294
- console.log(pc.green(' │') + ` ${pc.dim('Duration:')} ${pc.white(duration + 'ms').padEnd(27)} ` + pc.green('│'));
295
- console.log(pc.green(' │') + ` ${pc.dim('Pages:')} ${pc.white(pages + ' pages').padEnd(27)} ` + pc.green('│'));
296
- console.log(pc.green(' │') + ` ${pc.dim('Size:')} ${pc.white(size).padEnd(27)} ` + pc.green('│'));
297
- console.log(pc.green(' ╰─────────────────────────────────────────╯'));
298
- console.log('');
299
- }
300
-
301
- // ============================================================================
302
- // Dividers & Spacing
303
- // ============================================================================
304
-
305
- function divider() {
306
- console.log(pc.dim(' ─────────────────────────────────────────'));
307
- }
308
-
309
- function blank() {
310
- console.log('');
311
- }
312
-
313
- function clear() {
314
- console.clear();
315
- }
316
-
317
- // ============================================================================
318
- // Export
319
- // ============================================================================
320
-
321
- export const logger = {
322
- // Banners
323
- banner: () => console.log(banners.main),
324
- bannerCompact: () => console.log(banners.compact),
325
-
326
- // Panels
327
- serverPanel,
328
- pluginLoader,
329
-
330
- // Logging
331
- request,
332
- error,
333
- warning,
334
- info,
335
- success,
336
-
337
- // Build
338
- buildStart,
339
- buildStep,
340
- buildComplete,
341
-
342
- // Utilities
343
- divider,
344
- blank,
345
- clear,
346
- createBox,
347
-
348
- // Colors & Icons
349
- colors,
350
- icons,
351
- };
352
-
353
- export default logger;
@@ -1,345 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * FlexiReact Link Component
5
- * Enhanced link with prefetching, client-side navigation, and loading states
6
- */
7
-
8
- import React, { useCallback, useEffect, useRef, useState } from 'react';
9
-
10
- export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
11
- /** The URL to navigate to */
12
- href: string;
13
- /** Prefetch the page on hover/visibility */
14
- prefetch?: boolean | 'hover' | 'viewport';
15
- /** Replace the current history entry instead of pushing */
16
- replace?: boolean;
17
- /** Scroll to top after navigation */
18
- scroll?: boolean;
19
- /** Show loading indicator while navigating */
20
- showLoading?: boolean;
21
- /** Custom loading component */
22
- loadingComponent?: React.ReactNode;
23
- /** Callback when navigation starts */
24
- onNavigationStart?: () => void;
25
- /** Callback when navigation ends */
26
- onNavigationEnd?: () => void;
27
- /** Children */
28
- children: React.ReactNode;
29
- }
30
-
31
- // Prefetch cache to avoid duplicate requests
32
- const prefetchCache = new Set<string>();
33
-
34
- // Prefetch a URL
35
- async function prefetchUrl(url: string): Promise<void> {
36
- if (prefetchCache.has(url)) return;
37
-
38
- try {
39
- // Mark as prefetched immediately to prevent duplicate requests
40
- prefetchCache.add(url);
41
-
42
- // Use link preload for better browser optimization
43
- const link = document.createElement('link');
44
- link.rel = 'prefetch';
45
- link.href = url;
46
- link.as = 'document';
47
- document.head.appendChild(link);
48
-
49
- // Also fetch the page to warm the cache
50
- const controller = new AbortController();
51
- const timeoutId = setTimeout(() => controller.abort(), 5000);
52
-
53
- await fetch(url, {
54
- method: 'GET',
55
- credentials: 'same-origin',
56
- signal: controller.signal,
57
- headers: {
58
- 'X-Flexi-Prefetch': '1',
59
- 'Accept': 'text/html'
60
- }
61
- });
62
-
63
- clearTimeout(timeoutId);
64
- } catch (error) {
65
- // Remove from cache on error so it can be retried
66
- prefetchCache.delete(url);
67
- }
68
- }
69
-
70
- // Check if URL is internal
71
- function isInternalUrl(url: string): boolean {
72
- if (url.startsWith('/')) return true;
73
- if (url.startsWith('#')) return true;
74
-
75
- try {
76
- const parsed = new URL(url, window.location.origin);
77
- return parsed.origin === window.location.origin;
78
- } catch {
79
- return false;
80
- }
81
- }
82
-
83
- // Navigate to a URL
84
- function navigate(url: string, options: { replace?: boolean; scroll?: boolean } = {}): void {
85
- const { replace = false, scroll = true } = options;
86
-
87
- if (replace) {
88
- window.history.replaceState({}, '', url);
89
- } else {
90
- window.history.pushState({}, '', url);
91
- }
92
-
93
- // Dispatch popstate event to trigger any listeners
94
- window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
95
-
96
- // Scroll to top if requested
97
- if (scroll) {
98
- window.scrollTo({ top: 0, behavior: 'smooth' });
99
- }
100
- }
101
-
102
- /**
103
- * Link component with prefetching and client-side navigation
104
- *
105
- * @example
106
- * ```tsx
107
- * import { Link } from '@flexireact/core/client';
108
- *
109
- * // Basic usage
110
- * <Link href="/about">About</Link>
111
- *
112
- * // With prefetch on hover
113
- * <Link href="/products" prefetch="hover">Products</Link>
114
- *
115
- * // With prefetch on viewport visibility
116
- * <Link href="/contact" prefetch="viewport">Contact</Link>
117
- *
118
- * // Replace history instead of push
119
- * <Link href="/login" replace>Login</Link>
120
- *
121
- * // Disable scroll to top
122
- * <Link href="/section#anchor" scroll={false}>Go to section</Link>
123
- * ```
124
- */
125
- export function Link({
126
- href,
127
- prefetch = true,
128
- replace = false,
129
- scroll = true,
130
- showLoading = false,
131
- loadingComponent,
132
- onNavigationStart,
133
- onNavigationEnd,
134
- children,
135
- className,
136
- onClick,
137
- onMouseEnter,
138
- onFocus,
139
- ...props
140
- }: LinkProps) {
141
- const [isNavigating, setIsNavigating] = useState(false);
142
- const linkRef = useRef<HTMLAnchorElement>(null);
143
- const hasPrefetched = useRef(false);
144
-
145
- // Prefetch on viewport visibility
146
- useEffect(() => {
147
- if (prefetch !== 'viewport' && prefetch !== true) return;
148
- if (!isInternalUrl(href)) return;
149
- if (hasPrefetched.current) return;
150
-
151
- const observer = new IntersectionObserver(
152
- (entries) => {
153
- entries.forEach((entry) => {
154
- if (entry.isIntersecting) {
155
- prefetchUrl(href);
156
- hasPrefetched.current = true;
157
- observer.disconnect();
158
- }
159
- });
160
- },
161
- { rootMargin: '200px' }
162
- );
163
-
164
- if (linkRef.current) {
165
- observer.observe(linkRef.current);
166
- }
167
-
168
- return () => observer.disconnect();
169
- }, [href, prefetch]);
170
-
171
- // Handle hover prefetch
172
- const handleMouseEnter = useCallback(
173
- (e: React.MouseEvent<HTMLAnchorElement>) => {
174
- onMouseEnter?.(e);
175
-
176
- if ((prefetch === 'hover' || prefetch === true) && isInternalUrl(href)) {
177
- prefetchUrl(href);
178
- }
179
- },
180
- [href, prefetch, onMouseEnter]
181
- );
182
-
183
- // Handle focus prefetch (for keyboard navigation)
184
- const handleFocus = useCallback(
185
- (e: React.FocusEvent<HTMLAnchorElement>) => {
186
- onFocus?.(e);
187
-
188
- if ((prefetch === 'hover' || prefetch === true) && isInternalUrl(href)) {
189
- prefetchUrl(href);
190
- }
191
- },
192
- [href, prefetch, onFocus]
193
- );
194
-
195
- // Handle click for client-side navigation
196
- const handleClick = useCallback(
197
- async (e: React.MouseEvent<HTMLAnchorElement>) => {
198
- onClick?.(e);
199
-
200
- // Don't handle if default was prevented
201
- if (e.defaultPrevented) return;
202
-
203
- // Don't handle if modifier keys are pressed (open in new tab, etc.)
204
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
205
-
206
- // Don't handle external URLs
207
- if (!isInternalUrl(href)) return;
208
-
209
- // Don't handle if target is set
210
- if (props.target && props.target !== '_self') return;
211
-
212
- // Prevent default navigation
213
- e.preventDefault();
214
-
215
- // Start navigation
216
- setIsNavigating(true);
217
- onNavigationStart?.();
218
-
219
- try {
220
- // Fetch the new page
221
- const response = await fetch(href, {
222
- method: 'GET',
223
- credentials: 'same-origin',
224
- headers: {
225
- 'X-Flexi-Navigation': '1',
226
- 'Accept': 'text/html'
227
- }
228
- });
229
-
230
- if (response.ok) {
231
- const html = await response.text();
232
-
233
- // Parse and update the page
234
- const parser = new DOMParser();
235
- const doc = parser.parseFromString(html, 'text/html');
236
-
237
- // Update title
238
- const newTitle = doc.querySelector('title')?.textContent;
239
- if (newTitle) {
240
- document.title = newTitle;
241
- }
242
-
243
- // Update body content (or specific container)
244
- const newContent = doc.querySelector('#root') || doc.body;
245
- const currentContent = document.querySelector('#root') || document.body;
246
-
247
- if (newContent && currentContent) {
248
- currentContent.innerHTML = newContent.innerHTML;
249
- }
250
-
251
- // Update URL
252
- navigate(href, { replace, scroll });
253
- } else {
254
- // Fallback to regular navigation on error
255
- window.location.href = href;
256
- }
257
- } catch (error) {
258
- // Fallback to regular navigation on error
259
- window.location.href = href;
260
- } finally {
261
- setIsNavigating(false);
262
- onNavigationEnd?.();
263
- }
264
- },
265
- [href, replace, scroll, onClick, onNavigationStart, onNavigationEnd, props.target]
266
- );
267
-
268
- return (
269
- <a
270
- ref={linkRef}
271
- href={href}
272
- className={className}
273
- onClick={handleClick}
274
- onMouseEnter={handleMouseEnter}
275
- onFocus={handleFocus}
276
- data-prefetch={prefetch}
277
- data-navigating={isNavigating || undefined}
278
- {...props}
279
- >
280
- {showLoading && isNavigating ? (
281
- loadingComponent || (
282
- <span className="flexi-link-loading">
283
- <span className="flexi-link-spinner" />
284
- {children}
285
- </span>
286
- )
287
- ) : (
288
- children
289
- )}
290
- </a>
291
- );
292
- }
293
-
294
- /**
295
- * Programmatic navigation function
296
- *
297
- * @example
298
- * ```tsx
299
- * import { useRouter } from '@flexireact/core/client';
300
- *
301
- * function MyComponent() {
302
- * const router = useRouter();
303
- *
304
- * const handleClick = () => {
305
- * router.push('/dashboard');
306
- * };
307
- *
308
- * return <button onClick={handleClick}>Go to Dashboard</button>;
309
- * }
310
- * ```
311
- */
312
- export function useRouter() {
313
- return {
314
- push(url: string, options?: { scroll?: boolean }) {
315
- navigate(url, { replace: false, scroll: options?.scroll ?? true });
316
- // Trigger page reload for now (full SPA navigation requires more work)
317
- window.location.href = url;
318
- },
319
-
320
- replace(url: string, options?: { scroll?: boolean }) {
321
- navigate(url, { replace: true, scroll: options?.scroll ?? true });
322
- window.location.href = url;
323
- },
324
-
325
- back() {
326
- window.history.back();
327
- },
328
-
329
- forward() {
330
- window.history.forward();
331
- },
332
-
333
- prefetch(url: string) {
334
- if (isInternalUrl(url)) {
335
- prefetchUrl(url);
336
- }
337
- },
338
-
339
- refresh() {
340
- window.location.reload();
341
- }
342
- };
343
- }
344
-
345
- export default Link;