@flexireact/core 3.0.0 → 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 (56) hide show
  1. package/README.md +204 -52
  2. package/dist/cli/index.js +1514 -0
  3. package/dist/cli/index.js.map +1 -0
  4. package/dist/core/client/index.js +373 -0
  5. package/dist/core/client/index.js.map +1 -0
  6. package/dist/core/index.js +6415 -0
  7. package/dist/core/index.js.map +1 -0
  8. package/dist/core/server/index.js +3094 -0
  9. package/dist/core/server/index.js.map +1 -0
  10. package/package.json +80 -80
  11. package/bin/flexireact.js +0 -23
  12. package/cli/generators.ts +0 -616
  13. package/cli/index.ts +0 -1182
  14. package/core/actions/index.ts +0 -364
  15. package/core/api.ts +0 -143
  16. package/core/build/index.ts +0 -425
  17. package/core/cli/logger.ts +0 -353
  18. package/core/client/Link.tsx +0 -345
  19. package/core/client/hydration.ts +0 -147
  20. package/core/client/index.ts +0 -12
  21. package/core/client/islands.ts +0 -143
  22. package/core/client/navigation.ts +0 -212
  23. package/core/client/runtime.ts +0 -52
  24. package/core/config.ts +0 -116
  25. package/core/context.ts +0 -83
  26. package/core/dev.ts +0 -47
  27. package/core/devtools/index.ts +0 -644
  28. package/core/edge/cache.ts +0 -344
  29. package/core/edge/fetch-polyfill.ts +0 -247
  30. package/core/edge/handler.ts +0 -248
  31. package/core/edge/index.ts +0 -81
  32. package/core/edge/ppr.ts +0 -264
  33. package/core/edge/runtime.ts +0 -161
  34. package/core/font/index.ts +0 -306
  35. package/core/helpers.ts +0 -494
  36. package/core/image/index.ts +0 -413
  37. package/core/index.ts +0 -218
  38. package/core/islands/index.ts +0 -293
  39. package/core/loader.ts +0 -111
  40. package/core/logger.ts +0 -242
  41. package/core/metadata/index.ts +0 -622
  42. package/core/middleware/index.ts +0 -416
  43. package/core/plugins/index.ts +0 -373
  44. package/core/render/index.ts +0 -1243
  45. package/core/render.ts +0 -136
  46. package/core/router/index.ts +0 -551
  47. package/core/router.ts +0 -141
  48. package/core/rsc/index.ts +0 -199
  49. package/core/server/index.ts +0 -779
  50. package/core/server.ts +0 -203
  51. package/core/ssg/index.ts +0 -346
  52. package/core/start-dev.ts +0 -6
  53. package/core/start-prod.ts +0 -6
  54. package/core/tsconfig.json +0 -30
  55. package/core/types.ts +0 -239
  56. 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;