@djangocfg/ui-nextjs 2.1.59 → 2.1.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.59",
3
+ "version": "2.1.62",
4
4
  "description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -58,8 +58,8 @@
58
58
  "check": "tsc --noEmit"
59
59
  },
60
60
  "peerDependencies": {
61
- "@djangocfg/api": "^2.1.59",
62
- "@djangocfg/ui-core": "^2.1.59",
61
+ "@djangocfg/api": "^2.1.62",
62
+ "@djangocfg/ui-core": "^2.1.62",
63
63
  "@types/react": "^19.1.0",
64
64
  "@types/react-dom": "^19.1.0",
65
65
  "consola": "^3.4.2",
@@ -106,7 +106,7 @@
106
106
  "vidstack": "next"
107
107
  },
108
108
  "devDependencies": {
109
- "@djangocfg/typescript-config": "^2.1.59",
109
+ "@djangocfg/typescript-config": "^2.1.62",
110
110
  "@types/node": "^24.7.2",
111
111
  "eslint": "^9.37.0",
112
112
  "tailwindcss-animate": "1.0.7",
@@ -9,7 +9,9 @@ export * from '@djangocfg/ui-core/hooks';
9
9
 
10
10
  // Storage hooks (browser localStorage/sessionStorage)
11
11
  export { useLocalStorage } from './useLocalStorage';
12
+ export type { UseLocalStorageOptions } from './useLocalStorage';
12
13
  export { useSessionStorage } from './useSessionStorage';
14
+ export type { UseSessionStorageOptions } from './useSessionStorage';
13
15
 
14
16
  // Theme hook (standalone, no provider required)
15
17
  export { useResolvedTheme } from './useResolvedTheme';
@@ -3,7 +3,55 @@
3
3
  import { useEffect, useRef, useState } from 'react';
4
4
 
5
5
  /**
6
- * Simple localStorage hook with better error handling
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 useLocalStorage hook
19
+ */
20
+ export interface UseLocalStorageOptions {
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 localStorage hook with better error handling and optional TTL support
7
55
  *
8
56
  * IMPORTANT: To prevent hydration mismatch, this hook:
9
57
  * - Always returns initialValue on first render (same as SSR)
@@ -11,9 +59,25 @@ import { useEffect, useRef, useState } from 'react';
11
59
  *
12
60
  * @param key - Storage key
13
61
  * @param initialValue - Default value if key doesn't exist
62
+ * @param options - Optional configuration (ttl for auto-expiration)
14
63
  * @returns [value, setValue, removeValue] - Current value, setter function, and remove function
64
+ *
65
+ * @example
66
+ * // Without TTL (backwards compatible)
67
+ * const [value, setValue] = useLocalStorage('key', 'default');
68
+ *
69
+ * @example
70
+ * // With TTL (24 hours)
71
+ * const [value, setValue] = useLocalStorage('key', 'default', {
72
+ * ttl: 24 * 60 * 60 * 1000
73
+ * });
15
74
  */
16
- export function useLocalStorage<T>(key: string, initialValue: T) {
75
+ export function useLocalStorage<T>(
76
+ key: string,
77
+ initialValue: T,
78
+ options?: UseLocalStorageOptions
79
+ ) {
80
+ const ttl = options?.ttl;
17
81
  // Always start with initialValue to match SSR
18
82
  const [storedValue, setStoredValue] = useState<T>(initialValue);
19
83
  const [isHydrated, setIsHydrated] = useState(false);
@@ -29,7 +93,23 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
29
93
  if (item !== null) {
30
94
  // Try to parse as JSON first, fallback to string
31
95
  try {
32
- setStoredValue(JSON.parse(item));
96
+ const parsed = JSON.parse(item);
97
+
98
+ // Check if new format with _meta
99
+ if (isWrappedFormat<T>(parsed)) {
100
+ // Check TTL expiration
101
+ if (isExpired(parsed)) {
102
+ // Expired! Clean up and use initial value
103
+ window.localStorage.removeItem(key);
104
+ // Keep initialValue (already set)
105
+ } else {
106
+ // Not expired, extract value
107
+ setStoredValue(parsed._value);
108
+ }
109
+ } else {
110
+ // Old format (backwards compatible)
111
+ setStoredValue(parsed as T);
112
+ }
33
113
  } catch {
34
114
  // If JSON.parse fails, return as string
35
115
  setStoredValue(item as T);
@@ -101,18 +181,37 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
101
181
  }
102
182
  };
103
183
 
184
+ // Prepare data for storage (with or without TTL wrapper)
185
+ const prepareForStorage = (value: T): string => {
186
+ if (ttl) {
187
+ // Wrap with _meta for TTL support
188
+ const wrapped: StorageWrapper<T> = {
189
+ _meta: {
190
+ createdAt: Date.now(),
191
+ ttl,
192
+ },
193
+ _value: value,
194
+ };
195
+ return JSON.stringify(wrapped);
196
+ }
197
+ // Old format (no wrapper) - for strings, store directly
198
+ if (typeof value === 'string') {
199
+ return value;
200
+ }
201
+ return JSON.stringify(value);
202
+ };
203
+
104
204
  // Update localStorage when value changes
105
205
  const setValue = (value: T | ((val: T) => T)) => {
106
206
  try {
107
207
  const valueToStore = value instanceof Function ? value(storedValue) : value;
108
-
208
+
109
209
  // Check data size before attempting to save
110
210
  if (!checkDataSize(valueToStore)) {
111
211
  console.warn(`Data size too large for key "${key}", removing key`);
112
212
  // Remove the key if data is too large
113
213
  try {
114
214
  window.localStorage.removeItem(key);
115
- window.localStorage.removeItem(`${key}_timestamp`);
116
215
  } catch {
117
216
  // Ignore errors when removing
118
217
  }
@@ -120,45 +219,32 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
120
219
  setStoredValue(valueToStore);
121
220
  return;
122
221
  }
123
-
222
+
124
223
  setStoredValue(valueToStore);
125
224
 
126
225
  if (typeof window !== 'undefined') {
226
+ const dataToStore = prepareForStorage(valueToStore);
227
+
127
228
  // Try to set the value
128
229
  try {
129
- // For strings, store directly without JSON.stringify
130
- if (typeof valueToStore === 'string') {
131
- window.localStorage.setItem(key, valueToStore);
132
- } else {
133
- window.localStorage.setItem(key, JSON.stringify(valueToStore));
134
- }
230
+ window.localStorage.setItem(key, dataToStore);
135
231
  } catch (storageError: any) {
136
232
  // If quota exceeded, clear old data and try again
137
- if (storageError.name === 'QuotaExceededError' ||
138
- storageError.code === 22 ||
233
+ if (storageError.name === 'QuotaExceededError' ||
234
+ storageError.code === 22 ||
139
235
  storageError.message?.includes('quota')) {
140
236
  console.warn('localStorage quota exceeded, clearing old data...');
141
237
  clearOldData();
142
-
238
+
143
239
  // Try again after clearing
144
240
  try {
145
- // For strings, store directly without JSON.stringify
146
- if (typeof valueToStore === 'string') {
147
- window.localStorage.setItem(key, valueToStore);
148
- } else {
149
- window.localStorage.setItem(key, JSON.stringify(valueToStore));
150
- }
241
+ window.localStorage.setItem(key, dataToStore);
151
242
  } catch (retryError) {
152
243
  console.error(`Failed to set localStorage key "${key}" after clearing old data:`, retryError);
153
244
  // If still fails, force clear all and try one more time
154
245
  try {
155
246
  forceClearAll();
156
- // For strings, store directly without JSON.stringify
157
- if (typeof valueToStore === 'string') {
158
- window.localStorage.setItem(key, valueToStore);
159
- } else {
160
- window.localStorage.setItem(key, JSON.stringify(valueToStore));
161
- }
247
+ window.localStorage.setItem(key, dataToStore);
162
248
  } catch (finalError) {
163
249
  console.error(`Failed to set localStorage key "${key}" after force clearing:`, finalError);
164
250
  // If still fails, just update the state without localStorage
@@ -3,12 +3,78 @@
3
3
  import { useState } from 'react';
4
4
 
5
5
  /**
6
- * Simple sessionStorage hook with better error handling
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
+ *
7
56
  * @param key - Storage key
8
57
  * @param initialValue - Default value if key doesn't exist
58
+ * @param options - Optional configuration (ttl for auto-expiration)
9
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
+ * });
10
70
  */
11
- export function useSessionStorage<T>(key: string, initialValue: T) {
71
+ export function useSessionStorage<T>(
72
+ key: string,
73
+ initialValue: T,
74
+ options?: UseSessionStorageOptions
75
+ ) {
76
+ const ttl = options?.ttl;
77
+
12
78
  // Get initial value from sessionStorage or use provided initialValue
13
79
  const [storedValue, setStoredValue] = useState<T>(() => {
14
80
  if (typeof window === 'undefined') {
@@ -17,7 +83,29 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
17
83
 
18
84
  try {
19
85
  const item = window.sessionStorage.getItem(key);
20
- return item ? JSON.parse(item) : initialValue;
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
+ }
21
109
  } catch (error) {
22
110
  console.error(`Error reading sessionStorage key "${key}":`, error);
23
111
  return initialValue;
@@ -30,13 +118,13 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
30
118
  const jsonString = JSON.stringify(data);
31
119
  const sizeInBytes = new Blob([jsonString]).size;
32
120
  const sizeInKB = sizeInBytes / 1024;
33
-
121
+
34
122
  // Limit to 1MB per item
35
123
  if (sizeInKB > 1024) {
36
124
  console.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
37
125
  return false;
38
126
  }
39
-
127
+
40
128
  return true;
41
129
  } catch (error) {
42
130
  console.error(`Error checking data size for key "${key}":`, error);
@@ -47,16 +135,15 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
47
135
  // Clear old data when sessionStorage is full
48
136
  const clearOldData = () => {
49
137
  try {
50
- const keys = Object.keys(sessionStorage).filter(key => key && typeof key === 'string');
138
+ const keys = Object.keys(sessionStorage).filter(k => k && typeof k === 'string');
51
139
  // Remove oldest items if we have more than 50 items
52
140
  if (keys.length > 50) {
53
141
  const itemsToRemove = Math.ceil(keys.length * 0.2);
54
142
  for (let i = 0; i < itemsToRemove; i++) {
55
143
  try {
56
- const key = keys[i];
57
- if (key) {
58
- sessionStorage.removeItem(key);
59
- sessionStorage.removeItem(`${key}_timestamp`);
144
+ const k = keys[i];
145
+ if (k) {
146
+ sessionStorage.removeItem(k);
60
147
  }
61
148
  } catch {
62
149
  // Ignore errors when removing items
@@ -72,9 +159,9 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
72
159
  const forceClearAll = () => {
73
160
  try {
74
161
  const keys = Object.keys(sessionStorage);
75
- for (const key of keys) {
162
+ for (const k of keys) {
76
163
  try {
77
- sessionStorage.removeItem(key);
164
+ sessionStorage.removeItem(k);
78
165
  } catch {
79
166
  // Ignore errors when removing items
80
167
  }
@@ -84,18 +171,37 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
84
171
  }
85
172
  };
86
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
+
87
194
  // Update sessionStorage when value changes
88
195
  const setValue = (value: T | ((val: T) => T)) => {
89
196
  try {
90
197
  const valueToStore = value instanceof Function ? value(storedValue) : value;
91
-
198
+
92
199
  // Check data size before attempting to save
93
200
  if (!checkDataSize(valueToStore)) {
94
201
  console.warn(`Data size too large for key "${key}", removing key`);
95
202
  // Remove the key if data is too large
96
203
  try {
97
204
  window.sessionStorage.removeItem(key);
98
- window.sessionStorage.removeItem(`${key}_timestamp`);
99
205
  } catch {
100
206
  // Ignore errors when removing
101
207
  }
@@ -103,34 +209,32 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
103
209
  setStoredValue(valueToStore);
104
210
  return;
105
211
  }
106
-
212
+
107
213
  setStoredValue(valueToStore);
108
214
 
109
215
  if (typeof window !== 'undefined') {
216
+ const dataToStore = prepareForStorage(valueToStore);
217
+
110
218
  // Try to set the value
111
219
  try {
112
- window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
113
- // Add timestamp for cleanup
114
- window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
220
+ window.sessionStorage.setItem(key, dataToStore);
115
221
  } catch (storageError: any) {
116
222
  // If quota exceeded, clear old data and try again
117
- if (storageError.name === 'QuotaExceededError' ||
118
- storageError.code === 22 ||
223
+ if (storageError.name === 'QuotaExceededError' ||
224
+ storageError.code === 22 ||
119
225
  storageError.message?.includes('quota')) {
120
226
  console.warn('sessionStorage quota exceeded, clearing old data...');
121
227
  clearOldData();
122
-
228
+
123
229
  // Try again after clearing
124
230
  try {
125
- window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
126
- window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
231
+ window.sessionStorage.setItem(key, dataToStore);
127
232
  } catch (retryError) {
128
233
  console.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
129
234
  // If still fails, force clear all and try one more time
130
235
  try {
131
236
  forceClearAll();
132
- window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
133
- window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
237
+ window.sessionStorage.setItem(key, dataToStore);
134
238
  } catch (finalError) {
135
239
  console.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
136
240
  // If still fails, just update the state without sessionStorage
@@ -157,18 +261,16 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
157
261
  if (typeof window !== 'undefined') {
158
262
  try {
159
263
  window.sessionStorage.removeItem(key);
160
- window.sessionStorage.removeItem(`${key}_timestamp`);
161
264
  } catch (removeError: any) {
162
265
  // If removal fails due to quota, try to clear some data first
163
- if (removeError.name === 'QuotaExceededError' ||
164
- removeError.code === 22 ||
266
+ if (removeError.name === 'QuotaExceededError' ||
267
+ removeError.code === 22 ||
165
268
  removeError.message?.includes('quota')) {
166
269
  console.warn('sessionStorage quota exceeded during removal, clearing old data...');
167
270
  clearOldData();
168
-
271
+
169
272
  try {
170
273
  window.sessionStorage.removeItem(key);
171
- window.sessionStorage.removeItem(`${key}_timestamp`);
172
274
  } catch (retryError) {
173
275
  console.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
174
276
  // If still fails, force clear all