react-audio-wavekit 0.1.2 → 0.1.4

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
@@ -101,10 +101,7 @@ Scrolling timeline waveform (Voice Memos style). Canvas grows horizontally as re
101
101
  className="h-12 w-80 rounded bg-slate-100"
102
102
  appearance={{ scrollbar: { thumbColor: "#94a3b8" } }}
103
103
  >
104
- <LiveStreamingRecorder.Canvas
105
- appearance={{ barColor: "#3b82f6", barWidth: 2, barGap: 1 }}
106
- growWidth={true}
107
- />
104
+ <LiveStreamingRecorder.Canvas appearance={{ barColor: "#3b82f6", barWidth: 2, barGap: 1 }} />
108
105
  </LiveStreamingRecorder.Root>
109
106
  ```
110
107
 
@@ -122,7 +119,6 @@ Scrolling timeline waveform (Voice Memos style). Canvas grows horizontally as re
122
119
 
123
120
  | Prop | Type | Default | Description |
124
121
  |------|------|---------|-------------|
125
- | `growWidth` | `boolean` | `true` | Canvas grows horizontally (enables scrolling) |
126
122
  | `appearance` | `WaveformAppearance` | - | See [Appearance Options](#appearance-options) |
127
123
 
128
124
  ### LiveStreamingStackRecorder
package/dist/index.d.ts CHANGED
@@ -97,13 +97,6 @@ declare interface LiveStreamingRecorderCanvasProps extends HTMLAttributes<HTMLCa
97
97
  style?: React.CSSProperties;
98
98
  /** Waveform appearance configuration (barColor, barWidth, etc.) - scrollbar 설정은 Root에서만 유효 */
99
99
  appearance?: LiveStreamingRecorderAppearance;
100
- /**
101
- * Allow canvas width to grow beyond container (enables scrolling)
102
- * - true: Canvas grows horizontally as recording continues (Voice Memos style)
103
- * - false: Canvas stays fixed width, bars get compressed
104
- * @default true
105
- */
106
- growWidth?: boolean;
107
100
  }
108
101
 
109
102
  declare type LiveStreamingRecorderContextValue = UseRecordingAmplitudesReturn;
@@ -100,7 +100,7 @@ const LiveStreamingRecorderRoot = react.forwardRef(
100
100
  }
101
101
  );
102
102
  const LiveStreamingRecorderCanvas = react.forwardRef(
103
- function LiveStreamingRecorderCanvas2({ className = "", style, appearance, growWidth = true, ...props }, ref) {
103
+ function LiveStreamingRecorderCanvas2({ className = "", style, appearance, ...props }, ref) {
104
104
  const { amplitudes, isRecording, isPaused } = recorderContext.useLiveStreamingRecorderContext();
105
105
  const canvasRef = react.useRef(null);
106
106
  const animationRef = react.useRef(null);
@@ -137,17 +137,11 @@ const LiveStreamingRecorderCanvas = react.forwardRef(
137
137
  const barHeightScale = appearance?.barHeightScale ?? constants.DEFAULT_WAVEFORM_APPEARANCE.barHeightScale;
138
138
  const totalBarWidth = barWidth + barGap;
139
139
  if (isRecording || amplitudes.length > 0) {
140
- let canvasWidth;
141
- if (growWidth) {
142
- const requiredWidth = amplitudes.length * totalBarWidth;
143
- const calculatedWidth = amplitudes.length > 0 ? requiredWidth : containerWidth;
144
- canvasWidth = Math.max(calculatedWidth, prevCanvasWidthRef.current);
145
- prevCanvasWidthRef.current = canvasWidth;
146
- canvas.style.width = `${canvasWidth}px`;
147
- } else {
148
- canvasWidth = containerWidth;
149
- canvas.style.width = "100%";
150
- }
140
+ const requiredWidth = amplitudes.length * totalBarWidth;
141
+ const calculatedWidth = amplitudes.length > 0 ? requiredWidth : containerWidth;
142
+ const canvasWidth = Math.max(calculatedWidth, prevCanvasWidthRef.current);
143
+ prevCanvasWidthRef.current = canvasWidth;
144
+ canvas.style.width = `${canvasWidth}px`;
151
145
  canvas.width = canvasWidth * dpr;
152
146
  canvas.height = containerHeight * dpr;
153
147
  ctx.scale(dpr, dpr);
@@ -155,29 +149,16 @@ const LiveStreamingRecorderCanvas = react.forwardRef(
155
149
  ctx.fillStyle = barColor;
156
150
  const minBarHeight = 2;
157
151
  ctx.beginPath();
158
- if (growWidth) {
159
- for (let i = 0; i < amplitudes.length; i++) {
160
- const amplitude = amplitudes[i];
161
- const barHeight = Math.max(minBarHeight, amplitude * containerHeight * barHeightScale);
162
- const x = i * totalBarWidth;
163
- const y = (containerHeight - barHeight) / 2;
164
- ctx.roundRect(x, y, barWidth, barHeight, barRadius);
165
- }
166
- } else {
167
- const barsCount = Math.floor(canvasWidth / totalBarWidth);
168
- const step = amplitudes.length / barsCount;
169
- for (let i = 0; i < barsCount; i++) {
170
- const amplitudeIndex = Math.min(Math.floor(i * step), amplitudes.length - 1);
171
- const amplitude = amplitudes[amplitudeIndex] || 0;
172
- const barHeight = Math.max(minBarHeight, amplitude * containerHeight * barHeightScale);
173
- const x = i * totalBarWidth;
174
- const y = (containerHeight - barHeight) / 2;
175
- ctx.roundRect(x, y, barWidth, barHeight, barRadius);
176
- }
152
+ for (let i = 0; i < amplitudes.length; i++) {
153
+ const amplitude = amplitudes[i];
154
+ const barHeight = Math.max(minBarHeight, amplitude * containerHeight * barHeightScale);
155
+ const x = i * totalBarWidth;
156
+ const y = (containerHeight - barHeight) / 2;
157
+ ctx.roundRect(x, y, barWidth, barHeight, barRadius);
177
158
  }
178
159
  ctx.fill();
179
160
  }
180
- }, [amplitudes, isRecording, appearance, growWidth]);
161
+ }, [amplitudes, isRecording, appearance]);
181
162
  react.useEffect(() => {
182
163
  const canvas = canvasRef.current;
183
164
  if (!canvas) return;
@@ -204,7 +185,7 @@ const LiveStreamingRecorderCanvas = react.forwardRef(
204
185
  if (isRecording && !isPaused) {
205
186
  const draw = () => {
206
187
  drawWaveform();
207
- if (growWidth && containerRef.current) {
188
+ if (containerRef.current) {
208
189
  containerRef.current.scrollLeft = containerRef.current.scrollWidth;
209
190
  }
210
191
  animationRef.current = requestAnimationFrame(draw);
@@ -218,15 +199,14 @@ const LiveStreamingRecorderCanvas = react.forwardRef(
218
199
  };
219
200
  }
220
201
  drawWaveform();
221
- }, [isRecording, isPaused, drawWaveform, growWidth]);
202
+ }, [isRecording, isPaused, drawWaveform]);
222
203
  return /* @__PURE__ */ jsxRuntime.jsx(
223
204
  "canvas",
224
205
  {
225
206
  ref: canvasRef,
226
207
  className,
227
208
  style: {
228
- // Set to block in growWidth mode to allow self-determined width
229
- display: growWidth ? "block" : void 0,
209
+ display: "block",
230
210
  height: "100%",
231
211
  ...style
232
212
  },
@@ -98,7 +98,7 @@ const LiveStreamingRecorderRoot = forwardRef(
98
98
  }
99
99
  );
100
100
  const LiveStreamingRecorderCanvas = forwardRef(
101
- function LiveStreamingRecorderCanvas2({ className = "", style, appearance, growWidth = true, ...props }, ref) {
101
+ function LiveStreamingRecorderCanvas2({ className = "", style, appearance, ...props }, ref) {
102
102
  const { amplitudes, isRecording, isPaused } = useLiveStreamingRecorderContext();
103
103
  const canvasRef = useRef(null);
104
104
  const animationRef = useRef(null);
@@ -135,17 +135,11 @@ const LiveStreamingRecorderCanvas = forwardRef(
135
135
  const barHeightScale = appearance?.barHeightScale ?? DEFAULT_WAVEFORM_APPEARANCE.barHeightScale;
136
136
  const totalBarWidth = barWidth + barGap;
137
137
  if (isRecording || amplitudes.length > 0) {
138
- let canvasWidth;
139
- if (growWidth) {
140
- const requiredWidth = amplitudes.length * totalBarWidth;
141
- const calculatedWidth = amplitudes.length > 0 ? requiredWidth : containerWidth;
142
- canvasWidth = Math.max(calculatedWidth, prevCanvasWidthRef.current);
143
- prevCanvasWidthRef.current = canvasWidth;
144
- canvas.style.width = `${canvasWidth}px`;
145
- } else {
146
- canvasWidth = containerWidth;
147
- canvas.style.width = "100%";
148
- }
138
+ const requiredWidth = amplitudes.length * totalBarWidth;
139
+ const calculatedWidth = amplitudes.length > 0 ? requiredWidth : containerWidth;
140
+ const canvasWidth = Math.max(calculatedWidth, prevCanvasWidthRef.current);
141
+ prevCanvasWidthRef.current = canvasWidth;
142
+ canvas.style.width = `${canvasWidth}px`;
149
143
  canvas.width = canvasWidth * dpr;
150
144
  canvas.height = containerHeight * dpr;
151
145
  ctx.scale(dpr, dpr);
@@ -153,29 +147,16 @@ const LiveStreamingRecorderCanvas = forwardRef(
153
147
  ctx.fillStyle = barColor;
154
148
  const minBarHeight = 2;
155
149
  ctx.beginPath();
156
- if (growWidth) {
157
- for (let i = 0; i < amplitudes.length; i++) {
158
- const amplitude = amplitudes[i];
159
- const barHeight = Math.max(minBarHeight, amplitude * containerHeight * barHeightScale);
160
- const x = i * totalBarWidth;
161
- const y = (containerHeight - barHeight) / 2;
162
- ctx.roundRect(x, y, barWidth, barHeight, barRadius);
163
- }
164
- } else {
165
- const barsCount = Math.floor(canvasWidth / totalBarWidth);
166
- const step = amplitudes.length / barsCount;
167
- for (let i = 0; i < barsCount; i++) {
168
- const amplitudeIndex = Math.min(Math.floor(i * step), amplitudes.length - 1);
169
- const amplitude = amplitudes[amplitudeIndex] || 0;
170
- const barHeight = Math.max(minBarHeight, amplitude * containerHeight * barHeightScale);
171
- const x = i * totalBarWidth;
172
- const y = (containerHeight - barHeight) / 2;
173
- ctx.roundRect(x, y, barWidth, barHeight, barRadius);
174
- }
150
+ for (let i = 0; i < amplitudes.length; i++) {
151
+ const amplitude = amplitudes[i];
152
+ const barHeight = Math.max(minBarHeight, amplitude * containerHeight * barHeightScale);
153
+ const x = i * totalBarWidth;
154
+ const y = (containerHeight - barHeight) / 2;
155
+ ctx.roundRect(x, y, barWidth, barHeight, barRadius);
175
156
  }
176
157
  ctx.fill();
177
158
  }
178
- }, [amplitudes, isRecording, appearance, growWidth]);
159
+ }, [amplitudes, isRecording, appearance]);
179
160
  useEffect(() => {
180
161
  const canvas = canvasRef.current;
181
162
  if (!canvas) return;
@@ -202,7 +183,7 @@ const LiveStreamingRecorderCanvas = forwardRef(
202
183
  if (isRecording && !isPaused) {
203
184
  const draw = () => {
204
185
  drawWaveform();
205
- if (growWidth && containerRef.current) {
186
+ if (containerRef.current) {
206
187
  containerRef.current.scrollLeft = containerRef.current.scrollWidth;
207
188
  }
208
189
  animationRef.current = requestAnimationFrame(draw);
@@ -216,15 +197,14 @@ const LiveStreamingRecorderCanvas = forwardRef(
216
197
  };
217
198
  }
218
199
  drawWaveform();
219
- }, [isRecording, isPaused, drawWaveform, growWidth]);
200
+ }, [isRecording, isPaused, drawWaveform]);
220
201
  return /* @__PURE__ */ jsx(
221
202
  "canvas",
222
203
  {
223
204
  ref: canvasRef,
224
205
  className,
225
206
  style: {
226
- // Set to block in growWidth mode to allow self-determined width
227
- display: growWidth ? "block" : void 0,
207
+ display: "block",
228
208
  height: "100%",
229
209
  ...style
230
210
  },
@@ -90,14 +90,55 @@ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, ap
90
90
  },
91
91
  [onSeek, duration]
92
92
  );
93
+ const handleKeyDown = react.useCallback(
94
+ (e) => {
95
+ if (!onSeek || !duration || duration <= 0) return;
96
+ const SEEK_STEP = 5;
97
+ const current = currentTime ?? 0;
98
+ switch (e.key) {
99
+ case "ArrowLeft":
100
+ e.preventDefault();
101
+ onSeek(Math.max(0, current - SEEK_STEP));
102
+ break;
103
+ case "ArrowRight":
104
+ e.preventDefault();
105
+ onSeek(Math.min(duration, current + SEEK_STEP));
106
+ break;
107
+ case "Home":
108
+ e.preventDefault();
109
+ onSeek(0);
110
+ break;
111
+ case "End":
112
+ e.preventDefault();
113
+ onSeek(duration);
114
+ break;
115
+ }
116
+ },
117
+ [onSeek, duration, currentTime]
118
+ );
119
+ const formatTimeForScreen = (seconds) => {
120
+ const mins = Math.floor(seconds / 60);
121
+ const secs = Math.floor(seconds % 60);
122
+ if (mins > 0) {
123
+ return `${mins}분 ${secs}초`;
124
+ }
125
+ return `${secs}초`;
126
+ };
127
+ const isInteractive = !!onSeek && !!duration && duration > 0;
93
128
  return /* @__PURE__ */ jsxRuntime.jsx(
94
129
  "canvas",
95
130
  {
96
131
  ref: canvasRef,
97
- role: "img",
98
- "aria-label": "Audio waveform",
132
+ role: isInteractive ? "slider" : "img",
133
+ "aria-label": isInteractive ? "오디오 탐색" : "오디오 파형",
134
+ "aria-valuemin": isInteractive ? 0 : void 0,
135
+ "aria-valuemax": isInteractive ? Math.floor(duration) : void 0,
136
+ "aria-valuenow": isInteractive ? Math.floor(currentTime ?? 0) : void 0,
137
+ "aria-valuetext": isInteractive ? `${formatTimeForScreen(currentTime ?? 0)} / ${formatTimeForScreen(duration)}` : void 0,
138
+ tabIndex: isInteractive ? 0 : -1,
99
139
  onClick: handleClick,
100
- style: { cursor: onSeek ? "pointer" : void 0, ...style },
140
+ onKeyDown: isInteractive ? handleKeyDown : void 0,
141
+ style: { cursor: isInteractive ? "pointer" : void 0, ...style },
101
142
  ...props
102
143
  }
103
144
  );
@@ -88,14 +88,55 @@ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearan
88
88
  },
89
89
  [onSeek, duration]
90
90
  );
91
+ const handleKeyDown = useCallback(
92
+ (e) => {
93
+ if (!onSeek || !duration || duration <= 0) return;
94
+ const SEEK_STEP = 5;
95
+ const current = currentTime ?? 0;
96
+ switch (e.key) {
97
+ case "ArrowLeft":
98
+ e.preventDefault();
99
+ onSeek(Math.max(0, current - SEEK_STEP));
100
+ break;
101
+ case "ArrowRight":
102
+ e.preventDefault();
103
+ onSeek(Math.min(duration, current + SEEK_STEP));
104
+ break;
105
+ case "Home":
106
+ e.preventDefault();
107
+ onSeek(0);
108
+ break;
109
+ case "End":
110
+ e.preventDefault();
111
+ onSeek(duration);
112
+ break;
113
+ }
114
+ },
115
+ [onSeek, duration, currentTime]
116
+ );
117
+ const formatTimeForScreen = (seconds) => {
118
+ const mins = Math.floor(seconds / 60);
119
+ const secs = Math.floor(seconds % 60);
120
+ if (mins > 0) {
121
+ return `${mins}분 ${secs}초`;
122
+ }
123
+ return `${secs}초`;
124
+ };
125
+ const isInteractive = !!onSeek && !!duration && duration > 0;
91
126
  return /* @__PURE__ */ jsx(
92
127
  "canvas",
93
128
  {
94
129
  ref: canvasRef,
95
- role: "img",
96
- "aria-label": "Audio waveform",
130
+ role: isInteractive ? "slider" : "img",
131
+ "aria-label": isInteractive ? "오디오 탐색" : "오디오 파형",
132
+ "aria-valuemin": isInteractive ? 0 : void 0,
133
+ "aria-valuemax": isInteractive ? Math.floor(duration) : void 0,
134
+ "aria-valuenow": isInteractive ? Math.floor(currentTime ?? 0) : void 0,
135
+ "aria-valuetext": isInteractive ? `${formatTimeForScreen(currentTime ?? 0)} / ${formatTimeForScreen(duration)}` : void 0,
136
+ tabIndex: isInteractive ? 0 : -1,
97
137
  onClick: handleClick,
98
- style: { cursor: onSeek ? "pointer" : void 0, ...style },
138
+ onKeyDown: isInteractive ? handleKeyDown : void 0,
139
+ style: { cursor: isInteractive ? "pointer" : void 0, ...style },
99
140
  ...props
100
141
  }
101
142
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-audio-wavekit",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "license": "CC0-1.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",