@exode-team/react-recorder 1.1.2 → 1.1.5

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.
Files changed (32) hide show
  1. package/dist/components/AudioRecorder.d.ts +2 -3
  2. package/dist/components/AudioRecorder.d.ts.map +1 -1
  3. package/dist/components/AudioRecorder.styled.d.ts.map +1 -1
  4. package/dist/components/RecordingShell/RecordingShell.d.ts.map +1 -1
  5. package/dist/components/RecordingShell/RecordingShell.styled.d.ts.map +1 -1
  6. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.d.ts.map +1 -1
  7. package/dist/components/VoiceRecorderInput/PreviewBars.d.ts +1 -1
  8. package/dist/components/VoiceRecorderInput/PreviewBars.d.ts.map +1 -1
  9. package/dist/components/VoiceRecorderInput/TimelineBars.d.ts +1 -1
  10. package/dist/components/VoiceRecorderInput/TimelineBars.d.ts.map +1 -1
  11. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.d.ts +1 -2
  12. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.d.ts.map +1 -1
  13. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.d.ts.map +1 -1
  14. package/dist/hooks/useAudioBlobLoader.d.ts.map +1 -1
  15. package/dist/hooks/useRecordingSender.d.ts +1 -2
  16. package/dist/hooks/useRecordingSender.d.ts.map +1 -1
  17. package/dist/hooks/useVoiceRecorder.d.ts.map +1 -1
  18. package/dist/hooks/useVoiceRecorderInput.d.ts +1 -2
  19. package/dist/hooks/useVoiceRecorderInput.d.ts.map +1 -1
  20. package/dist/index.cjs +84 -65
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.ts +1 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +87 -67
  25. package/dist/index.js.map +1 -1
  26. package/dist/types/index.d.ts +1 -2
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/recorder.d.ts +1 -2
  29. package/dist/types/recorder.d.ts.map +1 -1
  30. package/dist/types/upload.d.ts +0 -7
  31. package/dist/types/upload.d.ts.map +1 -1
  32. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -12,9 +12,8 @@ export { useVoiceRecorder } from './hooks/useVoiceRecorder';
12
12
  export { useVoiceMessagePlayer } from './hooks/useVoiceMessagePlayer';
13
13
  export { useRecorderVisualization } from './hooks/useRecorderVisualization';
14
14
  export { useRecordingTiming } from './hooks/useRecordingTiming';
15
- export type { StorageSpace, StorageFileEntity, UploadStrategy } from './types/upload';
15
+ export type { StorageFileEntity, UploadStrategy } from './types/upload';
16
16
  export type { UseVoiceRecorderOptions, UseVoiceRecorderResult, UseVoiceMessagePlayerOptions, UseVoiceMessagePlayerResult, } from './types/recorder';
17
- export { StorageSpaceEnum } from './types/upload';
18
17
  export { AUDIO_LEVEL_BARS } from './constants';
19
18
  export { clampValue } from './utils/math';
20
19
  export { formatTime } from './utils/format';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,YAAY,EAAE,qBAAqB,EAAE,MAAM,oDAAoD,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAGtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGhE,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACtF,YAAY,EACR,uBAAuB,EACvB,sBAAsB,EACtB,4BAA4B,EAC5B,2BAA2B,GAC9B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,YAAY,EAAE,qBAAqB,EAAE,MAAM,oDAAoD,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAGtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGhE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,YAAY,EACR,uBAAuB,EACvB,sBAAsB,EACtB,4BAA4B,EAC5B,2BAA2B,GAC9B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import React, { useState, useRef, useCallback, useEffect, useMemo, useImperativeHandle, useLayoutEffect } from 'react';
2
+ import React, { useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect, useImperativeHandle } from 'react';
3
3
  import styled from 'styled-components';
4
- import { Icon32Microphone, Icon16Microphone, Icon16Square, Icon16Pause, Icon16Play, Icon28CancelOutline, Icon28SendOutline, Icon20VoiceOutline, Icon24Pause, Icon24Play } from '@vkontakte/icons';
4
+ import { Icon32Microphone, Icon16Microphone, Icon16Pause, Icon16Play, Icon28DeleteOutlineAndroid, Icon28SendOutline, Icon20Voice, Icon24Pause, Icon24Play } from '@vkontakte/icons';
5
5
 
6
6
  /**
7
7
  * Math utilities
@@ -585,7 +585,7 @@ const useRecorderVisualization = () => {
585
585
  * @author: exode <hello@exode.ru>
586
586
  */
587
587
  const useRecordingSender = (options) => {
588
- const { space, uploader, fileName, mimeType, uploadId, audioBlob, onUploaded, onRecordingError, deleteRecording, autoResetAfterUpload, } = options;
588
+ const { uploader, fileName, mimeType, uploadId, audioBlob, onUploaded, onRecordingError, deleteRecording, autoResetAfterUpload, } = options;
589
589
  const sendRecording = useCallback(async () => {
590
590
  if (!audioBlob) {
591
591
  const error = new Error('Voice recording is not available');
@@ -601,7 +601,7 @@ const useRecordingSender = (options) => {
601
601
  const file = new File([audioBlob], fileName, {
602
602
  type: audioBlob.type || mimeType,
603
603
  });
604
- const uploaded = await uploader(file, { space, uploadId });
604
+ const uploaded = await uploader(file, { uploadId });
605
605
  await onUploaded(uploaded);
606
606
  if (autoResetAfterUpload) {
607
607
  deleteRecording();
@@ -612,7 +612,6 @@ const useRecordingSender = (options) => {
612
612
  throw error;
613
613
  }
614
614
  }, [
615
- space,
616
615
  uploadId,
617
616
  uploader,
618
617
  fileName,
@@ -632,7 +631,7 @@ const useRecordingSender = (options) => {
632
631
  * @author: exode <hello@exode.ru>
633
632
  */
634
633
  function useVoiceRecorder(props) {
635
- const { space, uploadId, uploader, onUploaded, onRecordingStop, onRecordingStart, onRecordingError, onRecordingDelete, onPermissionDenied, fileName = 'record.webm', mimeType = 'audio/webm', autoResetAfterUpload = true, } = props;
634
+ const { uploadId, uploader, onUploaded, onRecordingStop, onRecordingStart, onRecordingError, onRecordingDelete, onPermissionDenied, fileName = 'record.webm', mimeType = 'audio/webm', autoResetAfterUpload = true, } = props;
636
635
  const [audioBlob, setAudioBlob] = useState(null);
637
636
  const { audioURL, isPlaying, mountBlob, playbackTime, stopPlayback, resetPlayback, togglePlayback: _togglePlayback, playbackDuration, } = useAudioPlayback();
638
637
  const { audioLevels, previewLevels, resetAudioLevels, stopVisualization, resetPreviewLevels, resumeVisualization, cleanupVisualization, generatePreviewLevels, initializeVisualization, } = useRecorderVisualization();
@@ -662,7 +661,6 @@ function useVoiceRecorder(props) {
662
661
  audioBlob,
663
662
  fileName,
664
663
  mimeType,
665
- space,
666
664
  uploadId,
667
665
  uploader,
668
666
  autoResetAfterUpload,
@@ -782,6 +780,7 @@ const useAudioBlobLoader = (options) => {
782
780
  const audioRef = useRef(null);
783
781
  const objectUrlRef = useRef(null);
784
782
  const abortControllerRef = useRef(null);
783
+ const failedSrcRef = useRef(null);
785
784
  const callbacksRef = useRef(playbackCallbacks);
786
785
  callbacksRef.current = playbackCallbacks;
787
786
  const createAudioFromBlob = useCallback((blob) => {
@@ -818,7 +817,7 @@ const useAudioBlobLoader = (options) => {
818
817
  }, [waveformData, createAudioFromBlob]);
819
818
  const ensureAudioLoaded = useCallback(async () => {
820
819
  var _a;
821
- if (audioRef.current) {
820
+ if (audioRef.current || failedSrcRef.current === src) {
822
821
  return;
823
822
  }
824
823
  if (preloadedBlob) {
@@ -849,6 +848,7 @@ const useAudioBlobLoader = (options) => {
849
848
  if (error instanceof DOMException && error.name === 'AbortError') {
850
849
  return;
851
850
  }
851
+ failedSrcRef.current = src;
852
852
  console.warn('[voice-player] Failed to load audio', error);
853
853
  }
854
854
  finally {
@@ -859,6 +859,7 @@ const useAudioBlobLoader = (options) => {
859
859
  }, [src, waveformData, preloadedBlob, mountFromBlob, createAudioFromBlob]);
860
860
  /** Cleanup on src change or unmount */
861
861
  useEffect(() => {
862
+ failedSrcRef.current = null;
862
863
  return () => {
863
864
  var _a;
864
865
  (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
@@ -1188,7 +1189,6 @@ const useAudioRecorderHandlers = (options) => {
1188
1189
  const Container = styled.div `
1189
1190
  position: relative;
1190
1191
  width: 100%;
1191
- padding: 16px;
1192
1192
  transition: color 200ms ease, background-color 200ms ease;
1193
1193
  color: var(--text_primary, #0a0a0a);
1194
1194
  `;
@@ -1273,29 +1273,28 @@ const Wrapper$1 = styled.div `
1273
1273
  gap: 12px;
1274
1274
  padding: 10px 14px;
1275
1275
  border-radius: 9999px;
1276
- background: var(--background_content, #ffffff);
1277
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent);
1276
+ background: var(--background_hover, color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent));
1278
1277
  `;
1279
1278
  const CancelButton$1 = styled.button `
1280
- width: 36px;
1281
- height: 36px;
1279
+ width: 38px;
1280
+ height: 38px;
1282
1281
  display: inline-flex;
1283
1282
  align-items: center;
1284
1283
  justify-content: center;
1285
- border-radius: 18px;
1286
- background: transparent;
1284
+ border-radius: 19px;
1285
+ background: color-mix(in srgb, var(--exode_red) 10%, transparent);
1287
1286
  border: 0;
1287
+ flex-shrink: 0;
1288
1288
  color: color-mix(in srgb, var(--text_primary, #0a0a0a) 54%, transparent);
1289
1289
  cursor: pointer;
1290
- transition: color 160ms ease, background-color 160ms ease;
1290
+ transition: transform 160ms ease;
1291
1291
 
1292
- &:hover {
1293
- background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 12%, transparent);
1294
- color: color-mix(in srgb, var(--text_primary, #0a0a0a) 76%, transparent);
1292
+ &:hover:not(:disabled) {
1293
+ transform: translateY(-1px);
1295
1294
  }
1296
1295
 
1297
- &:active {
1298
- background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 18%, transparent);
1296
+ &:active:not(:disabled) {
1297
+ transform: translateY(0);
1299
1298
  }
1300
1299
  `;
1301
1300
  const Track = styled.div `
@@ -1303,7 +1302,7 @@ const Track = styled.div `
1303
1302
  display: flex;
1304
1303
  align-items: center;
1305
1304
  gap: 12px;
1306
- padding: 8px 12px;
1305
+ padding: 8px 10px;
1307
1306
  border-radius: 9999px;
1308
1307
  background: color-mix(in srgb, var(--text_primary, #0a0a0a) 10%, transparent);
1309
1308
  `;
@@ -1318,7 +1317,7 @@ const CircleButton = styled.button `
1318
1317
  cursor: pointer;
1319
1318
  transition: transform 160ms ease, color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
1320
1319
  flex-shrink: 0;
1321
- background: ${({ $variant }) => ($variant === 'play' ? 'var(--background_content, #ffffff)' : 'var(--background_content, #ffffff)')};
1320
+ background: var(--background_content, #ffffff);
1322
1321
 
1323
1322
  color: ${({ $variant }) => {
1324
1323
  if ($variant === 'record') {
@@ -1467,7 +1466,9 @@ const RecordingShell = (props) => {
1467
1466
  return formatTime(0);
1468
1467
  }, [formatTime, hasRecording, isPlaying, playbackDuration, playbackTime, recordingTime]);
1469
1468
  const recordButtonDisabled = isSending;
1470
- const recordButtonIcon = !isRecording || isPaused ? jsx(Icon16Microphone, {}) : jsx(Icon16Square, {});
1469
+ const recordButtonIcon = !isRecording || isPaused
1470
+ ? jsx(Icon16Microphone, { width: 24, height: 24 })
1471
+ : jsx(Icon16Pause, { width: 24, height: 24 });
1471
1472
  const recordButtonVariant = !isRecording || isPaused ? 'resume' : 'record';
1472
1473
  const recordButtonAria = !isRecording
1473
1474
  ? startAriaLabel
@@ -1476,14 +1477,16 @@ const RecordingShell = (props) => {
1476
1477
  : pauseRecordingAriaLabel || stopAriaLabel;
1477
1478
  const canShowPlayback = !isRecording || isPaused;
1478
1479
  const playbackDisabled = !hasRecording && !isRecording;
1479
- const playbackIcon = isPlaying ? jsx(Icon16Pause, {}) : jsx(Icon16Play, {});
1480
+ const playbackIcon = isPlaying
1481
+ ? jsx(Icon16Pause, { width: 24, height: 24 })
1482
+ : jsx(Icon16Play, { width: 24, height: 24 });
1480
1483
  const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
1481
1484
  const sendDisabled = isSending || (!hasRecording && !isRecording);
1482
- return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(Icon28CancelOutline, { width: 20, height: 20 }) }), jsxs(Track, { children: [jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsx(Waveform, { "$expanded": canShowPlayback, children: jsx(WaveformBars, { values: waveformValues, divider: 100, scale: 42, minHeight: 6, styleInterpolator: ({ normalized }) => ({
1485
+ return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 24, height: 24, style: { minWidth: 24 } }) }), jsxs(Track, { children: [jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsx(Waveform, { "$expanded": canShowPlayback, children: jsx(WaveformBars, { values: waveformValues, divider: 100, scale: 42, minHeight: 6, styleInterpolator: ({ normalized }) => ({
1483
1486
  height: `${12 + normalized / 2}px`,
1484
1487
  maxHeight: '34px',
1485
1488
  opacity: 0.38 + Math.min(0.5, normalized / 120),
1486
- }) }) }), jsx(Timer$1, { children: displayTime })] }), jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsx(SpinnerSlot, { children: jsx(Spinner, {}) })) : (jsx(Icon28SendOutline, { width: 20, height: 20 })) })] }));
1489
+ }) }) }), jsx(Timer$1, { children: displayTime })] }), jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsx(SpinnerSlot, { children: jsx(Spinner, {}) })) : (jsx(Icon28SendOutline, { width: 24, height: 24 })) })] }));
1487
1490
  };
1488
1491
 
1489
1492
  /**
@@ -1531,24 +1534,12 @@ const mergeMessages = (custom) => {
1531
1534
  };
1532
1535
  };
1533
1536
 
1534
- /**
1535
- * Upload types
1536
- *
1537
- * @author: exode <hello@exode.ru>
1538
- */
1539
- const StorageSpaceEnum = {
1540
- User: 'user',
1541
- Shared: 'shared',
1542
- System: 'system',
1543
- };
1544
-
1545
1537
  const AudioRecorder = (props) => {
1546
- const { space, fileName, uploadId, mimeType, className, uploader, onUploaded, onUploadError, onRecordingStop, onRecordingError, onRecordingStart, onRecordingDelete, onPermissionDenied, autoResetAfterUpload, messages: customMessages, } = props;
1538
+ const { fileName, uploadId, mimeType, className, uploader, onUploaded, onUploadError, onRecordingStop, onRecordingError, onRecordingStart, onRecordingDelete, onPermissionDenied, autoResetAfterUpload, messages: customMessages, } = props;
1547
1539
  const messages = useMemo(() => mergeMessages(customMessages), [customMessages]);
1548
1540
  const [resolvedUploadId] = useState(() => uploadId !== null && uploadId !== void 0 ? uploadId : (typeof crypto !== 'undefined' && 'randomUUID' in crypto
1549
1541
  ? crypto.randomUUID()
1550
1542
  : `upload-${Date.now()}`));
1551
- const resolvedSpace = space !== null && space !== void 0 ? space : StorageSpaceEnum.User;
1552
1543
  const handleRecorderError = useCallback((error) => {
1553
1544
  onRecordingError === null || onRecordingError === void 0 ? void 0 : onRecordingError(error);
1554
1545
  onUploadError === null || onUploadError === void 0 ? void 0 : onUploadError(error);
@@ -1566,7 +1557,6 @@ const AudioRecorder = (props) => {
1566
1557
  onRecordingDelete,
1567
1558
  onPermissionDenied,
1568
1559
  autoResetAfterUpload,
1569
- space: resolvedSpace,
1570
1560
  uploader,
1571
1561
  uploadId: resolvedUploadId,
1572
1562
  onRecordingError: handleRecorderError,
@@ -1589,7 +1579,7 @@ const AudioRecorder = (props) => {
1589
1579
  * @author: exode <hello@exode.ru>
1590
1580
  */
1591
1581
  const useVoiceRecorderInput = (options) => {
1592
- const { onSend, onError, onCancel, uploader, onUploaded, onPermissionDenied, autoStart = true, space = StorageSpaceEnum.User, mimeType = 'audio/webm', fileName = 'voice-message.webm', } = options;
1582
+ const { onSend, onError, onCancel, uploader, onUploaded, onPermissionDenied, autoStart = true, mimeType = 'audio/webm', fileName = 'voice-message.webm', } = options;
1593
1583
  const [isSending, setIsSending] = useState(false);
1594
1584
  const [timelineLevels, setTimelineLevels] = useState([]);
1595
1585
  const audioBlobRef = useRef(null);
@@ -1610,7 +1600,6 @@ const useVoiceRecorderInput = (options) => {
1610
1600
  }
1611
1601
  }, []);
1612
1602
  const recorder = useVoiceRecorder({
1613
- space,
1614
1603
  fileName,
1615
1604
  mimeType,
1616
1605
  uploadId,
@@ -1743,8 +1732,8 @@ const useVoiceRecorderInput = (options) => {
1743
1732
 
1744
1733
  const PreviewBars = (props) => {
1745
1734
  var _a, _b;
1746
- const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, getVisibleBarsCount, } = props;
1747
- const maxBars = getVisibleBarsCount();
1735
+ const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, visibleBarsCount, } = props;
1736
+ const maxBars = visibleBarsCount;
1748
1737
  const sourceLevels = timelineLevels.length > 0
1749
1738
  ? timelineLevels
1750
1739
  : previewLevels;
@@ -1788,8 +1777,8 @@ const PreviewBars = (props) => {
1788
1777
  };
1789
1778
 
1790
1779
  const TimelineBars = (props) => {
1791
- const { timelineLevels, getVisibleBarsCount } = props;
1792
- const maxBars = getVisibleBarsCount();
1780
+ const { timelineLevels, visibleBarsCount } = props;
1781
+ const maxBars = visibleBarsCount;
1793
1782
  const visible = timelineLevels.slice(-maxBars);
1794
1783
  const padded = [
1795
1784
  ...visible,
@@ -1834,15 +1823,11 @@ const CancelButton = styled.button `
1834
1823
  border-radius: 16px;
1835
1824
  background: transparent;
1836
1825
  border: 0;
1826
+ margin-right: 4px;
1837
1827
  flex-shrink: 0;
1838
1828
  color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);
1839
1829
  cursor: pointer;
1840
1830
  transition: color 160ms ease, background-color 160ms ease;
1841
-
1842
- &:hover {
1843
- background-color: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);
1844
- color: color-mix(in srgb, var(--text_primary, #0a0a0a) 72%, transparent);
1845
- }
1846
1831
  `;
1847
1832
  const RecordTrack = styled.div `
1848
1833
  flex: 1;
@@ -1858,7 +1843,8 @@ const RecordTrack = styled.div `
1858
1843
  const ButtonSlot = styled.div `
1859
1844
  display: flex;
1860
1845
  align-items: center;
1861
- gap: 2px;
1846
+ gap: 4px;
1847
+ padding: 2px;
1862
1848
  flex-shrink: 0;
1863
1849
  `;
1864
1850
  const StopButton = styled.button `
@@ -1940,14 +1926,10 @@ const MicIndicator = styled.button `
1940
1926
  border: 0;
1941
1927
  flex-shrink: 0;
1942
1928
  cursor: pointer;
1943
- background: color-mix(in srgb, var(--exode_red, #ed1c24) 14%, transparent);
1944
- color: var(--exode_red, #ed1c24);
1929
+ background: var(--background_content, #ffffff);
1930
+ color: color-mix(in srgb, var(--accent, #3f8ae0) 86%, white 14%);
1945
1931
  transition: background-color 140ms ease, transform 120ms ease;
1946
1932
 
1947
- &:hover {
1948
- background: color-mix(in srgb, var(--exode_red, #ed1c24) 22%, transparent);
1949
- }
1950
-
1951
1933
  &:active {
1952
1934
  transform: scale(0.94);
1953
1935
  }
@@ -1995,19 +1977,39 @@ const VoiceRecorderInput = (props) => {
1995
1977
  const waveformRef = useRef(null);
1996
1978
  const { recorder, isSending, isPreview, timelineLevels, lastDurationRef, handleResume, handlePause, handleCancel, handleSendRequest, } = useVoiceRecorderInput(inputOptions);
1997
1979
  const { isPlaying, isRecording, recordingTime, hasRecording, previewLevels, playbackTime, formatTime, togglePlayback, } = recorder;
1998
- const getVisibleBarsCount = useCallback(() => {
1980
+ const [visibleBarsCount, setVisibleBarsCount] = useState(0);
1981
+ const measureBars = () => {
1999
1982
  const el = waveformRef.current;
2000
1983
  if (!el) {
2001
- return 60;
1984
+ return;
2002
1985
  }
2003
- return Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP));
1986
+ setVisibleBarsCount(Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)));
1987
+ };
1988
+ /** Sync recalc before paint — catches layout shifts from button changes */
1989
+ useLayoutEffect(measureBars);
1990
+ /** ResizeObserver for external size changes (window resize, container resize) */
1991
+ useEffect(() => {
1992
+ const el = waveformRef.current;
1993
+ if (!el) {
1994
+ return;
1995
+ }
1996
+ let rafId = 0;
1997
+ const observer = new ResizeObserver(() => {
1998
+ cancelAnimationFrame(rafId);
1999
+ rafId = requestAnimationFrame(measureBars);
2000
+ });
2001
+ observer.observe(el);
2002
+ return () => {
2003
+ cancelAnimationFrame(rafId);
2004
+ observer.disconnect();
2005
+ };
2004
2006
  }, []);
2005
2007
  const displayTime = (isPreview && isPlaying)
2006
2008
  ? formatTime(playbackTime)
2007
2009
  : formatTime(recordingTime || lastDurationRef.current);
2008
- return (jsxs(Wrapper, { className: className, children: [jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsx(Icon28CancelOutline, { width: 20, height: 20 }) }), jsxs(RecordTrack, { children: [jsx(ButtonSlot, { children: isPreview ? (jsxs(Fragment, { children: [jsx(MicIndicator, { type: "button", onClick: handleResume, "aria-label": "Resume recording", children: jsx(Icon20VoiceOutline, { width: 14, height: 14 }) }), jsx(PlayPauseButton, { type: "button", onClick: togglePlayback, "aria-label": isPlaying ? 'Pause' : 'Play', children: isPlaying
2010
+ return (jsxs(Wrapper, { className: className, children: [jsx(CancelButton, { type: "button", onClick: handleCancel, "aria-label": "Cancel", children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 28, height: 28, style: { minWidth: 28 } }) }), jsxs(RecordTrack, { children: [jsx(ButtonSlot, { children: isPreview ? (jsxs(Fragment, { children: [jsx(MicIndicator, { type: "button", onClick: handleResume, "aria-label": "Resume recording", children: jsx(Icon20Voice, { width: 14, height: 14 }) }), jsx(PlayPauseButton, { type: "button", onClick: togglePlayback, "aria-label": isPlaying ? 'Pause' : 'Play', children: isPlaying
2009
2011
  ? jsx(Icon16Pause, { width: 14, height: 14 })
2010
- : jsx(Icon16Play, { width: 14, height: 14 }) })] })) : (jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsx(Icon16Square, { width: 12, height: 12 }) })) }), jsx(WaveformArea, { ref: waveformRef, children: isPreview ? (jsx(PreviewBars, { timelineLevels: timelineLevels, previewLevels: previewLevels, isPlaying: isPlaying, playbackTime: playbackTime, recordingTime: recordingTime, lastDuration: lastDurationRef.current, getVisibleBarsCount: getVisibleBarsCount })) : (jsx(TimelineBars, { timelineLevels: timelineLevels, getVisibleBarsCount: getVisibleBarsCount })) }), jsx(Timer, { children: displayTime })] }), jsx(SendButton, { type: "button", onClick: handleSendRequest, disabled: isSending || (!hasRecording && !isRecording), "aria-label": "Send", children: isSending ? (jsx(SpinnerIcon, {})) : (jsx(Icon28SendOutline, {})) })] }));
2012
+ : jsx(Icon16Play, { width: 14, height: 14 }) })] })) : (jsx(StopButton, { type: "button", onClick: handlePause, "aria-label": "Pause recording", children: jsx(Icon16Pause, { width: 14, height: 14 }) })) }), jsx(WaveformArea, { ref: waveformRef, children: isPreview ? (jsx(PreviewBars, { timelineLevels: timelineLevels, previewLevels: previewLevels, isPlaying: isPlaying, playbackTime: playbackTime, recordingTime: recordingTime, lastDuration: lastDurationRef.current, visibleBarsCount: visibleBarsCount })) : (jsx(TimelineBars, { timelineLevels: timelineLevels, visibleBarsCount: visibleBarsCount })) }), jsx(Timer, { children: displayTime })] }), jsx(SendButton, { type: "button", onClick: handleSendRequest, disabled: isSending || (!hasRecording && !isRecording), "aria-label": "Send", children: isSending ? (jsx(SpinnerIcon, {})) : (jsx(Icon28SendOutline, {})) })] }));
2011
2013
  };
2012
2014
 
2013
2015
  /**
@@ -2183,11 +2185,29 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
2183
2185
  return VoicePlaybackChain.register(chainId, () => play());
2184
2186
  }
2185
2187
  }, [chainId, play]);
2186
- /** Measure track width once after mount */
2187
- useLayoutEffect(() => {
2188
+ const measureTrackWidth = () => {
2188
2189
  if (trackRef.current) {
2189
2190
  setTrackWidth(trackRef.current.clientWidth);
2190
2191
  }
2192
+ };
2193
+ /** Sync recalc before paint — catches layout shifts */
2194
+ useLayoutEffect(measureTrackWidth);
2195
+ /** ResizeObserver for external size changes (window resize, container resize) */
2196
+ useEffect(() => {
2197
+ const el = trackRef.current;
2198
+ if (!el) {
2199
+ return;
2200
+ }
2201
+ let rafId = 0;
2202
+ const observer = new ResizeObserver(() => {
2203
+ cancelAnimationFrame(rafId);
2204
+ rafId = requestAnimationFrame(measureTrackWidth);
2205
+ });
2206
+ observer.observe(el);
2207
+ return () => {
2208
+ cancelAnimationFrame(rafId);
2209
+ observer.disconnect();
2210
+ };
2191
2211
  }, []);
2192
2212
  const displayTime = isPlaying
2193
2213
  ? formatTime(currentTime)
@@ -2272,5 +2292,5 @@ class VoiceBlobCacheClass {
2272
2292
  }
2273
2293
  const VoiceBlobCache = new VoiceBlobCacheClass();
2274
2294
 
2275
- export { AUDIO_LEVEL_BARS, AudioRecorder, StorageSpaceEnum, VoiceBlobCache, VoiceMessagePlayer, VoicePlaybackChain, VoiceRecorderInput, WaveformBars, clampValue, formatTime, useRecorderVisualization, useRecordingTiming, useVoiceMessagePlayer, useVoiceRecorder };
2295
+ export { AUDIO_LEVEL_BARS, AudioRecorder, VoiceBlobCache, VoiceMessagePlayer, VoicePlaybackChain, VoiceRecorderInput, WaveformBars, clampValue, formatTime, useRecorderVisualization, useRecordingTiming, useVoiceMessagePlayer, useVoiceRecorder };
2276
2296
  //# sourceMappingURL=index.js.map