@djangocfg/ui-nextjs 2.1.80 → 2.1.82

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.80",
3
+ "version": "2.1.82",
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.80",
62
- "@djangocfg/ui-core": "^2.1.80",
61
+ "@djangocfg/api": "^2.1.82",
62
+ "@djangocfg/ui-core": "^2.1.82",
63
63
  "@types/react": "^19.1.0",
64
64
  "@types/react-dom": "^19.1.0",
65
65
  "consola": "^3.4.2",
@@ -110,7 +110,7 @@
110
110
  "wavesurfer.js": "^7.12.1"
111
111
  },
112
112
  "devDependencies": {
113
- "@djangocfg/typescript-config": "^2.1.80",
113
+ "@djangocfg/typescript-config": "^2.1.82",
114
114
  "@types/node": "^24.7.2",
115
115
  "eslint": "^9.37.0",
116
116
  "tailwindcss-animate": "1.0.7",
@@ -19,7 +19,7 @@ import {
19
19
  import { useWavesurfer } from '@wavesurfer/react';
20
20
  import type { AudioContextState, AudioSource, WaveformOptions } from '../types';
21
21
  import { useSharedWebAudio, useAudioAnalysis, useAudioSource } from '../hooks';
22
- import { useAudioCache } from '../../../stores/mediaCache';
22
+ import { useMediaCacheStore } from '../../../stores/mediaCache';
23
23
  import { audioDebug } from '../utils/debug';
24
24
 
25
25
  // =============================================================================
@@ -57,8 +57,9 @@ export function AudioProvider({
57
57
  containerRef,
58
58
  children,
59
59
  }: AudioProviderProps) {
60
- // Cache for playback position persistence
61
- const { saveAudioPosition, getAudioPosition } = useAudioCache();
60
+ // Get stable function references from store
61
+ const saveAudioPosition = useMediaCacheStore.getState().saveAudioPosition;
62
+ const getAudioPosition = useMediaCacheStore.getState().getAudioPosition;
62
63
  const lastSavedTimeRef = useRef<number>(0);
63
64
 
64
65
  // Handle prefetch if enabled (for streaming URLs)
@@ -149,7 +150,8 @@ export function AudioProvider({
149
150
  }
150
151
  }
151
152
  }
152
- }, [isReady, wavesurfer, source.uri, getAudioPosition]);
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
154
+ }, [isReady, wavesurfer, source.uri]);
153
155
 
154
156
  // Save playback position periodically and on pause
155
157
  useEffect(() => {
@@ -169,7 +171,8 @@ export function AudioProvider({
169
171
  saveAudioPosition(source.uri, currentTime);
170
172
  lastSavedTimeRef.current = currentTime;
171
173
  }
172
- }, [isPlaying, currentTime, source.uri, saveAudioPosition]);
174
+ // eslint-disable-next-line react-hooks/exhaustive-deps
175
+ }, [isPlaying, currentTime, source.uri]);
173
176
 
174
177
  // Derived state
175
178
  const duration = wavesurfer?.getDuration() ?? 0;
@@ -58,15 +58,30 @@ export function useAudioSource(source: AudioSource): UseAudioSourceResult {
58
58
 
59
59
  const response = await fetch(source.uri, {
60
60
  signal: abortController.signal,
61
+ headers: {
62
+ // Request full file - some servers require Range header
63
+ 'Range': 'bytes=0-',
64
+ },
61
65
  });
62
66
 
67
+ // Accept 200 OK or 206 Partial Content (response.ok covers both)
63
68
  if (!response.ok) {
64
69
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
65
70
  }
66
71
 
67
72
  // Get content length for progress tracking
68
- const contentLength = response.headers.get('Content-Length');
69
- const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
73
+ // For Range requests, use Content-Range header (format: "bytes 0-1234/5678")
74
+ let totalBytes = 0;
75
+ const contentRange = response.headers.get('Content-Range');
76
+ if (contentRange) {
77
+ const match = contentRange.match(/\/(\d+)$/);
78
+ if (match) {
79
+ totalBytes = parseInt(match[1], 10);
80
+ }
81
+ } else {
82
+ const contentLength = response.headers.get('Content-Length');
83
+ totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
84
+ }
70
85
 
71
86
  if (!response.body) {
72
87
  // Fallback for browsers without ReadableStream
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { useState, useEffect, useRef } from 'react';
8
- import { useImageCache, generateContentKey } from '../../../stores/mediaCache';
8
+ import { useMediaCacheStore, generateContentKey } from '../../../stores/mediaCache';
9
9
  import { createLQIP, MAX_IMAGE_SIZE, WARNING_IMAGE_SIZE, PROGRESSIVE_LOADING_THRESHOLD, imageDebug } from '../utils';
10
10
 
11
11
  // =============================================================================
@@ -50,7 +50,9 @@ export interface UseImageLoadingReturn {
50
50
  export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadingReturn {
51
51
  const { content, mimeType, src: directSrc } = options;
52
52
 
53
- const { getOrCreateBlobUrl, releaseBlobUrl } = useImageCache();
53
+ // Get stable function references from store (not from hook to avoid re-renders)
54
+ const getOrCreateBlobUrl = useMediaCacheStore.getState().getOrCreateBlobUrl;
55
+ const releaseBlobUrl = useMediaCacheStore.getState().releaseBlobUrl;
54
56
 
55
57
  const [src, setSrc] = useState<string | null>(null);
56
58
  const [lqip, setLqip] = useState<string | null>(null);
@@ -58,6 +60,7 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
58
60
  const [error, setError] = useState<string | null>(null);
59
61
 
60
62
  const contentKeyRef = useRef<string | null>(null);
63
+ const isMountedRef = useRef(true);
61
64
 
62
65
  // Calculate size and flags
63
66
  const size = content ? (typeof content === 'string' ? content.length : content.byteLength) : 0;
@@ -65,6 +68,19 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
65
68
  const hasContent = directSrc ? true : size > 0;
66
69
  const useProgressiveLoading = directSrc ? false : size > PROGRESSIVE_LOADING_THRESHOLD;
67
70
 
71
+ // Track unmount for cleanup
72
+ useEffect(() => {
73
+ isMountedRef.current = true;
74
+ return () => {
75
+ isMountedRef.current = false;
76
+ // Release blob URL only on actual unmount
77
+ if (contentKeyRef.current) {
78
+ useMediaCacheStore.getState().releaseBlobUrl(contentKeyRef.current);
79
+ contentKeyRef.current = null;
80
+ }
81
+ };
82
+ }, []);
83
+
68
84
  // Create blob URL with caching and size validation
69
85
  useEffect(() => {
70
86
  // Reset error state
@@ -112,6 +128,12 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
112
128
  const encoder = new TextEncoder();
113
129
  const buffer = encoder.encode(content).buffer;
114
130
  const contentKey = generateContentKey(buffer);
131
+
132
+ // Release previous blob URL if content changed
133
+ if (contentKeyRef.current && contentKeyRef.current !== contentKey) {
134
+ releaseBlobUrl(contentKeyRef.current);
135
+ }
136
+
115
137
  contentKeyRef.current = contentKey;
116
138
  const url = getOrCreateBlobUrl(contentKey, buffer, mimeType || 'image/png');
117
139
  imageDebug.load(url, 'blob');
@@ -122,19 +144,21 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
122
144
 
123
145
  // Handle ArrayBuffer with cached blob URL
124
146
  const contentKey = generateContentKey(content);
147
+
148
+ // Release previous blob URL if content changed
149
+ if (contentKeyRef.current && contentKeyRef.current !== contentKey) {
150
+ releaseBlobUrl(contentKeyRef.current);
151
+ }
152
+
125
153
  contentKeyRef.current = contentKey;
126
154
  const url = getOrCreateBlobUrl(contentKey, content, mimeType || 'image/png');
127
155
  imageDebug.load(url, 'blob');
128
156
  imageDebug.state('loaded', { size, mimeType, contentKey });
129
157
  setSrc(url);
130
158
 
131
- return () => {
132
- if (contentKeyRef.current) {
133
- releaseBlobUrl(contentKeyRef.current);
134
- contentKeyRef.current = null;
135
- }
136
- };
137
- }, [content, mimeType, hasContent, size, directSrc, getOrCreateBlobUrl, releaseBlobUrl]);
159
+ // No cleanup here - cleanup happens in unmount effect above
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
161
+ }, [content, mimeType, hasContent, size, directSrc]);
138
162
 
139
163
  // Create LQIP for progressive loading
140
164
  useEffect(() => {
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { useRef, useEffect, useCallback } from 'react';
10
- import { useVideoCache } from '../../../stores/mediaCache';
10
+ import { useMediaCacheStore } from '../../../stores/mediaCache';
11
11
 
12
12
  // =============================================================================
13
13
  // TYPES
@@ -54,7 +54,10 @@ export function useVideoPositionCache(
54
54
  ): UseVideoPositionCacheReturn {
55
55
  const { cacheKey, currentTime, duration, isPlaying, isReady, onSeek } = options;
56
56
 
57
- const { saveVideoPosition, getVideoPosition } = useVideoCache();
57
+ // Get stable function references from store
58
+ const saveVideoPosition = useMediaCacheStore.getState().saveVideoPosition;
59
+ const getVideoPosition = useMediaCacheStore.getState().getVideoPosition;
60
+
58
61
  const lastSavedTimeRef = useRef<number>(0);
59
62
  const hasRestoredRef = useRef<boolean>(false);
60
63
 
@@ -70,7 +73,8 @@ export function useVideoPositionCache(
70
73
  }
71
74
  }
72
75
  hasRestoredRef.current = true;
73
- }, [isReady, cacheKey, duration, getVideoPosition, onSeek]);
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ }, [isReady, cacheKey, duration, onSeek]);
74
78
 
75
79
  // Reset restored flag when cache key changes
76
80
  useEffect(() => {
@@ -87,20 +91,23 @@ export function useVideoPositionCache(
87
91
  saveVideoPosition(cacheKey, currentTime);
88
92
  lastSavedTimeRef.current = currentTime;
89
93
  }
90
- }, [cacheKey, isPlaying, currentTime, saveVideoPosition]);
94
+ // eslint-disable-next-line react-hooks/exhaustive-deps
95
+ }, [cacheKey, isPlaying, currentTime]);
91
96
 
92
97
  const savePosition = useCallback(() => {
93
98
  if (cacheKey && currentTime > 0) {
94
99
  saveVideoPosition(cacheKey, currentTime);
95
100
  lastSavedTimeRef.current = currentTime;
96
101
  }
97
- }, [cacheKey, currentTime, saveVideoPosition]);
102
+ // eslint-disable-next-line react-hooks/exhaustive-deps
103
+ }, [cacheKey, currentTime]);
98
104
 
99
105
  const clearPosition = useCallback(() => {
100
106
  if (cacheKey) {
101
107
  saveVideoPosition(cacheKey, 0);
102
108
  }
103
- }, [cacheKey, saveVideoPosition]);
109
+ // eslint-disable-next-line react-hooks/exhaustive-deps
110
+ }, [cacheKey]);
104
111
 
105
112
  return {
106
113
  savePosition,
@@ -14,7 +14,7 @@ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef,
14
14
 
15
15
  import { cn } from '@djangocfg/ui-core/lib';
16
16
  import { Preloader, AspectRatio } from '@djangocfg/ui-core';
17
- import { useVideoCache, generateContentKey } from '../../../stores/mediaCache';
17
+ import { useMediaCacheStore, generateContentKey } from '../../../stores/mediaCache';
18
18
 
19
19
  import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
20
20
  import { videoDebug } from '../utils/debug';
@@ -77,14 +77,12 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
77
77
  const contentKeyRef = useRef<string | null>(null);
78
78
  const lastSavedTimeRef = useRef<number>(0);
79
79
 
80
- // Cache hooks
81
- const {
82
- getOrCreateBlobUrl,
83
- releaseBlobUrl,
84
- getOrCreateStreamUrl,
85
- saveVideoPosition,
86
- getVideoPosition,
87
- } = useVideoCache();
80
+ // Get stable function references from store (not from hook to avoid re-renders)
81
+ const getOrCreateBlobUrl = useMediaCacheStore.getState().getOrCreateBlobUrl;
82
+ const releaseBlobUrl = useMediaCacheStore.getState().releaseBlobUrl;
83
+ const getOrCreateStreamUrl = useMediaCacheStore.getState().getOrCreateStreamUrl;
84
+ const saveVideoPosition = useMediaCacheStore.getState().saveVideoPosition;
85
+ const getVideoPosition = useMediaCacheStore.getState().getVideoPosition;
88
86
 
89
87
  // Retry function for error fallback
90
88
  // Regenerates URL for stream sources to get fresh token/session
@@ -143,12 +141,31 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
143
141
  []
144
142
  );
145
143
 
144
+ // Track unmount for cleanup
145
+ const isMountedRef = useRef(true);
146
+ useEffect(() => {
147
+ isMountedRef.current = true;
148
+ return () => {
149
+ isMountedRef.current = false;
150
+ // Release blob URL only on actual unmount
151
+ if (contentKeyRef.current) {
152
+ useMediaCacheStore.getState().releaseBlobUrl(contentKeyRef.current);
153
+ contentKeyRef.current = null;
154
+ }
155
+ };
156
+ }, []);
157
+
146
158
  // Create video URL based on source type with caching
147
159
  useEffect(() => {
148
- // Cleanup previous blob URL from cache
160
+ // Release previous blob URL if source changed
149
161
  if (contentKeyRef.current) {
150
- releaseBlobUrl(contentKeyRef.current);
151
- contentKeyRef.current = null;
162
+ const newKey = source.type === 'blob'
163
+ ? generateContentKey((source as BlobSource).data)
164
+ : null;
165
+ if (newKey !== contentKeyRef.current) {
166
+ releaseBlobUrl(contentKeyRef.current);
167
+ contentKeyRef.current = null;
168
+ }
152
169
  }
153
170
 
154
171
  setHasError(false);
@@ -198,13 +215,9 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
198
215
  setErrorMessage('Invalid video source');
199
216
  }
200
217
 
201
- return () => {
202
- if (contentKeyRef.current) {
203
- releaseBlobUrl(contentKeyRef.current);
204
- contentKeyRef.current = null;
205
- }
206
- };
207
- }, [source, getOrCreateBlobUrl, getOrCreateStreamUrl, releaseBlobUrl]);
218
+ // No cleanup here - cleanup happens in unmount effect above
219
+ // eslint-disable-next-line react-hooks/exhaustive-deps
220
+ }, [source]);
208
221
 
209
222
  // Get source key for position caching
210
223
  const getSourceKey = useCallback(() => {
@@ -244,7 +257,8 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
244
257
  }
245
258
 
246
259
  onCanPlay?.();
247
- }, [getSourceKey, getVideoPosition, onCanPlay]);
260
+ // eslint-disable-next-line react-hooks/exhaustive-deps
261
+ }, [getSourceKey, onCanPlay]);
248
262
 
249
263
  // Save playback position periodically
250
264
  const handleTimeUpdate = useCallback(() => {
@@ -262,7 +276,8 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
262
276
  }
263
277
 
264
278
  onTimeUpdate?.(video.currentTime, video.duration);
265
- }, [getSourceKey, saveVideoPosition, onTimeUpdate]);
279
+ // eslint-disable-next-line react-hooks/exhaustive-deps
280
+ }, [getSourceKey, onTimeUpdate]);
266
281
 
267
282
  // Save position on pause
268
283
  const handlePause = useCallback(() => {
@@ -273,7 +288,8 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
273
288
  lastSavedTimeRef.current = video.currentTime;
274
289
  }
275
290
  onPause?.();
276
- }, [getSourceKey, saveVideoPosition, onPause]);
291
+ // eslint-disable-next-line react-hooks/exhaustive-deps
292
+ }, [getSourceKey, onPause]);
277
293
 
278
294
  // Handle buffer progress
279
295
  const handleProgress = useCallback(() => {