@djangocfg/ui-core 2.1.90 → 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.
@@ -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
+ }
@@ -1,247 +1,23 @@
1
1
  "use client"
2
2
 
3
- // Inspired by react-hot-toast library
4
- import * as React from 'react';
5
-
6
- import type {
7
- ToastActionElement,
8
- ToastProps,
9
- } from "../components/toast"
10
-
11
- const TOAST_LIMIT = 1
12
- const TOAST_DEFAULT_DURATION = 5000 // 5 seconds default auto-dismiss
13
- const TOAST_DISMISS_DELAY = 300 // Animation delay before removing from DOM
14
-
15
- type ToasterToast = ToastProps & {
16
- id: string
17
- title?: React.ReactNode
18
- description?: React.ReactNode
19
- action?: ToastActionElement
20
- duration?: number // Auto-dismiss duration in ms
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
@@ -1,2 +1,3 @@
1
1
  export * from "./utils";
2
2
  export * from "./og-image";
3
+ export * from "./logger";
@@ -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';