@djangocfg/ui-core 2.1.89 → 2.1.91
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/README.md +8 -9
- package/package.json +5 -3
- package/src/components/button-download.tsx +276 -0
- package/src/components/dropdown-menu.tsx +219 -0
- package/src/components/index.ts +16 -2
- package/src/components/menubar.tsx +274 -0
- package/src/components/multi-select-pro/async.tsx +600 -0
- package/src/components/multi-select-pro/helpers.tsx +84 -0
- package/src/components/multi-select-pro/index.tsx +613 -0
- package/src/components/navigation-menu.tsx +153 -0
- package/src/components/otp/index.tsx +198 -0
- package/src/components/otp/types.ts +133 -0
- package/src/components/otp/use-otp-input.ts +225 -0
- package/src/components/phone-input.tsx +277 -0
- package/src/components/sonner.tsx +32 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useCopy.ts +2 -10
- package/src/hooks/useLocalStorage.ts +300 -0
- package/src/hooks/useResolvedTheme.ts +68 -0
- package/src/hooks/useSessionStorage.ts +290 -0
- package/src/hooks/useToast.ts +20 -244
- package/src/lib/index.ts +1 -0
- package/src/lib/logger/index.ts +10 -0
- package/src/lib/logger/logStore.ts +122 -0
- package/src/lib/logger/logger.ts +175 -0
- package/src/lib/logger/types.ts +82 -0
- package/src/utils/LazyComponent.tsx +116 -0
- package/src/utils/index.ts +9 -0
- package/src/components/toast.tsx +0 -144
- package/src/components/toaster.tsx +0 -41
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Store
|
|
3
|
+
*
|
|
4
|
+
* Zustand store for log accumulation and filtering.
|
|
5
|
+
* Keeps logs in memory for Console panel display.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { create } from 'zustand';
|
|
11
|
+
import type { LogStore, LogEntry, LogFilter } from './types';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_FILTER: LogFilter = {
|
|
14
|
+
levels: ['debug', 'info', 'warn', 'error', 'success'],
|
|
15
|
+
component: undefined,
|
|
16
|
+
search: undefined,
|
|
17
|
+
since: undefined,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MAX_LOGS = 1000;
|
|
21
|
+
|
|
22
|
+
function generateId(): string {
|
|
23
|
+
return `log-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function matchesFilter(entry: LogEntry, filter: LogFilter): boolean {
|
|
27
|
+
// Level filter
|
|
28
|
+
if (!filter.levels.includes(entry.level)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Component filter (case-insensitive partial match)
|
|
33
|
+
if (filter.component) {
|
|
34
|
+
const comp = filter.component.toLowerCase();
|
|
35
|
+
if (!entry.component.toLowerCase().includes(comp)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Search filter (case-insensitive, searches message and data)
|
|
41
|
+
if (filter.search) {
|
|
42
|
+
const search = filter.search.toLowerCase();
|
|
43
|
+
const inMessage = entry.message.toLowerCase().includes(search);
|
|
44
|
+
const inData = entry.data
|
|
45
|
+
? JSON.stringify(entry.data).toLowerCase().includes(search)
|
|
46
|
+
: false;
|
|
47
|
+
if (!inMessage && !inData) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Time filter
|
|
53
|
+
if (filter.since && entry.timestamp < filter.since) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const useLogStore = create<LogStore>((set, get) => ({
|
|
61
|
+
logs: [],
|
|
62
|
+
filter: DEFAULT_FILTER,
|
|
63
|
+
maxLogs: MAX_LOGS,
|
|
64
|
+
|
|
65
|
+
addLog: (entry) => {
|
|
66
|
+
const newEntry: LogEntry = {
|
|
67
|
+
...entry,
|
|
68
|
+
id: generateId(),
|
|
69
|
+
timestamp: new Date(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
set((state) => {
|
|
73
|
+
const newLogs = [...state.logs, newEntry];
|
|
74
|
+
// Trim to max size
|
|
75
|
+
if (newLogs.length > state.maxLogs) {
|
|
76
|
+
return { logs: newLogs.slice(-state.maxLogs) };
|
|
77
|
+
}
|
|
78
|
+
return { logs: newLogs };
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
clearLogs: () => {
|
|
83
|
+
set({ logs: [] });
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
setFilter: (filter) => {
|
|
87
|
+
set((state) => ({
|
|
88
|
+
filter: { ...state.filter, ...filter },
|
|
89
|
+
}));
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
resetFilter: () => {
|
|
93
|
+
set({ filter: DEFAULT_FILTER });
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
getFilteredLogs: () => {
|
|
97
|
+
const { logs, filter } = get();
|
|
98
|
+
return logs.filter((entry) => matchesFilter(entry, filter));
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
exportLogs: () => {
|
|
102
|
+
const { logs } = get();
|
|
103
|
+
return JSON.stringify(logs, null, 2);
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
// Selector hooks for performance
|
|
108
|
+
export const useFilteredLogs = () => {
|
|
109
|
+
const logs = useLogStore((state) => state.logs);
|
|
110
|
+
const filter = useLogStore((state) => state.filter);
|
|
111
|
+
return logs.filter((entry) => matchesFilter(entry, filter));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const useLogCount = () => {
|
|
115
|
+
return useLogStore((state) => state.logs.length);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const useErrorCount = () => {
|
|
119
|
+
return useLogStore((state) =>
|
|
120
|
+
state.logs.filter((log) => log.level === 'error').length
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger
|
|
3
|
+
*
|
|
4
|
+
* Universal logger with consola + zustand store integration.
|
|
5
|
+
* Logs are accumulated for Console panel display.
|
|
6
|
+
* In production, only logs to store (no console output).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use client';
|
|
10
|
+
|
|
11
|
+
import { consola, type ConsolaInstance } from 'consola';
|
|
12
|
+
import { useLogStore } from './logStore';
|
|
13
|
+
import type { Logger, LogLevel, MediaLogger } from './types';
|
|
14
|
+
|
|
15
|
+
// Check environment
|
|
16
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
17
|
+
const isBrowser = typeof window !== 'undefined';
|
|
18
|
+
|
|
19
|
+
// Create base consola instance
|
|
20
|
+
const baseConsola = consola.create({
|
|
21
|
+
level: isDev ? 4 : 2, // 4 = debug, 2 = warn
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract error details from unknown error type
|
|
26
|
+
*/
|
|
27
|
+
function extractErrorData(data?: Record<string, unknown>): {
|
|
28
|
+
cleanData: Record<string, unknown> | undefined;
|
|
29
|
+
stack: string | undefined;
|
|
30
|
+
} {
|
|
31
|
+
if (!data) return { cleanData: undefined, stack: undefined };
|
|
32
|
+
|
|
33
|
+
const cleanData = { ...data };
|
|
34
|
+
let stack: string | undefined;
|
|
35
|
+
|
|
36
|
+
// Extract stack from error object
|
|
37
|
+
if (data.error instanceof Error) {
|
|
38
|
+
stack = data.error.stack;
|
|
39
|
+
cleanData.error = {
|
|
40
|
+
name: data.error.name,
|
|
41
|
+
message: data.error.message,
|
|
42
|
+
};
|
|
43
|
+
} else if (typeof data.error === 'object' && data.error !== null) {
|
|
44
|
+
const errObj = data.error as Record<string, unknown>;
|
|
45
|
+
if (typeof errObj.stack === 'string') {
|
|
46
|
+
stack = errObj.stack;
|
|
47
|
+
}
|
|
48
|
+
if (typeof errObj.message === 'string') {
|
|
49
|
+
cleanData.error = errObj.message;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { cleanData, stack };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format buffer ranges for logging
|
|
58
|
+
*/
|
|
59
|
+
function formatBufferRanges(buffered: TimeRanges, duration: number): string {
|
|
60
|
+
if (buffered.length === 0) return 'empty';
|
|
61
|
+
|
|
62
|
+
const ranges: string[] = [];
|
|
63
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
64
|
+
const start = buffered.start(i);
|
|
65
|
+
const end = buffered.end(i);
|
|
66
|
+
const percent = ((end - start) / duration * 100).toFixed(1);
|
|
67
|
+
ranges.push(`${start.toFixed(1)}-${end.toFixed(1)}s (${percent}%)`);
|
|
68
|
+
}
|
|
69
|
+
return ranges.join(', ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a logger for a specific component/module
|
|
74
|
+
*/
|
|
75
|
+
export function createLogger(component: string): Logger {
|
|
76
|
+
const consolaTagged = baseConsola.withTag(component);
|
|
77
|
+
|
|
78
|
+
const log = (level: LogLevel, message: string, data?: Record<string, unknown>) => {
|
|
79
|
+
const { cleanData, stack } = extractErrorData(data);
|
|
80
|
+
|
|
81
|
+
// Add to store (for Console panel)
|
|
82
|
+
if (isBrowser) {
|
|
83
|
+
useLogStore.getState().addLog({
|
|
84
|
+
level,
|
|
85
|
+
component,
|
|
86
|
+
message,
|
|
87
|
+
data: cleanData,
|
|
88
|
+
stack,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Log to console via consola (in dev mode)
|
|
93
|
+
if (isDev) {
|
|
94
|
+
const consolaMethod = level === 'success' ? 'success' : level;
|
|
95
|
+
if (cleanData) {
|
|
96
|
+
consolaTagged[consolaMethod](message, cleanData);
|
|
97
|
+
} else {
|
|
98
|
+
consolaTagged[consolaMethod](message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
debug: (message, data) => log('debug', message, data),
|
|
105
|
+
info: (message, data) => log('info', message, data),
|
|
106
|
+
warn: (message, data) => log('warn', message, data),
|
|
107
|
+
error: (message, data) => log('error', message, data),
|
|
108
|
+
success: (message, data) => log('success', message, data),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a media-specific logger with helper methods
|
|
114
|
+
* for AudioPlayer, VideoPlayer, ImageViewer
|
|
115
|
+
*/
|
|
116
|
+
export function createMediaLogger(component: string): MediaLogger {
|
|
117
|
+
const baseLogger = createLogger(component);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...baseLogger,
|
|
121
|
+
|
|
122
|
+
load: (src: string, type?: string) => {
|
|
123
|
+
const typeStr = type ? ` (${type})` : '';
|
|
124
|
+
baseLogger.info(`LOAD: ${src}${typeStr}`);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
state: (state: string, details?: Record<string, unknown>) => {
|
|
128
|
+
baseLogger.debug(`STATE: ${state}`, details);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
seek: (from: number, to: number, duration: number) => {
|
|
132
|
+
baseLogger.debug(`SEEK: ${from.toFixed(2)}s -> ${to.toFixed(2)}s`, {
|
|
133
|
+
from,
|
|
134
|
+
to,
|
|
135
|
+
duration,
|
|
136
|
+
progress: `${((to / duration) * 100).toFixed(1)}%`,
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
buffer: (buffered: TimeRanges, duration: number) => {
|
|
141
|
+
if (buffered.length > 0) {
|
|
142
|
+
baseLogger.debug(`BUFFER: ${formatBufferRanges(buffered, duration)}`);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
event: (name: string, data?: unknown) => {
|
|
147
|
+
if (data !== undefined) {
|
|
148
|
+
baseLogger.debug(`EVENT: ${name}`, { data });
|
|
149
|
+
} else {
|
|
150
|
+
baseLogger.debug(`EVENT: ${name}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Global logger for non-component code
|
|
158
|
+
*/
|
|
159
|
+
export const logger = createLogger('App');
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Quick access for one-off logs
|
|
163
|
+
*/
|
|
164
|
+
export const log = {
|
|
165
|
+
debug: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
166
|
+
createLogger(component).debug(message, data),
|
|
167
|
+
info: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
168
|
+
createLogger(component).info(message, data),
|
|
169
|
+
warn: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
170
|
+
createLogger(component).warn(message, data),
|
|
171
|
+
error: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
172
|
+
createLogger(component).error(message, data),
|
|
173
|
+
success: (component: string, message: string, data?: Record<string, unknown>) =>
|
|
174
|
+
createLogger(component).success(message, data),
|
|
175
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the universal logging system.
|
|
5
|
+
* Compatible with Console panel in FileWorkspace IDE layout.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
|
|
9
|
+
|
|
10
|
+
export interface LogEntry {
|
|
11
|
+
/** Unique log ID */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Timestamp when log was created */
|
|
14
|
+
timestamp: Date;
|
|
15
|
+
/** Log level */
|
|
16
|
+
level: LogLevel;
|
|
17
|
+
/** Component/module name that created the log */
|
|
18
|
+
component: string;
|
|
19
|
+
/** Log message */
|
|
20
|
+
message: string;
|
|
21
|
+
/** Additional data (objects, errors, etc.) */
|
|
22
|
+
data?: Record<string, unknown>;
|
|
23
|
+
/** Error stack trace (for error level) */
|
|
24
|
+
stack?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LogFilter {
|
|
28
|
+
/** Filter by log levels */
|
|
29
|
+
levels: LogLevel[];
|
|
30
|
+
/** Filter by component name (partial match) */
|
|
31
|
+
component?: string;
|
|
32
|
+
/** Filter by message text (partial match) */
|
|
33
|
+
search?: string;
|
|
34
|
+
/** Filter by time range */
|
|
35
|
+
since?: Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LogStore {
|
|
39
|
+
/** All accumulated logs */
|
|
40
|
+
logs: LogEntry[];
|
|
41
|
+
/** Current filter settings */
|
|
42
|
+
filter: LogFilter;
|
|
43
|
+
/** Maximum logs to keep */
|
|
44
|
+
maxLogs: number;
|
|
45
|
+
|
|
46
|
+
/** Add new log entry */
|
|
47
|
+
addLog: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
48
|
+
/** Clear all logs */
|
|
49
|
+
clearLogs: () => void;
|
|
50
|
+
/** Update filter settings */
|
|
51
|
+
setFilter: (filter: Partial<LogFilter>) => void;
|
|
52
|
+
/** Reset filter to defaults */
|
|
53
|
+
resetFilter: () => void;
|
|
54
|
+
/** Get filtered logs */
|
|
55
|
+
getFilteredLogs: () => LogEntry[];
|
|
56
|
+
/** Export logs as JSON string */
|
|
57
|
+
exportLogs: () => string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Logger {
|
|
61
|
+
debug: (message: string, data?: Record<string, unknown>) => void;
|
|
62
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
63
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
64
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
65
|
+
success: (message: string, data?: Record<string, unknown>) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Media-specific log helper methods
|
|
70
|
+
*/
|
|
71
|
+
export interface MediaLogger extends Logger {
|
|
72
|
+
/** Log source load event */
|
|
73
|
+
load: (src: string, type?: string) => void;
|
|
74
|
+
/** Log state change */
|
|
75
|
+
state: (state: string, details?: Record<string, unknown>) => void;
|
|
76
|
+
/** Log seek event */
|
|
77
|
+
seek: (from: number, to: number, duration: number) => void;
|
|
78
|
+
/** Log buffer ranges */
|
|
79
|
+
buffer: (buffered: TimeRanges, duration: number) => void;
|
|
80
|
+
/** Log generic event */
|
|
81
|
+
event: (name: string, data?: unknown) => void;
|
|
82
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LazyComponent - Universal lazy loading wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent Suspense handling for lazy-loaded components
|
|
5
|
+
* Works in any React environment: Next.js, Vite, Wails, CRA
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { Suspense, ReactNode, ComponentType, lazy } from 'react';
|
|
11
|
+
|
|
12
|
+
// Default loading spinner
|
|
13
|
+
export const DefaultLoader = () => (
|
|
14
|
+
<div className="flex items-center justify-center p-8">
|
|
15
|
+
<div className="flex flex-col items-center gap-2">
|
|
16
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary" />
|
|
17
|
+
<span className="text-sm text-muted-foreground">Loading...</span>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Minimal loader for inline components
|
|
23
|
+
export const InlineLoader = () => (
|
|
24
|
+
<span className="inline-flex items-center gap-1">
|
|
25
|
+
<span className="h-3 w-3 animate-spin rounded-full border-2 border-muted border-t-primary" />
|
|
26
|
+
<span className="text-xs text-muted-foreground">Loading...</span>
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Full-screen loader
|
|
31
|
+
export const FullScreenLoader = () => (
|
|
32
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
33
|
+
<div className="text-center">
|
|
34
|
+
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent" />
|
|
35
|
+
<p className="mt-4 text-sm text-muted-foreground">Loading...</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export interface LazyWrapperProps {
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
fallback?: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Suspense wrapper with default fallback
|
|
47
|
+
*/
|
|
48
|
+
export function LazyWrapper({ children, fallback }: LazyWrapperProps) {
|
|
49
|
+
return (
|
|
50
|
+
<Suspense fallback={fallback ?? <DefaultLoader />}>
|
|
51
|
+
{children}
|
|
52
|
+
</Suspense>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a lazy-loaded component with Suspense wrapper built-in
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```tsx
|
|
61
|
+
* const HeavyComponent = createLazyComponent(
|
|
62
|
+
* () => import('./HeavyComponent'),
|
|
63
|
+
* <CustomLoader />
|
|
64
|
+
* );
|
|
65
|
+
*
|
|
66
|
+
* // Usage - no need to wrap in Suspense
|
|
67
|
+
* <HeavyComponent someProps={value} />
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function createLazyComponent<T extends ComponentType<any>>(
|
|
71
|
+
importFn: () => Promise<{ default: T }>,
|
|
72
|
+
fallback?: ReactNode
|
|
73
|
+
) {
|
|
74
|
+
const LazyComponent = lazy(importFn);
|
|
75
|
+
|
|
76
|
+
return function WrappedLazyComponent(props: React.ComponentProps<T>) {
|
|
77
|
+
return (
|
|
78
|
+
<Suspense fallback={fallback ?? <DefaultLoader />}>
|
|
79
|
+
<LazyComponent {...props} />
|
|
80
|
+
</Suspense>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a lazy-loaded component from a named export
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* const NamedComponent = createLazyNamedComponent(
|
|
91
|
+
* () => import('./module'),
|
|
92
|
+
* 'NamedExport',
|
|
93
|
+
* <CustomLoader />
|
|
94
|
+
* );
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function createLazyNamedComponent<
|
|
98
|
+
T extends Record<string, ComponentType<any>>,
|
|
99
|
+
K extends keyof T
|
|
100
|
+
>(
|
|
101
|
+
importFn: () => Promise<T>,
|
|
102
|
+
exportName: K,
|
|
103
|
+
fallback?: ReactNode
|
|
104
|
+
) {
|
|
105
|
+
const LazyComponent = lazy(() =>
|
|
106
|
+
importFn().then((mod) => ({ default: mod[exportName] }))
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return function WrappedLazyComponent(props: React.ComponentProps<T[K]>) {
|
|
110
|
+
return (
|
|
111
|
+
<Suspense fallback={fallback ?? <DefaultLoader />}>
|
|
112
|
+
<LazyComponent {...props} />
|
|
113
|
+
</Suspense>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
}
|
package/src/components/toast.tsx
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
-
import { X } from 'lucide-react';
|
|
5
|
-
import * as React from 'react';
|
|
6
|
-
|
|
7
|
-
import * as ToastPrimitives from '@radix-ui/react-toast';
|
|
8
|
-
|
|
9
|
-
import { cn } from '../lib/utils';
|
|
10
|
-
|
|
11
|
-
const ToastProvider = ToastPrimitives.Provider
|
|
12
|
-
|
|
13
|
-
const ToastViewport = React.forwardRef<
|
|
14
|
-
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
|
15
|
-
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
16
|
-
>(({ className, ...props }, ref) => (
|
|
17
|
-
<ToastPrimitives.Viewport
|
|
18
|
-
ref={ref}
|
|
19
|
-
className={cn(
|
|
20
|
-
"fixed bottom-0 right-0 z-9999 flex max-h-screen w-full flex-col p-4 md:max-w-md",
|
|
21
|
-
className
|
|
22
|
-
)}
|
|
23
|
-
{...props}
|
|
24
|
-
/>
|
|
25
|
-
))
|
|
26
|
-
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
|
27
|
-
|
|
28
|
-
const toastVariants = cva(
|
|
29
|
-
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-bottom-full",
|
|
30
|
-
{
|
|
31
|
-
variants: {
|
|
32
|
-
variant: {
|
|
33
|
-
default: "border bg-background text-foreground",
|
|
34
|
-
destructive:
|
|
35
|
-
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
|
36
|
-
success:
|
|
37
|
-
"success group border-green-600 bg-green-500 text-white",
|
|
38
|
-
warning:
|
|
39
|
-
"warning group border-yellow-600 bg-yellow-500 text-black",
|
|
40
|
-
info:
|
|
41
|
-
"info group border-blue-600 bg-blue-500 text-white",
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
defaultVariants: {
|
|
45
|
-
variant: "default",
|
|
46
|
-
},
|
|
47
|
-
}
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
const Toast = React.forwardRef<
|
|
51
|
-
React.ElementRef<typeof ToastPrimitives.Root>,
|
|
52
|
-
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
|
53
|
-
VariantProps<typeof toastVariants>
|
|
54
|
-
>(({ className, variant, ...props }, ref) => {
|
|
55
|
-
return (
|
|
56
|
-
<ToastPrimitives.Root
|
|
57
|
-
ref={ref}
|
|
58
|
-
className={cn(toastVariants({ variant }), className)}
|
|
59
|
-
{...props}
|
|
60
|
-
/>
|
|
61
|
-
)
|
|
62
|
-
})
|
|
63
|
-
Toast.displayName = ToastPrimitives.Root.displayName
|
|
64
|
-
|
|
65
|
-
const ToastAction = React.forwardRef<
|
|
66
|
-
React.ElementRef<typeof ToastPrimitives.Action>,
|
|
67
|
-
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
|
68
|
-
>(({ className, ...props }, ref) => (
|
|
69
|
-
<ToastPrimitives.Action
|
|
70
|
-
ref={ref}
|
|
71
|
-
className={cn(
|
|
72
|
-
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
73
|
-
"group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
|
74
|
-
"group-[.success]:border-green-300/40 group-[.success]:hover:bg-green-600 group-[.success]:hover:text-white group-[.success]:focus:ring-green-400",
|
|
75
|
-
"group-[.warning]:border-yellow-600/40 group-[.warning]:hover:bg-yellow-600 group-[.warning]:hover:text-black group-[.warning]:focus:ring-yellow-400",
|
|
76
|
-
"group-[.info]:border-blue-300/40 group-[.info]:hover:bg-blue-600 group-[.info]:hover:text-white group-[.info]:focus:ring-blue-400",
|
|
77
|
-
className
|
|
78
|
-
)}
|
|
79
|
-
{...props}
|
|
80
|
-
/>
|
|
81
|
-
))
|
|
82
|
-
ToastAction.displayName = ToastPrimitives.Action.displayName
|
|
83
|
-
|
|
84
|
-
const ToastClose = React.forwardRef<
|
|
85
|
-
React.ElementRef<typeof ToastPrimitives.Close>,
|
|
86
|
-
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
|
87
|
-
>(({ className, ...props }, ref) => (
|
|
88
|
-
<ToastPrimitives.Close
|
|
89
|
-
ref={ref}
|
|
90
|
-
className={cn(
|
|
91
|
-
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100",
|
|
92
|
-
"group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
|
93
|
-
"group-[.success]:text-green-100 group-[.success]:hover:text-white group-[.success]:focus:ring-green-300",
|
|
94
|
-
"group-[.warning]:text-yellow-800 group-[.warning]:hover:text-black group-[.warning]:focus:ring-yellow-600",
|
|
95
|
-
"group-[.info]:text-blue-100 group-[.info]:hover:text-white group-[.info]:focus:ring-blue-300",
|
|
96
|
-
className
|
|
97
|
-
)}
|
|
98
|
-
toast-close=""
|
|
99
|
-
{...props}
|
|
100
|
-
>
|
|
101
|
-
<X className="h-4 w-4" />
|
|
102
|
-
</ToastPrimitives.Close>
|
|
103
|
-
))
|
|
104
|
-
ToastClose.displayName = ToastPrimitives.Close.displayName
|
|
105
|
-
|
|
106
|
-
const ToastTitle = React.forwardRef<
|
|
107
|
-
React.ElementRef<typeof ToastPrimitives.Title>,
|
|
108
|
-
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
|
109
|
-
>(({ className, ...props }, ref) => (
|
|
110
|
-
<ToastPrimitives.Title
|
|
111
|
-
ref={ref}
|
|
112
|
-
className={cn("text-sm font-semibold", className)}
|
|
113
|
-
{...props}
|
|
114
|
-
/>
|
|
115
|
-
))
|
|
116
|
-
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
|
117
|
-
|
|
118
|
-
const ToastDescription = React.forwardRef<
|
|
119
|
-
React.ElementRef<typeof ToastPrimitives.Description>,
|
|
120
|
-
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
|
121
|
-
>(({ className, ...props }, ref) => (
|
|
122
|
-
<ToastPrimitives.Description
|
|
123
|
-
ref={ref}
|
|
124
|
-
className={cn("text-sm opacity-90", className)}
|
|
125
|
-
{...props}
|
|
126
|
-
/>
|
|
127
|
-
))
|
|
128
|
-
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
|
129
|
-
|
|
130
|
-
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
|
131
|
-
|
|
132
|
-
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
|
133
|
-
|
|
134
|
-
export {
|
|
135
|
-
type ToastProps,
|
|
136
|
-
type ToastActionElement,
|
|
137
|
-
ToastProvider,
|
|
138
|
-
ToastViewport,
|
|
139
|
-
Toast,
|
|
140
|
-
ToastTitle,
|
|
141
|
-
ToastDescription,
|
|
142
|
-
ToastClose,
|
|
143
|
-
ToastAction,
|
|
144
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
import { useToast } from '../hooks/useToast';
|
|
6
|
-
import {
|
|
7
|
-
Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport
|
|
8
|
-
} from './toast';
|
|
9
|
-
|
|
10
|
-
export function Toaster() {
|
|
11
|
-
const { toasts } = useToast()
|
|
12
|
-
const [isMounted, setIsMounted] = useState(false)
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
setIsMounted(true)
|
|
16
|
-
}, [])
|
|
17
|
-
|
|
18
|
-
if (!isMounted) {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<ToastProvider>
|
|
24
|
-
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
25
|
-
return (
|
|
26
|
-
<Toast key={id} {...props}>
|
|
27
|
-
<div className="grid gap-1">
|
|
28
|
-
{title && <ToastTitle>{title}</ToastTitle>}
|
|
29
|
-
{description && (
|
|
30
|
-
<ToastDescription>{description}</ToastDescription>
|
|
31
|
-
)}
|
|
32
|
-
</div>
|
|
33
|
-
{action}
|
|
34
|
-
<ToastClose />
|
|
35
|
-
</Toast>
|
|
36
|
-
)
|
|
37
|
-
})}
|
|
38
|
-
<ToastViewport />
|
|
39
|
-
</ToastProvider>
|
|
40
|
-
)
|
|
41
|
-
}
|