@djangocfg/ui-nextjs 2.1.78 → 2.1.79

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 CHANGED
@@ -57,11 +57,11 @@ All components from `@djangocfg/ui-core` are re-exported.
57
57
  import { Hero } from '@djangocfg/ui-nextjs/blocks';
58
58
  ```
59
59
 
60
- ### Tools (4)
61
- `JsonTree` `PrettyCode` `Mermaid` `LottiePlayer`
60
+ ### Tools (7)
61
+ `JsonTree` `PrettyCode` `Mermaid` `LottiePlayer` `AudioPlayer` `VideoPlayer` `ImageViewer`
62
62
 
63
63
  ```tsx
64
- import { PrettyCode } from '@djangocfg/ui-nextjs/tools';
64
+ import { PrettyCode, AudioPlayer, VideoPlayer } from '@djangocfg/ui-nextjs/tools';
65
65
  ```
66
66
 
67
67
  ## Hooks
@@ -108,6 +108,25 @@ function Example() {
108
108
  import '@djangocfg/ui-nextjs/styles/globals';
109
109
  ```
110
110
 
111
+ ## Logger
112
+
113
+ Universal logger with consola + zustand for Console panel integration.
114
+
115
+ ```tsx
116
+ import { createLogger, createMediaLogger } from '@djangocfg/ui-nextjs/lib';
117
+
118
+ // Basic logger
119
+ const log = createLogger('MyComponent');
120
+ log.info('User logged in', { userId: 123 });
121
+ log.error('Failed to load', { error });
122
+
123
+ // Media logger (with seek/buffer helpers)
124
+ const mediaLog = createMediaLogger('AudioPlayer');
125
+ mediaLog.load(src, 'stream');
126
+ mediaLog.seek(from, to, duration);
127
+ mediaLog.buffer(buffered, duration);
128
+ ```
129
+
111
130
  ## Exports
112
131
 
113
132
  | Path | Content |
@@ -116,7 +135,8 @@ import '@djangocfg/ui-nextjs/styles/globals';
116
135
  | `@djangocfg/ui-nextjs/components` | Components only |
117
136
  | `@djangocfg/ui-nextjs/hooks` | Hooks only |
118
137
  | `@djangocfg/ui-nextjs/blocks` | Landing page blocks |
119
- | `@djangocfg/ui-nextjs/tools` | JsonTree, Mermaid, etc. |
138
+ | `@djangocfg/ui-nextjs/tools` | JsonTree, Mermaid, Media players |
139
+ | `@djangocfg/ui-nextjs/lib` | Logger, utilities |
120
140
  | `@djangocfg/ui-nextjs/theme` | ThemeProvider, ThemeToggle |
121
141
  | `@djangocfg/ui-nextjs/styles` | CSS |
122
142
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.78",
3
+ "version": "2.1.79",
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.78",
62
- "@djangocfg/ui-core": "^2.1.78",
61
+ "@djangocfg/api": "^2.1.79",
62
+ "@djangocfg/ui-core": "^2.1.79",
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.78",
113
+ "@djangocfg/typescript-config": "^2.1.79",
114
114
  "@types/node": "^24.7.2",
115
115
  "eslint": "^9.37.0",
116
116
  "tailwindcss-animate": "1.0.7",
@@ -46,6 +46,7 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
46
46
  | Prop | Type | Default | Description |
47
47
  |------|------|---------|-------------|
48
48
  | `src` | `string` | required | Audio URL |
49
+ | `prefetch` | `boolean` | `true` | Pre-fetch audio as blob (required for streaming URLs to enable seek) |
49
50
  | `title` | `string` | - | Track title |
50
51
  | `artist` | `string` | - | Artist name |
51
52
  | `coverArt` | `string \| ReactNode` | - | Cover image URL or custom element |
@@ -54,6 +55,7 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
54
55
  | `showEqualizer` | `boolean` | `false` | Show equalizer bars |
55
56
  | `showTimer` | `boolean` | `true` | Show time display |
56
57
  | `showVolume` | `boolean` | `true` | Show volume control |
58
+ | `showLoop` | `boolean` | `true` | Show loop/repeat button |
57
59
  | `reactiveCover` | `boolean` | `true` | Enable reactive effects |
58
60
  | `variant` | `VisualizationVariant` | - | Effect variant |
59
61
  | `intensity` | `EffectIntensity` | - | Effect intensity |
@@ -61,6 +63,8 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
61
63
  | `autoPlay` | `boolean` | `false` | Auto-play on load |
62
64
  | `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
63
65
 
66
+ > **Note:** The `prefetch` option is enabled by default. This fetches the entire audio file as a blob before loading into WaveSurfer, which is required for seeking to work correctly with streaming URLs. For very large files (> 50MB), consider using `prefetch={false}` and the Progressive player mode instead.
67
+
64
68
  ---
65
69
 
66
70
  ## Advanced Usage
@@ -99,7 +103,10 @@ Context provider for audio state. Wraps all audio components.
99
103
 
100
104
  ```tsx
101
105
  <AudioProvider
102
- source={{ uri: 'https://example.com/audio.mp3' }}
106
+ source={{
107
+ uri: 'https://example.com/audio.mp3',
108
+ prefetch: true // Fetch as blob for seek support (default: false)
109
+ }}
103
110
  containerRef={containerRef}
104
111
  autoPlay={false}
105
112
  waveformOptions={{
@@ -115,6 +122,13 @@ Context provider for audio state. Wraps all audio components.
115
122
  </AudioProvider>
116
123
  ```
117
124
 
125
+ #### AudioSource Options
126
+
127
+ | Prop | Type | Default | Description |
128
+ |------|------|---------|-------------|
129
+ | `uri` | `string` | required | Audio URL |
130
+ | `prefetch` | `boolean` | `false` | Pre-fetch as blob (enables seek for streaming URLs) |
131
+
118
132
  ### AudioPlayer
119
133
 
120
134
  Main player component with waveform and controls.
@@ -294,6 +308,7 @@ AudioPlayer/
294
308
  │ └── effects.ts # Visualization effect types
295
309
  ├── hooks/
296
310
  │ ├── index.ts
311
+ │ ├── useAudioSource.ts # Audio source loading with prefetch
297
312
  │ ├── useAudioHotkeys.ts # Keyboard shortcuts
298
313
  │ ├── useVisualization.tsx # Visualization settings
299
314
  │ ├── useAudioAnalysis.ts # Web Audio frequency analysis
@@ -53,6 +53,13 @@ export interface SimpleAudioPlayerProps {
53
53
  /** Audio source URL */
54
54
  src: string;
55
55
 
56
+ /**
57
+ * Pre-fetch audio as blob before loading into WaveSurfer.
58
+ * Required for streaming URLs because WaveSurfer needs complete file for seek to work.
59
+ * @default true
60
+ */
61
+ prefetch?: boolean;
62
+
56
63
  /** Track title */
57
64
  title?: string;
58
65
 
@@ -132,6 +139,7 @@ export function SimpleAudioPlayer(props: SimpleAudioPlayerProps) {
132
139
 
133
140
  function SimpleAudioPlayerContent({
134
141
  src,
142
+ prefetch = true,
135
143
  title,
136
144
  artist,
137
145
  coverArt,
@@ -190,7 +198,7 @@ function SimpleAudioPlayerContent({
190
198
 
191
199
  return (
192
200
  <AudioProvider
193
- source={{ uri: src }}
201
+ source={{ uri: src, prefetch }}
194
202
  containerRef={containerRef}
195
203
  autoPlay={autoPlay}
196
204
  waveformOptions={waveformOptions}
@@ -18,7 +18,7 @@ import {
18
18
  } from 'react';
19
19
  import { useWavesurfer } from '@wavesurfer/react';
20
20
  import type { AudioContextState, AudioSource, WaveformOptions } from '../types';
21
- import { useSharedWebAudio, useAudioAnalysis } from '../hooks';
21
+ import { useSharedWebAudio, useAudioAnalysis, useAudioSource } from '../hooks';
22
22
  import { useAudioCache } from '../../../stores/mediaCache';
23
23
  import { audioDebug } from '../utils/debug';
24
24
 
@@ -61,11 +61,14 @@ export function AudioProvider({
61
61
  const { saveAudioPosition, getAudioPosition } = useAudioCache();
62
62
  const lastSavedTimeRef = useRef<number>(0);
63
63
 
64
+ // Handle prefetch if enabled (for streaming URLs)
65
+ const { url: resolvedUrl, isLoading: isPrefetching, progress: prefetchProgress } = useAudioSource(source);
66
+
64
67
  // Memoize WaveSurfer options with theme-aware colors
65
68
  const options = useMemo(
66
69
  () => ({
67
70
  container: containerRef,
68
- url: source.uri,
71
+ url: resolvedUrl || undefined, // Use prefetched blob URL or original
69
72
  // Theme-aware colors using HSL
70
73
  waveColor: waveformOptions.waveColor || 'hsl(217 91% 60% / 0.3)',
71
74
  progressColor: waveformOptions.progressColor || 'hsl(217 91% 60%)',
@@ -80,7 +83,7 @@ export function AudioProvider({
80
83
  hideScrollbar: true,
81
84
  autoplay: autoPlay,
82
85
  }),
83
- [source.uri, autoPlay, waveformOptions, containerRef]
86
+ [resolvedUrl, autoPlay, waveformOptions, containerRef]
84
87
  );
85
88
 
86
89
  // Use official wavesurfer-react hook
@@ -309,6 +312,10 @@ export function AudioProvider({
309
312
  isMuted,
310
313
  isLooping,
311
314
 
315
+ // Prefetch state
316
+ isPrefetching,
317
+ prefetchProgress,
318
+
312
319
  // Audio analysis
313
320
  audioLevels,
314
321
 
@@ -336,6 +343,8 @@ export function AudioProvider({
336
343
  volume,
337
344
  isMuted,
338
345
  isLooping,
346
+ isPrefetching,
347
+ prefetchProgress,
339
348
  audioLevels,
340
349
  play,
341
350
  pause,
@@ -5,6 +5,8 @@
5
5
  // Internal hooks (used by provider)
6
6
  export { useSharedWebAudio } from './useSharedWebAudio';
7
7
  export { useAudioAnalysis } from './useAudioAnalysis';
8
+ export { useAudioSource } from './useAudioSource';
9
+ export type { UseAudioSourceResult } from './useAudioSource';
8
10
 
9
11
  // Public hooks
10
12
  export { useAudioHotkeys, AUDIO_SHORTCUTS } from './useAudioHotkeys';
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useAudioSource - Handles audio source loading with optional prefetch
5
+ *
6
+ * For streaming URLs, WaveSurfer needs the complete file to enable seeking.
7
+ * This hook fetches the URL as blob when prefetch is enabled.
8
+ */
9
+
10
+ import { useState, useEffect, useRef } from 'react';
11
+ import type { AudioSource } from '../types';
12
+ import { audioDebug } from '../utils/debug';
13
+
14
+ export interface UseAudioSourceResult {
15
+ /** The resolved URL (blob URL if prefetched, original URL otherwise) */
16
+ url: string | null;
17
+ /** Whether the source is currently being fetched */
18
+ isLoading: boolean;
19
+ /** Error message if fetch failed */
20
+ error: string | null;
21
+ /** Progress percentage (0-100) during fetch */
22
+ progress: number;
23
+ }
24
+
25
+ export function useAudioSource(source: AudioSource): UseAudioSourceResult {
26
+ const [url, setUrl] = useState<string | null>(null);
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [error, setError] = useState<string | null>(null);
29
+ const [progress, setProgress] = useState(0);
30
+ const blobUrlRef = useRef<string | null>(null);
31
+
32
+ useEffect(() => {
33
+ // Cleanup previous blob URL
34
+ if (blobUrlRef.current) {
35
+ URL.revokeObjectURL(blobUrlRef.current);
36
+ blobUrlRef.current = null;
37
+ }
38
+
39
+ // Reset state
40
+ setError(null);
41
+ setProgress(0);
42
+
43
+ // No prefetch - use URL directly
44
+ if (!source.prefetch) {
45
+ setUrl(source.uri);
46
+ setIsLoading(false);
47
+ return;
48
+ }
49
+
50
+ // Prefetch enabled - fetch as blob
51
+ const abortController = new AbortController();
52
+ setIsLoading(true);
53
+
54
+ const fetchAsBlob = async () => {
55
+ try {
56
+ audioDebug.info('Prefetching audio as blob', { uri: source.uri });
57
+
58
+ const response = await fetch(source.uri, {
59
+ signal: abortController.signal,
60
+ });
61
+
62
+ if (!response.ok) {
63
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
64
+ }
65
+
66
+ // Get content length for progress tracking
67
+ const contentLength = response.headers.get('Content-Length');
68
+ const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
69
+
70
+ if (!response.body) {
71
+ // Fallback for browsers without ReadableStream
72
+ const blob = await response.blob();
73
+ const blobUrl = URL.createObjectURL(blob);
74
+ blobUrlRef.current = blobUrl;
75
+ setUrl(blobUrl);
76
+ setProgress(100);
77
+ audioDebug.success('Audio prefetched (no stream)', { size: blob.size });
78
+ return;
79
+ }
80
+
81
+ // Stream the response for progress tracking
82
+ const reader = response.body.getReader();
83
+ const chunks: ArrayBuffer[] = [];
84
+ let receivedBytes = 0;
85
+
86
+ while (true) {
87
+ const { done, value } = await reader.read();
88
+
89
+ if (done) break;
90
+
91
+ // Convert Uint8Array to ArrayBuffer for Blob compatibility
92
+ chunks.push(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
93
+ receivedBytes += value.length;
94
+
95
+ if (totalBytes > 0) {
96
+ setProgress(Math.round((receivedBytes / totalBytes) * 100));
97
+ }
98
+ }
99
+
100
+ // Combine chunks into blob
101
+ const blob = new Blob(chunks, { type: 'audio/mpeg' });
102
+ const blobUrl = URL.createObjectURL(blob);
103
+ blobUrlRef.current = blobUrl;
104
+ setUrl(blobUrl);
105
+ setProgress(100);
106
+
107
+ audioDebug.success('Audio prefetched', {
108
+ size: blob.size,
109
+ sizeFormatted: `${(blob.size / 1024 / 1024).toFixed(2)} MB`,
110
+ });
111
+ } catch (err) {
112
+ if (err instanceof Error && err.name === 'AbortError') {
113
+ return; // Ignore abort errors
114
+ }
115
+
116
+ const errorMessage = err instanceof Error ? err.message : 'Failed to prefetch audio';
117
+ audioDebug.error('Failed to prefetch audio', { error: errorMessage, uri: source.uri });
118
+ setError(errorMessage);
119
+
120
+ // Fallback to direct URL (may have seek issues)
121
+ setUrl(source.uri);
122
+ } finally {
123
+ setIsLoading(false);
124
+ }
125
+ };
126
+
127
+ fetchAsBlob();
128
+
129
+ return () => {
130
+ abortController.abort();
131
+ if (blobUrlRef.current) {
132
+ URL.revokeObjectURL(blobUrlRef.current);
133
+ blobUrlRef.current = null;
134
+ }
135
+ };
136
+ }, [source.uri, source.prefetch]);
137
+
138
+ return { url, isLoading, error, progress };
139
+ }
@@ -10,7 +10,15 @@ import type { AudioLevels } from './effects';
10
10
  // =============================================================================
11
11
 
12
12
  export interface AudioSource {
13
+ /** Audio URL (streaming or direct) */
13
14
  uri: string;
15
+ /**
16
+ * Pre-fetch the URL as blob before passing to WaveSurfer.
17
+ * Required for streaming URLs because WaveSurfer needs complete file for seek to work.
18
+ * When true, the URL will be fetched and converted to blob URL.
19
+ * @default false
20
+ */
21
+ prefetch?: boolean;
14
22
  }
15
23
 
16
24
  // =============================================================================
@@ -89,6 +97,12 @@ export interface AudioContextState {
89
97
  isMuted: boolean;
90
98
  isLooping: boolean;
91
99
 
100
+ // Prefetch state (for streaming URLs)
101
+ /** Whether audio is being prefetched as blob */
102
+ isPrefetching: boolean;
103
+ /** Prefetch progress (0-100) */
104
+ prefetchProgress: number;
105
+
92
106
  // Audio analysis (for reactive effects)
93
107
  audioLevels: AudioLevels;
94
108