@djangocfg/ui-core 2.1.90 → 2.1.92
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,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export type ResolvedTheme = 'light' | 'dark';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to detect the current resolved theme (light or dark)
|
|
9
|
+
*
|
|
10
|
+
* Standalone hook - doesn't require ThemeProvider.
|
|
11
|
+
* Detects theme from:
|
|
12
|
+
* 1. 'dark' class on html element
|
|
13
|
+
* 2. System preference (prefers-color-scheme)
|
|
14
|
+
*
|
|
15
|
+
* For full theme control (setTheme, toggleTheme), use useThemeContext instead.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const theme = useResolvedTheme(); // 'light' | 'dark'
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const useResolvedTheme = (): ResolvedTheme => {
|
|
23
|
+
const [theme, setTheme] = useState<ResolvedTheme>('light');
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const checkTheme = (): ResolvedTheme => {
|
|
27
|
+
// Check if dark class is applied to html element
|
|
28
|
+
if (document.documentElement.classList.contains('dark')) {
|
|
29
|
+
return 'dark';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check system preference
|
|
33
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
34
|
+
return 'dark';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 'light';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Set initial theme
|
|
41
|
+
setTheme(checkTheme());
|
|
42
|
+
|
|
43
|
+
// Listen for class changes on html element
|
|
44
|
+
const observer = new MutationObserver(() => {
|
|
45
|
+
setTheme(checkTheme());
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
observer.observe(document.documentElement, {
|
|
49
|
+
attributes: true,
|
|
50
|
+
attributeFilter: ['class']
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Listen for system theme changes
|
|
54
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
55
|
+
const handleMediaChange = () => {
|
|
56
|
+
setTheme(checkTheme());
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
mediaQuery.addEventListener('change', handleMediaChange);
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
observer.disconnect();
|
|
63
|
+
mediaQuery.removeEventListener('change', handleMediaChange);
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return theme;
|
|
68
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storage wrapper format with metadata
|
|
7
|
+
* Used when TTL is specified
|
|
8
|
+
*/
|
|
9
|
+
interface StorageWrapper<T> {
|
|
10
|
+
_meta: {
|
|
11
|
+
createdAt: number;
|
|
12
|
+
ttl: number;
|
|
13
|
+
};
|
|
14
|
+
_value: T;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for useSessionStorage hook
|
|
19
|
+
*/
|
|
20
|
+
export interface UseSessionStorageOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Time-to-live in milliseconds.
|
|
23
|
+
* After this time, value is considered expired and initialValue is returned.
|
|
24
|
+
* Data is automatically cleaned up on next read.
|
|
25
|
+
* @example 24 * 60 * 60 * 1000 // 24 hours
|
|
26
|
+
*/
|
|
27
|
+
ttl?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if data is in new wrapped format with _meta
|
|
32
|
+
*/
|
|
33
|
+
function isWrappedFormat<T>(data: unknown): data is StorageWrapper<T> {
|
|
34
|
+
return (
|
|
35
|
+
data !== null &&
|
|
36
|
+
typeof data === 'object' &&
|
|
37
|
+
'_meta' in data &&
|
|
38
|
+
'_value' in data &&
|
|
39
|
+
typeof (data as StorageWrapper<T>)._meta === 'object' &&
|
|
40
|
+
typeof (data as StorageWrapper<T>)._meta.createdAt === 'number'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if wrapped data is expired
|
|
46
|
+
*/
|
|
47
|
+
function isExpired<T>(wrapped: StorageWrapper<T>): boolean {
|
|
48
|
+
if (!wrapped._meta.ttl) return false;
|
|
49
|
+
const age = Date.now() - wrapped._meta.createdAt;
|
|
50
|
+
return age > wrapped._meta.ttl;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Simple sessionStorage hook with better error handling and optional TTL support
|
|
55
|
+
*
|
|
56
|
+
* @param key - Storage key
|
|
57
|
+
* @param initialValue - Default value if key doesn't exist
|
|
58
|
+
* @param options - Optional configuration (ttl for auto-expiration)
|
|
59
|
+
* @returns [value, setValue, removeValue] - Current value, setter function, and remove function
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // Without TTL (backwards compatible)
|
|
63
|
+
* const [value, setValue] = useSessionStorage('key', 'default');
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // With TTL (1 hour)
|
|
67
|
+
* const [value, setValue] = useSessionStorage('key', 'default', {
|
|
68
|
+
* ttl: 60 * 60 * 1000
|
|
69
|
+
* });
|
|
70
|
+
*/
|
|
71
|
+
export function useSessionStorage<T>(
|
|
72
|
+
key: string,
|
|
73
|
+
initialValue: T,
|
|
74
|
+
options?: UseSessionStorageOptions
|
|
75
|
+
) {
|
|
76
|
+
const ttl = options?.ttl;
|
|
77
|
+
|
|
78
|
+
// Get initial value from sessionStorage or use provided initialValue
|
|
79
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
80
|
+
if (typeof window === 'undefined') {
|
|
81
|
+
return initialValue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const item = window.sessionStorage.getItem(key);
|
|
86
|
+
if (item === null) return initialValue;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(item);
|
|
90
|
+
|
|
91
|
+
// Check if new format with _meta
|
|
92
|
+
if (isWrappedFormat<T>(parsed)) {
|
|
93
|
+
// Check TTL expiration
|
|
94
|
+
if (isExpired(parsed)) {
|
|
95
|
+
// Expired! Clean up and use initial value
|
|
96
|
+
window.sessionStorage.removeItem(key);
|
|
97
|
+
return initialValue;
|
|
98
|
+
}
|
|
99
|
+
// Not expired, extract value
|
|
100
|
+
return parsed._value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Old format (backwards compatible)
|
|
104
|
+
return parsed as T;
|
|
105
|
+
} catch {
|
|
106
|
+
// If JSON.parse fails, return as string
|
|
107
|
+
return item as T;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`Error reading sessionStorage key "${key}":`, error);
|
|
111
|
+
return initialValue;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Check data size and limit
|
|
116
|
+
const checkDataSize = (data: any): boolean => {
|
|
117
|
+
try {
|
|
118
|
+
const jsonString = JSON.stringify(data);
|
|
119
|
+
const sizeInBytes = new Blob([jsonString]).size;
|
|
120
|
+
const sizeInKB = sizeInBytes / 1024;
|
|
121
|
+
|
|
122
|
+
// Limit to 1MB per item
|
|
123
|
+
if (sizeInKB > 1024) {
|
|
124
|
+
console.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return true;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`Error checking data size for key "${key}":`, error);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Clear old data when sessionStorage is full
|
|
136
|
+
const clearOldData = () => {
|
|
137
|
+
try {
|
|
138
|
+
const keys = Object.keys(sessionStorage).filter(k => k && typeof k === 'string');
|
|
139
|
+
// Remove oldest items if we have more than 50 items
|
|
140
|
+
if (keys.length > 50) {
|
|
141
|
+
const itemsToRemove = Math.ceil(keys.length * 0.2);
|
|
142
|
+
for (let i = 0; i < itemsToRemove; i++) {
|
|
143
|
+
try {
|
|
144
|
+
const k = keys[i];
|
|
145
|
+
if (k) {
|
|
146
|
+
sessionStorage.removeItem(k);
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// Ignore errors when removing items
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('Error clearing old sessionStorage data:', error);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Force clear all data if quota is exceeded
|
|
159
|
+
const forceClearAll = () => {
|
|
160
|
+
try {
|
|
161
|
+
const keys = Object.keys(sessionStorage);
|
|
162
|
+
for (const k of keys) {
|
|
163
|
+
try {
|
|
164
|
+
sessionStorage.removeItem(k);
|
|
165
|
+
} catch {
|
|
166
|
+
// Ignore errors when removing items
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Error force clearing sessionStorage:', error);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Prepare data for storage (with or without TTL wrapper)
|
|
175
|
+
const prepareForStorage = (value: T): string => {
|
|
176
|
+
if (ttl) {
|
|
177
|
+
// Wrap with _meta for TTL support
|
|
178
|
+
const wrapped: StorageWrapper<T> = {
|
|
179
|
+
_meta: {
|
|
180
|
+
createdAt: Date.now(),
|
|
181
|
+
ttl,
|
|
182
|
+
},
|
|
183
|
+
_value: value,
|
|
184
|
+
};
|
|
185
|
+
return JSON.stringify(wrapped);
|
|
186
|
+
}
|
|
187
|
+
// Old format (no wrapper) - for strings, store directly
|
|
188
|
+
if (typeof value === 'string') {
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
return JSON.stringify(value);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Update sessionStorage when value changes
|
|
195
|
+
const setValue = (value: T | ((val: T) => T)) => {
|
|
196
|
+
try {
|
|
197
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
198
|
+
|
|
199
|
+
// Check data size before attempting to save
|
|
200
|
+
if (!checkDataSize(valueToStore)) {
|
|
201
|
+
console.warn(`Data size too large for key "${key}", removing key`);
|
|
202
|
+
// Remove the key if data is too large
|
|
203
|
+
try {
|
|
204
|
+
window.sessionStorage.removeItem(key);
|
|
205
|
+
} catch {
|
|
206
|
+
// Ignore errors when removing
|
|
207
|
+
}
|
|
208
|
+
// Still update the state
|
|
209
|
+
setStoredValue(valueToStore);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
setStoredValue(valueToStore);
|
|
214
|
+
|
|
215
|
+
if (typeof window !== 'undefined') {
|
|
216
|
+
const dataToStore = prepareForStorage(valueToStore);
|
|
217
|
+
|
|
218
|
+
// Try to set the value
|
|
219
|
+
try {
|
|
220
|
+
window.sessionStorage.setItem(key, dataToStore);
|
|
221
|
+
} catch (storageError: any) {
|
|
222
|
+
// If quota exceeded, clear old data and try again
|
|
223
|
+
if (storageError.name === 'QuotaExceededError' ||
|
|
224
|
+
storageError.code === 22 ||
|
|
225
|
+
storageError.message?.includes('quota')) {
|
|
226
|
+
console.warn('sessionStorage quota exceeded, clearing old data...');
|
|
227
|
+
clearOldData();
|
|
228
|
+
|
|
229
|
+
// Try again after clearing
|
|
230
|
+
try {
|
|
231
|
+
window.sessionStorage.setItem(key, dataToStore);
|
|
232
|
+
} catch (retryError) {
|
|
233
|
+
console.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
|
|
234
|
+
// If still fails, force clear all and try one more time
|
|
235
|
+
try {
|
|
236
|
+
forceClearAll();
|
|
237
|
+
window.sessionStorage.setItem(key, dataToStore);
|
|
238
|
+
} catch (finalError) {
|
|
239
|
+
console.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
|
|
240
|
+
// If still fails, just update the state without sessionStorage
|
|
241
|
+
setStoredValue(valueToStore);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
throw storageError;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(`Error setting sessionStorage key "${key}":`, error);
|
|
251
|
+
// Still update the state even if sessionStorage fails
|
|
252
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
253
|
+
setStoredValue(valueToStore);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Remove value from sessionStorage
|
|
258
|
+
const removeValue = () => {
|
|
259
|
+
try {
|
|
260
|
+
setStoredValue(initialValue);
|
|
261
|
+
if (typeof window !== 'undefined') {
|
|
262
|
+
try {
|
|
263
|
+
window.sessionStorage.removeItem(key);
|
|
264
|
+
} catch (removeError: any) {
|
|
265
|
+
// If removal fails due to quota, try to clear some data first
|
|
266
|
+
if (removeError.name === 'QuotaExceededError' ||
|
|
267
|
+
removeError.code === 22 ||
|
|
268
|
+
removeError.message?.includes('quota')) {
|
|
269
|
+
console.warn('sessionStorage quota exceeded during removal, clearing old data...');
|
|
270
|
+
clearOldData();
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
window.sessionStorage.removeItem(key);
|
|
274
|
+
} catch (retryError) {
|
|
275
|
+
console.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
|
|
276
|
+
// If still fails, force clear all
|
|
277
|
+
forceClearAll();
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
throw removeError;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error(`Error removing sessionStorage key "${key}":`, error);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return [storedValue, setValue, removeValue] as const;
|
|
290
|
+
}
|
package/src/hooks/useToast.ts
CHANGED
|
@@ -1,247 +1,23 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const actionTypes = {
|
|
24
|
-
ADD_TOAST: "ADD_TOAST",
|
|
25
|
-
UPDATE_TOAST: "UPDATE_TOAST",
|
|
26
|
-
DISMISS_TOAST: "DISMISS_TOAST",
|
|
27
|
-
REMOVE_TOAST: "REMOVE_TOAST",
|
|
28
|
-
} as const
|
|
29
|
-
|
|
30
|
-
let count = 0
|
|
31
|
-
|
|
32
|
-
function genId() {
|
|
33
|
-
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
|
34
|
-
return count.toString()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type ActionType = typeof actionTypes
|
|
38
|
-
|
|
39
|
-
type Action =
|
|
40
|
-
| {
|
|
41
|
-
type: ActionType["ADD_TOAST"]
|
|
42
|
-
toast: ToasterToast
|
|
43
|
-
}
|
|
44
|
-
| {
|
|
45
|
-
type: ActionType["UPDATE_TOAST"]
|
|
46
|
-
toast: Partial<ToasterToast>
|
|
47
|
-
}
|
|
48
|
-
| {
|
|
49
|
-
type: ActionType["DISMISS_TOAST"]
|
|
50
|
-
toastId?: ToasterToast["id"]
|
|
51
|
-
}
|
|
52
|
-
| {
|
|
53
|
-
type: ActionType["REMOVE_TOAST"]
|
|
54
|
-
toastId?: ToasterToast["id"]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface State {
|
|
58
|
-
toasts: ToasterToast[]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
62
|
-
const autoDismissTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
63
|
-
|
|
64
|
-
// Queue toast for removal after animation
|
|
65
|
-
const addToRemoveQueue = (toastId: string, delay: number = TOAST_DISMISS_DELAY) => {
|
|
66
|
-
if (toastTimeouts.has(toastId)) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const timeout = setTimeout(() => {
|
|
71
|
-
toastTimeouts.delete(toastId)
|
|
72
|
-
dispatch({
|
|
73
|
-
type: "REMOVE_TOAST",
|
|
74
|
-
toastId: toastId,
|
|
75
|
-
})
|
|
76
|
-
}, delay)
|
|
77
|
-
|
|
78
|
-
toastTimeouts.set(toastId, timeout)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Schedule auto-dismiss after duration
|
|
82
|
-
const scheduleAutoDismiss = (toastId: string, duration: number) => {
|
|
83
|
-
// Clear existing auto-dismiss if any
|
|
84
|
-
const existing = autoDismissTimeouts.get(toastId)
|
|
85
|
-
if (existing) {
|
|
86
|
-
clearTimeout(existing)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const timeout = setTimeout(() => {
|
|
90
|
-
autoDismissTimeouts.delete(toastId)
|
|
91
|
-
dispatch({ type: "DISMISS_TOAST", toastId })
|
|
92
|
-
}, duration)
|
|
93
|
-
|
|
94
|
-
autoDismissTimeouts.set(toastId, timeout)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Clear auto-dismiss timeout (e.g., on manual dismiss)
|
|
98
|
-
const clearAutoDismiss = (toastId: string) => {
|
|
99
|
-
const timeout = autoDismissTimeouts.get(toastId)
|
|
100
|
-
if (timeout) {
|
|
101
|
-
clearTimeout(timeout)
|
|
102
|
-
autoDismissTimeouts.delete(toastId)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export const reducer = (state: State, action: Action): State => {
|
|
107
|
-
switch (action.type) {
|
|
108
|
-
case "ADD_TOAST":
|
|
109
|
-
return {
|
|
110
|
-
...state,
|
|
111
|
-
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
case "UPDATE_TOAST":
|
|
115
|
-
return {
|
|
116
|
-
...state,
|
|
117
|
-
toasts: state.toasts.map((t) =>
|
|
118
|
-
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
|
119
|
-
),
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
case "DISMISS_TOAST": {
|
|
123
|
-
const { toastId } = action
|
|
124
|
-
|
|
125
|
-
// Clear auto-dismiss timer and queue for removal
|
|
126
|
-
if (toastId) {
|
|
127
|
-
clearAutoDismiss(toastId)
|
|
128
|
-
addToRemoveQueue(toastId)
|
|
129
|
-
} else {
|
|
130
|
-
state.toasts.forEach((toast) => {
|
|
131
|
-
clearAutoDismiss(toast.id)
|
|
132
|
-
addToRemoveQueue(toast.id)
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
...state,
|
|
138
|
-
toasts: state.toasts.map((t) =>
|
|
139
|
-
t.id === toastId || toastId === undefined
|
|
140
|
-
? {
|
|
141
|
-
...t,
|
|
142
|
-
open: false,
|
|
143
|
-
}
|
|
144
|
-
: t
|
|
145
|
-
),
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
case "REMOVE_TOAST":
|
|
149
|
-
if (action.toastId === undefined) {
|
|
150
|
-
return {
|
|
151
|
-
...state,
|
|
152
|
-
toasts: [],
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
...state,
|
|
157
|
-
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const listeners: Array<(state: State) => void> = []
|
|
163
|
-
|
|
164
|
-
let memoryState: State = { toasts: [] }
|
|
165
|
-
|
|
166
|
-
function dispatch(action: Action) {
|
|
167
|
-
memoryState = reducer(memoryState, action)
|
|
168
|
-
listeners.forEach((listener) => {
|
|
169
|
-
listener(memoryState)
|
|
170
|
-
})
|
|
3
|
+
/**
|
|
4
|
+
* Toast notifications using Sonner
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { toast } from '@djangocfg/ui-core/hooks';
|
|
8
|
+
*
|
|
9
|
+
* toast.success('Saved!');
|
|
10
|
+
* toast.error('Error', { description: 'Details here' });
|
|
11
|
+
* toast.promise(fetchData(), { loading: 'Loading...', success: 'Done!', error: 'Failed' });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { toast } from 'sonner';
|
|
15
|
+
|
|
16
|
+
// Re-export types and toast from sonner
|
|
17
|
+
export { toast, type ExternalToast as ToastOptions } from 'sonner';
|
|
18
|
+
|
|
19
|
+
// Hook for components using destructuring pattern: const { toast } = useToast()
|
|
20
|
+
type ToastFn = typeof toast;
|
|
21
|
+
export function useToast(): { toast: ToastFn; dismiss: ToastFn['dismiss'] } {
|
|
22
|
+
return { toast, dismiss: toast.dismiss };
|
|
171
23
|
}
|
|
172
|
-
|
|
173
|
-
type Toast = Omit<ToasterToast, "id">
|
|
174
|
-
|
|
175
|
-
function createToast({ duration = TOAST_DEFAULT_DURATION, ...props }: Toast) {
|
|
176
|
-
const id = genId()
|
|
177
|
-
|
|
178
|
-
const update = (props: ToasterToast) =>
|
|
179
|
-
dispatch({
|
|
180
|
-
type: "UPDATE_TOAST",
|
|
181
|
-
toast: { ...props, id },
|
|
182
|
-
})
|
|
183
|
-
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
|
184
|
-
|
|
185
|
-
dispatch({
|
|
186
|
-
type: "ADD_TOAST",
|
|
187
|
-
toast: {
|
|
188
|
-
...props,
|
|
189
|
-
id,
|
|
190
|
-
open: true,
|
|
191
|
-
onOpenChange: (open) => {
|
|
192
|
-
if (!open) dismiss()
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// Schedule auto-dismiss after duration (0 means no auto-dismiss)
|
|
198
|
-
if (duration > 0) {
|
|
199
|
-
scheduleAutoDismiss(id, duration)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
id: id,
|
|
204
|
-
dismiss,
|
|
205
|
-
update,
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Main toast function with variant helpers
|
|
210
|
-
function toast(props: Toast) {
|
|
211
|
-
return createToast(props)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Convenience methods for different variants
|
|
215
|
-
toast.success = (props: Omit<Toast, "variant">) =>
|
|
216
|
-
createToast({ ...props, variant: "success" })
|
|
217
|
-
|
|
218
|
-
toast.error = (props: Omit<Toast, "variant">) =>
|
|
219
|
-
createToast({ ...props, variant: "destructive" })
|
|
220
|
-
|
|
221
|
-
toast.warning = (props: Omit<Toast, "variant">) =>
|
|
222
|
-
createToast({ ...props, variant: "warning" })
|
|
223
|
-
|
|
224
|
-
toast.info = (props: Omit<Toast, "variant">) =>
|
|
225
|
-
createToast({ ...props, variant: "info" })
|
|
226
|
-
|
|
227
|
-
function useToast() {
|
|
228
|
-
const [state, setState] = React.useState<State>(memoryState)
|
|
229
|
-
|
|
230
|
-
React.useEffect(() => {
|
|
231
|
-
listeners.push(setState)
|
|
232
|
-
return () => {
|
|
233
|
-
const index = listeners.indexOf(setState)
|
|
234
|
-
if (index > -1) {
|
|
235
|
-
listeners.splice(index, 1)
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}, [state])
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
...state,
|
|
242
|
-
toast,
|
|
243
|
-
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export { useToast, toast }
|
package/src/lib/index.ts
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Logger
|
|
3
|
+
*
|
|
4
|
+
* Combines console logging with zustand store for Console panel.
|
|
5
|
+
* Use createMediaLogger for media tools (AudioPlayer, VideoPlayer, ImageViewer).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { createLogger, createMediaLogger, logger, log } from './logger';
|
|
9
|
+
export { useLogStore, useFilteredLogs, useLogCount, useErrorCount } from './logStore';
|
|
10
|
+
export type { LogEntry, LogLevel, LogFilter, LogStore, Logger, MediaLogger } from './types';
|