@exode-team/react-recorder 1.1.12 → 1.1.15

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 (92) hide show
  1. package/README.md +2 -2
  2. package/dist/components/AudioRecorder.js +50 -0
  3. package/dist/components/AudioRecorder.js.map +1 -0
  4. package/dist/components/AudioRecorder.messages.js +47 -0
  5. package/dist/components/AudioRecorder.messages.js.map +1 -0
  6. package/dist/components/AudioRecorder.styled.js +16 -0
  7. package/dist/components/AudioRecorder.styled.js.map +1 -0
  8. package/dist/components/IdleControl/IdleControl.js +12 -0
  9. package/dist/components/IdleControl/IdleControl.js.map +1 -0
  10. package/dist/components/IdleControl/IdleControl.styled.js +68 -0
  11. package/dist/components/IdleControl/IdleControl.styled.js.map +1 -0
  12. package/dist/components/RecordingShell/RecordingShell.js +80 -0
  13. package/dist/components/RecordingShell/RecordingShell.js.map +1 -0
  14. package/dist/components/RecordingShell/RecordingShell.styled.js +159 -0
  15. package/dist/components/RecordingShell/RecordingShell.styled.js.map +1 -0
  16. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js +151 -0
  17. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js.map +1 -0
  18. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js +92 -0
  19. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js.map +1 -0
  20. package/dist/components/VoiceRecorderInput/PreviewBars.js +52 -0
  21. package/dist/components/VoiceRecorderInput/PreviewBars.js.map +1 -0
  22. package/dist/components/VoiceRecorderInput/TimelineBars.js +31 -0
  23. package/dist/components/VoiceRecorderInput/TimelineBars.js.map +1 -0
  24. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js +51 -0
  25. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js.map +1 -0
  26. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js +175 -0
  27. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js.map +1 -0
  28. package/dist/components/WaveformBars/WaveformBars.js +29 -0
  29. package/dist/components/WaveformBars/WaveformBars.js.map +1 -0
  30. package/dist/components/WaveformBars/WaveformBars.styled.js +20 -0
  31. package/dist/components/WaveformBars/WaveformBars.styled.js.map +1 -0
  32. package/dist/constants.js +17 -0
  33. package/dist/constants.js.map +1 -0
  34. package/dist/hooks/useAudioBlobLoader.d.ts +1 -5
  35. package/dist/hooks/useAudioBlobLoader.d.ts.map +1 -1
  36. package/dist/hooks/useAudioBlobLoader.js +175 -0
  37. package/dist/hooks/useAudioBlobLoader.js.map +1 -0
  38. package/dist/hooks/useAudioPlayback.js +119 -0
  39. package/dist/hooks/useAudioPlayback.js.map +1 -0
  40. package/dist/hooks/useAudioRecorderHandlers.js +137 -0
  41. package/dist/hooks/useAudioRecorderHandlers.js.map +1 -0
  42. package/dist/hooks/useMediaRecorder.js +127 -0
  43. package/dist/hooks/useMediaRecorder.js.map +1 -0
  44. package/dist/hooks/useRecorderVisualization.js +94 -0
  45. package/dist/hooks/useRecorderVisualization.js.map +1 -0
  46. package/dist/hooks/useRecordingSender.js +53 -0
  47. package/dist/hooks/useRecordingSender.js.map +1 -0
  48. package/dist/hooks/useRecordingTiming.js +93 -0
  49. package/dist/hooks/useRecordingTiming.js.map +1 -0
  50. package/dist/hooks/useVoiceMessagePlayer.js +177 -0
  51. package/dist/hooks/useVoiceMessagePlayer.js.map +1 -0
  52. package/dist/hooks/useVoiceRecorder.js +153 -0
  53. package/dist/hooks/useVoiceRecorder.js.map +1 -0
  54. package/dist/hooks/useVoiceRecorderInput.js +163 -0
  55. package/dist/hooks/useVoiceRecorderInput.js.map +1 -0
  56. package/dist/index.cjs +108 -61
  57. package/dist/index.cjs.map +1 -1
  58. package/dist/index.js +13 -2612
  59. package/dist/index.js.map +1 -1
  60. package/dist/platform/animation-frame-adapter.js +14 -0
  61. package/dist/platform/animation-frame-adapter.js.map +1 -0
  62. package/dist/platform/audio-analyzer-adapter.d.ts.map +1 -1
  63. package/dist/platform/audio-analyzer-adapter.js +74 -0
  64. package/dist/platform/audio-analyzer-adapter.js.map +1 -0
  65. package/dist/platform/audio-blob-fetcher.js +19 -0
  66. package/dist/platform/audio-blob-fetcher.js.map +1 -0
  67. package/dist/platform/audio-playback-adapter.d.ts.map +1 -1
  68. package/dist/platform/audio-playback-adapter.js +114 -0
  69. package/dist/platform/audio-playback-adapter.js.map +1 -0
  70. package/dist/platform/audio-playback-manager.js +26 -0
  71. package/dist/platform/audio-playback-manager.js.map +1 -0
  72. package/dist/platform/media-recorder-adapter.js +23 -0
  73. package/dist/platform/media-recorder-adapter.js.map +1 -0
  74. package/dist/platform/microphone.js +24 -0
  75. package/dist/platform/microphone.js.map +1 -0
  76. package/dist/platform/mpeg-frame.js +60 -0
  77. package/dist/platform/mpeg-frame.js.map +1 -0
  78. package/dist/utils/format.js +13 -0
  79. package/dist/utils/format.js.map +1 -0
  80. package/dist/utils/index.d.ts +1 -0
  81. package/dist/utils/index.d.ts.map +1 -1
  82. package/dist/utils/math.js +16 -0
  83. package/dist/utils/math.js.map +1 -0
  84. package/dist/utils/pseudo-waveform.d.ts +12 -0
  85. package/dist/utils/pseudo-waveform.d.ts.map +1 -0
  86. package/dist/utils/pseudo-waveform.js +40 -0
  87. package/dist/utils/pseudo-waveform.js.map +1 -0
  88. package/dist/utils/voice-blob-cache.js +35 -0
  89. package/dist/utils/voice-blob-cache.js.map +1 -0
  90. package/dist/utils/voice-playback-chain.js +42 -0
  91. package/dist/utils/voice-playback-chain.js.map +1 -0
  92. package/package.json +2 -1
package/README.md CHANGED
@@ -146,7 +146,7 @@ const Messages = () => {
146
146
  | `duration` | `number` | — | — | Длительность в секундах (отображается до загрузки аудио). |
147
147
  | `autoPreload` | `boolean` | — | `true` | Автозагрузка аудио при попадании в viewport (IntersectionObserver). |
148
148
  | `waveformData` | `number[]` | — | — | Предварительно вычисленные уровни waveform. |
149
- | `preloadedBlob` | `Blob \| null` | — | — | Blob для мгновенного воспроизведения без сетевого запроса. |
149
+ | `preloadedBlob` | `Blob \| null` | — | — | Локальная запись. Играет из него, только пока нет `src`; при заданном `src` идёт на него, а blob переиспользуется для waveform. |
150
150
  | `onEnded` | `() => void` | — | — | Колбэк окончания воспроизведения. |
151
151
  | `className` | `string` | — | — | CSS-класс контейнера. |
152
152
  | `onClick` | `(e: React.MouseEvent) => void`| — | — | Обработчик клика по контейнеру. |
@@ -154,7 +154,7 @@ const Messages = () => {
154
154
  **Ref API (`VoiceMessagePlayerRef`):**
155
155
 
156
156
  - `play()` — программный запуск воспроизведения.
157
- - `prefetch()` — загрузка аудио без воспроизведения (fetch + decode waveform).
157
+ - `prefetch()` — подготовка аудио без воспроизведения (waveform считается фоном и play не задерживает).
158
158
 
159
159
  #### `VoicePlaybackChain`
160
160
 
@@ -0,0 +1,50 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useMemo, useState, useCallback } from 'react';
3
+ import { useVoiceRecorder } from '../hooks/useVoiceRecorder.js';
4
+ import { useAudioRecorderHandlers } from '../hooks/useAudioRecorderHandlers.js';
5
+ import { Container } from './AudioRecorder.styled.js';
6
+ import { IdleControl } from './IdleControl/IdleControl.js';
7
+ import { RecordingShell } from './RecordingShell/RecordingShell.js';
8
+ import { mergeMessages } from './AudioRecorder.messages.js';
9
+
10
+ const AudioRecorder = (props) => {
11
+ const { fileName, uploadId, mimeType, className, uploader, onUploaded, onUploadError, onRecordingStop, onRecordingError, onRecordingStart, onRecordingDelete, onPermissionDenied, autoResetAfterUpload, messages: customMessages, } = props;
12
+ const messages = useMemo(() => mergeMessages(customMessages), [customMessages]);
13
+ const [resolvedUploadId] = useState(() => uploadId !== null && uploadId !== void 0 ? uploadId : (typeof crypto !== 'undefined' && 'randomUUID' in crypto
14
+ ? crypto.randomUUID()
15
+ : `upload-${Date.now()}`));
16
+ const handleRecorderError = useCallback((error) => {
17
+ onRecordingError === null || onRecordingError === void 0 ? void 0 : onRecordingError(error);
18
+ onUploadError === null || onUploadError === void 0 ? void 0 : onUploadError(error);
19
+ }, [onRecordingError, onUploadError]);
20
+ const vibrate = useCallback((pattern) => {
21
+ if (typeof navigator !== 'undefined' && 'vibrate' in navigator) {
22
+ navigator.vibrate(pattern);
23
+ }
24
+ }, []);
25
+ const recorder = useVoiceRecorder({
26
+ fileName,
27
+ mimeType,
28
+ onRecordingStop,
29
+ onRecordingStart,
30
+ onRecordingDelete,
31
+ onPermissionDenied,
32
+ autoResetAfterUpload,
33
+ uploader,
34
+ uploadId: resolvedUploadId,
35
+ onRecordingError: handleRecorderError,
36
+ onUploaded: onUploaded !== null && onUploaded !== void 0 ? onUploaded : (() => { }),
37
+ });
38
+ const { isSending, handleCancel, handleSendRequest, handleRecordToggle, handlePlaybackToggle, } = useAudioRecorderHandlers({
39
+ vibrate,
40
+ recorder,
41
+ messages,
42
+ onUploadError,
43
+ });
44
+ const { isPaused, isPlaying, formatTime, audioLevels, isRecording, lastDuration, playbackTime, hasRecording, previewLevels, recordingTime, playbackDuration, } = recorder;
45
+ const showRecorderBar = isRecording || hasRecording || isSending;
46
+ return (jsx(Container, { className: className, children: showRecorderBar ? (jsx(RecordingShell, { isPaused: isPaused, isSending: isSending, isPlaying: isPlaying, formatTime: formatTime, onCancel: handleCancel, isRecording: isRecording, audioLevels: audioLevels, onSend: handleSendRequest, playbackTime: playbackTime, lastDuration: lastDuration, hasRecording: hasRecording, previewLevels: previewLevels, recordingTime: recordingTime, playbackDuration: playbackDuration, onRecordToggle: handleRecordToggle, sendAriaLabel: messages.buttons.send, onTogglePlayback: handlePlaybackToggle, cancelAriaLabel: messages.buttons.cancel, playAriaLabel: messages.aria.playRecording, stopAriaLabel: messages.aria.stopRecording, startAriaLabel: messages.aria.startRecording, pauseRecordingAriaLabel: messages.aria.pauseRecording, resumeRecordingAriaLabel: messages.aria.resumeRecording, pausePlaybackAriaLabel: messages.aria.pausePlayback })) : (jsx(IdleControl, { onClick: handleRecordToggle, startAriaLabel: messages.aria.startRecording, instruction: messages.instruction })) }));
47
+ };
48
+
49
+ export { AudioRecorder };
50
+ //# sourceMappingURL=AudioRecorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioRecorder.js","sources":["../../src/components/AudioRecorder.tsx"],"sourcesContent":["/**\n * AudioRecorder\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport { useVoiceRecorder } from '../hooks';\nimport { useAudioRecorderHandlers } from '../hooks/useAudioRecorderHandlers';\n\nimport { Container } from './AudioRecorder.styled';\nimport { IdleControl } from './IdleControl/IdleControl';\nimport { RecordingShell } from './RecordingShell/RecordingShell';\n\nimport { mergeMessages, type RecorderMessagesInput } from './AudioRecorder.messages';\n\nimport {\n type StorageFileEntity,\n type UploadStrategy,\n type UseVoiceRecorderOptions,\n} from '../types';\n\n\ntype RecorderOptionOverrides = Omit<\n UseVoiceRecorderOptions<Partial<StorageFileEntity>>,\n 'uploadId' | 'onUploaded' | 'uploader'\n>;\n\ninterface Props extends Partial<RecorderOptionOverrides> {\n uploader: UploadStrategy<Partial<StorageFileEntity>>;\n messages?: RecorderMessagesInput;\n uploadId?: string;\n className?: string;\n onUploadError?: (error: unknown) => void;\n onUploaded?: (file: Partial<StorageFileEntity>, duration: number) => void | Promise<void>;\n}\n\n\nconst AudioRecorder = (props: Props) => {\n\n const {\n fileName,\n uploadId,\n mimeType,\n className,\n uploader,\n onUploaded,\n onUploadError,\n onRecordingStop,\n onRecordingError,\n onRecordingStart,\n onRecordingDelete,\n onPermissionDenied,\n autoResetAfterUpload,\n messages: customMessages,\n } = props;\n\n const messages = useMemo(() => mergeMessages(customMessages), [ customMessages ]);\n\n const [ resolvedUploadId ] = useState(\n () =>\n uploadId ??\n (typeof crypto !== 'undefined' && 'randomUUID' in crypto\n ? crypto.randomUUID()\n : `upload-${Date.now()}`),\n );\n\n const handleRecorderError = useCallback(\n (error: unknown) => {\n onRecordingError?.(error);\n onUploadError?.(error);\n },\n [ onRecordingError, onUploadError ],\n );\n\n const vibrate = useCallback((pattern: number | number[]) => {\n if (typeof navigator !== 'undefined' && 'vibrate' in navigator) {\n navigator.vibrate(pattern);\n }\n }, []);\n\n const recorder = useVoiceRecorder<Partial<StorageFileEntity>>({\n fileName,\n mimeType,\n onRecordingStop,\n onRecordingStart,\n onRecordingDelete,\n onPermissionDenied,\n autoResetAfterUpload,\n uploader,\n uploadId: resolvedUploadId,\n onRecordingError: handleRecorderError,\n onUploaded: onUploaded ?? (() => {}),\n });\n\n const {\n isSending,\n handleCancel,\n handleSendRequest,\n handleRecordToggle,\n handlePlaybackToggle,\n } = useAudioRecorderHandlers({\n vibrate,\n recorder,\n messages,\n onUploadError,\n });\n\n const {\n isPaused,\n isPlaying,\n formatTime,\n audioLevels,\n isRecording,\n lastDuration,\n playbackTime,\n hasRecording,\n previewLevels,\n recordingTime,\n playbackDuration,\n } = recorder;\n\n const showRecorderBar = isRecording || hasRecording || isSending;\n\n return (\n <Container className={className}>\n {showRecorderBar ? (\n <RecordingShell isPaused={isPaused}\n isSending={isSending}\n isPlaying={isPlaying}\n formatTime={formatTime}\n onCancel={handleCancel}\n isRecording={isRecording}\n audioLevels={audioLevels}\n onSend={handleSendRequest}\n playbackTime={playbackTime}\n lastDuration={lastDuration}\n hasRecording={hasRecording}\n previewLevels={previewLevels}\n recordingTime={recordingTime}\n playbackDuration={playbackDuration}\n onRecordToggle={handleRecordToggle}\n sendAriaLabel={messages.buttons.send}\n onTogglePlayback={handlePlaybackToggle}\n cancelAriaLabel={messages.buttons.cancel}\n playAriaLabel={messages.aria.playRecording}\n stopAriaLabel={messages.aria.stopRecording}\n startAriaLabel={messages.aria.startRecording}\n pauseRecordingAriaLabel={messages.aria.pauseRecording}\n resumeRecordingAriaLabel={messages.aria.resumeRecording}\n pausePlaybackAriaLabel={messages.aria.pausePlayback}/>\n ) : (\n <IdleControl onClick={handleRecordToggle}\n startAriaLabel={messages.aria.startRecording}\n instruction={messages.instruction}/>\n )}\n </Container>\n );\n};\n\n\nexport { AudioRecorder };\n"],"names":["_jsx"],"mappings":";;;;;;;;;AAuCA,MAAM,aAAa,GAAG,CAAC,KAAY,KAAI;AAEnC,IAAA,MAAM,EACF,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,UAAU,EACV,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,QAAQ,EAAE,cAAc,GAC3B,GAAG,KAAK;AAET,IAAA,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,aAAa,CAAC,cAAc,CAAC,EAAE,CAAE,cAAc,CAAE,CAAC;IAEjF,MAAM,CAAE,gBAAgB,CAAE,GAAG,QAAQ,CACjC,MACI,QAAQ,KAAA,IAAA,IAAR,QAAQ,KAAA,MAAA,GAAR,QAAQ,IACP,OAAO,MAAM,KAAK,WAAW,IAAI,YAAY,IAAI;AAC9C,UAAE,MAAM,CAAC,UAAU;UACjB,UAAU,IAAI,CAAC,GAAG,EAAE,CAAA,CAAE,CAAC,CACpC;AAED,IAAA,MAAM,mBAAmB,GAAG,WAAW,CACnC,CAAC,KAAc,KAAI;AACf,QAAA,gBAAgB,aAAhB,gBAAgB,KAAA,MAAA,GAAA,MAAA,GAAhB,gBAAgB,CAAG,KAAK,CAAC;AACzB,QAAA,aAAa,aAAb,aAAa,KAAA,MAAA,GAAA,MAAA,GAAb,aAAa,CAAG,KAAK,CAAC;AAC1B,IAAA,CAAC,EACD,CAAE,gBAAgB,EAAE,aAAa,CAAE,CACtC;AAED,IAAA,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,OAA0B,KAAI;QACvD,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,IAAI,SAAS,EAAE;AAC5D,YAAA,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC;QAC9B;IACJ,CAAC,EAAE,EAAE,CAAC;IAEN,MAAM,QAAQ,GAAG,gBAAgB,CAA6B;QAC1D,QAAQ;QACR,QAAQ;QACR,eAAe;QACf,gBAAgB;QAChB,iBAAiB;QACjB,kBAAkB;QAClB,oBAAoB;QACpB,QAAQ;AACR,QAAA,QAAQ,EAAE,gBAAgB;AAC1B,QAAA,gBAAgB,EAAE,mBAAmB;AACrC,QAAA,UAAU,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAA,MAAA,GAAV,UAAU,IAAK,MAAK,EAAE,CAAC,CAAC;AACvC,KAAA,CAAC;AAEF,IAAA,MAAM,EACF,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,GACvB,GAAG,wBAAwB,CAAC;QACzB,OAAO;QACP,QAAQ;QACR,QAAQ;QACR,aAAa;AAChB,KAAA,CAAC;IAEF,MAAM,EACF,QAAQ,EACR,SAAS,EACT,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,aAAa,EACb,gBAAgB,GACnB,GAAG,QAAQ;AAEZ,IAAA,MAAM,eAAe,GAAG,WAAW,IAAI,YAAY,IAAI,SAAS;AAEhE,IAAA,QACIA,GAAA,CAAC,SAAS,IAAC,SAAS,EAAE,SAAS,EAAA,QAAA,EAC1B,eAAe,IACZA,GAAA,CAAC,cAAc,EAAA,EAAC,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,YAAY,EACtB,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,iBAAiB,EACzB,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,EAC1B,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,aAAa,EAC5B,gBAAgB,EAAE,gBAAgB,EAClC,cAAc,EAAE,kBAAkB,EAClC,aAAa,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,EACpC,gBAAgB,EAAE,oBAAoB,EACtC,eAAe,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,EACxC,aAAa,EAAE,QAAQ,CAAC,IAAI,CAAC,aAAa,EAC1C,aAAa,EAAE,QAAQ,CAAC,IAAI,CAAC,aAAa,EAC1C,cAAc,EAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,EAC5C,uBAAuB,EAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,EACrD,wBAAwB,EAAE,QAAQ,CAAC,IAAI,CAAC,eAAe,EACvD,sBAAsB,EAAE,QAAQ,CAAC,IAAI,CAAC,aAAa,GAAG,KAEtEA,IAAC,WAAW,EAAA,EAAC,OAAO,EAAE,kBAAkB,EAC3B,cAAc,EAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,EAC5C,WAAW,EAAE,QAAQ,CAAC,WAAW,GAAG,CACpD,EAAA,CACO;AAEpB;;;;"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * AudioRecorder messages — i18n-ready message definitions and merge utility
3
+ *
4
+ * @author: exode <hello@exode.ru>
5
+ */
6
+ const DEFAULT_MESSAGES = {
7
+ buttons: {
8
+ cancel: 'Cancel',
9
+ send: 'Send',
10
+ },
11
+ instruction: 'Tap to start recording',
12
+ aria: {
13
+ startRecording: 'Start recording voice message',
14
+ stopRecording: 'Stop recording voice message',
15
+ pauseRecording: 'Pause recording',
16
+ resumeRecording: 'Resume recording',
17
+ playRecording: 'Play recording',
18
+ pausePlayback: 'Pause playback',
19
+ },
20
+ errors: {
21
+ loadFileFailed: 'Failed to upload audio file',
22
+ },
23
+ };
24
+ const mergeMessages = (custom) => {
25
+ var _a;
26
+ if (!custom) {
27
+ return DEFAULT_MESSAGES;
28
+ }
29
+ return {
30
+ buttons: {
31
+ ...DEFAULT_MESSAGES.buttons,
32
+ ...custom.buttons,
33
+ },
34
+ instruction: (_a = custom.instruction) !== null && _a !== void 0 ? _a : DEFAULT_MESSAGES.instruction,
35
+ aria: {
36
+ ...DEFAULT_MESSAGES.aria,
37
+ ...custom.aria,
38
+ },
39
+ errors: {
40
+ ...DEFAULT_MESSAGES.errors,
41
+ ...custom.errors,
42
+ },
43
+ };
44
+ };
45
+
46
+ export { DEFAULT_MESSAGES, mergeMessages };
47
+ //# sourceMappingURL=AudioRecorder.messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioRecorder.messages.js","sources":["../../src/components/AudioRecorder.messages.ts"],"sourcesContent":["/**\n * AudioRecorder messages — i18n-ready message definitions and merge utility\n *\n * @author: exode <hello@exode.ru>\n */\n\n\nexport interface RecorderMessages {\n buttons: {\n send: string;\n cancel: string;\n };\n instruction: string;\n aria: {\n startRecording: string;\n stopRecording: string;\n pauseRecording: string;\n resumeRecording: string;\n playRecording: string;\n pausePlayback: string;\n };\n errors: {\n loadFileFailed: string;\n };\n}\n\nexport type RecorderMessagesInput = Partial<{\n buttons: Partial<RecorderMessages['buttons']>;\n instruction: RecorderMessages['instruction'];\n aria: Partial<RecorderMessages['aria']>;\n errors: Partial<RecorderMessages['errors']>;\n}>;\n\nexport const DEFAULT_MESSAGES: RecorderMessages = {\n buttons: {\n cancel: 'Cancel',\n send: 'Send',\n },\n instruction: 'Tap to start recording',\n aria: {\n startRecording: 'Start recording voice message',\n stopRecording: 'Stop recording voice message',\n pauseRecording: 'Pause recording',\n resumeRecording: 'Resume recording',\n playRecording: 'Play recording',\n pausePlayback: 'Pause playback',\n },\n errors: {\n loadFileFailed: 'Failed to upload audio file',\n },\n};\n\nexport const mergeMessages = (custom?: RecorderMessagesInput): RecorderMessages => {\n if (!custom) {\n return DEFAULT_MESSAGES;\n }\n\n return {\n buttons: {\n ...DEFAULT_MESSAGES.buttons,\n ...custom.buttons,\n },\n instruction: custom.instruction ?? DEFAULT_MESSAGES.instruction,\n aria: {\n ...DEFAULT_MESSAGES.aria,\n ...custom.aria,\n },\n errors: {\n ...DEFAULT_MESSAGES.errors,\n ...custom.errors,\n },\n };\n};\n"],"names":[],"mappings":"AAAA;;;;AAIG;AA6BI,MAAM,gBAAgB,GAAqB;AAC9C,IAAA,OAAO,EAAE;AACL,QAAA,MAAM,EAAE,QAAQ;AAChB,QAAA,IAAI,EAAE,MAAM;AACf,KAAA;AACD,IAAA,WAAW,EAAE,wBAAwB;AACrC,IAAA,IAAI,EAAE;AACF,QAAA,cAAc,EAAE,+BAA+B;AAC/C,QAAA,aAAa,EAAE,8BAA8B;AAC7C,QAAA,cAAc,EAAE,iBAAiB;AACjC,QAAA,eAAe,EAAE,kBAAkB;AACnC,QAAA,aAAa,EAAE,gBAAgB;AAC/B,QAAA,aAAa,EAAE,gBAAgB;AAClC,KAAA;AACD,IAAA,MAAM,EAAE;AACJ,QAAA,cAAc,EAAE,6BAA6B;AAChD,KAAA;;AAGE,MAAM,aAAa,GAAG,CAAC,MAA8B,KAAsB;;IAC9E,IAAI,CAAC,MAAM,EAAE;AACT,QAAA,OAAO,gBAAgB;IAC3B;IAEA,OAAO;AACH,QAAA,OAAO,EAAE;YACL,GAAG,gBAAgB,CAAC,OAAO;YAC3B,GAAG,MAAM,CAAC,OAAO;AACpB,SAAA;QACD,WAAW,EAAE,MAAA,MAAM,CAAC,WAAW,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,gBAAgB,CAAC,WAAW;AAC/D,QAAA,IAAI,EAAE;YACF,GAAG,gBAAgB,CAAC,IAAI;YACxB,GAAG,MAAM,CAAC,IAAI;AACjB,SAAA;AACD,QAAA,MAAM,EAAE;YACJ,GAAG,gBAAgB,CAAC,MAAM;YAC1B,GAAG,MAAM,CAAC,MAAM;AACnB,SAAA;KACJ;AACL;;;;"}
@@ -0,0 +1,16 @@
1
+ import styled from 'styled-components';
2
+
3
+ /**
4
+ * AudioRecorder styled components
5
+ *
6
+ * @author: exode <hello@exode.ru>
7
+ */
8
+ const Container = styled.div `
9
+ position: relative;
10
+ width: 100%;
11
+ transition: color 200ms ease, background-color 200ms ease;
12
+ color: var(--text_primary, #0a0a0a);
13
+ `;
14
+
15
+ export { Container };
16
+ //# sourceMappingURL=AudioRecorder.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioRecorder.styled.js","sources":["../../src/components/AudioRecorder.styled.ts"],"sourcesContent":["/**\n * AudioRecorder styled components\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport styled from 'styled-components';\n\n\nexport const Container = styled.div`\n position: relative;\n width: 100%;\n transition: color 200ms ease, background-color 200ms ease;\n color: var(--text_primary, #0a0a0a);\n`;\n"],"names":[],"mappings":";;AAAA;;;;AAIG;AAKI,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;"}
@@ -0,0 +1,12 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import 'react';
3
+ import { Icon32Microphone } from '@vkontakte/icons';
4
+ import { IdleWrapper, IdleButton, IdleButtonOutline, IdleButtonGlow, IdleInstruction } from './IdleControl.styled.js';
5
+
6
+ const IdleControl = (props) => {
7
+ const { onClick, instruction, startAriaLabel, } = props;
8
+ return (jsxs(IdleWrapper, { children: [jsxs(IdleButton, { type: "button", onClick: onClick, "aria-label": startAriaLabel, children: [jsx(IdleButtonOutline, { "aria-hidden": true }), jsx(IdleButtonGlow, { "aria-hidden": true }), jsx(Icon32Microphone, { height: 48, width: 48 })] }), jsx(IdleInstruction, { children: instruction })] }));
9
+ };
10
+
11
+ export { IdleControl };
12
+ //# sourceMappingURL=IdleControl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IdleControl.js","sources":["../../../src/components/IdleControl/IdleControl.tsx"],"sourcesContent":["/**\n * IdleControl\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React from 'react';\n\nimport { Icon32Microphone } from '@vkontakte/icons';\n\nimport { IdleButton, IdleButtonGlow, IdleButtonOutline, IdleInstruction, IdleWrapper } from './IdleControl.styled';\n\n\ninterface Props {\n onClick: () => void;\n instruction: string;\n startAriaLabel: string;\n}\n\n\nconst IdleControl = (props: Props) => {\n\n const {\n onClick,\n instruction,\n startAriaLabel,\n } = props;\n\n return (\n <IdleWrapper>\n <IdleButton type=\"button\" onClick={onClick} aria-label={startAriaLabel}>\n <IdleButtonOutline aria-hidden/>\n <IdleButtonGlow aria-hidden/>\n\n <Icon32Microphone height={48} width={48}/>\n </IdleButton>\n\n <IdleInstruction>\n {instruction}\n </IdleInstruction>\n </IdleWrapper>\n );\n};\n\n\nexport { IdleControl };"],"names":["_jsxs","_jsx"],"mappings":";;;;;AAoBA,MAAM,WAAW,GAAG,CAAC,KAAY,KAAI;IAEjC,MAAM,EACF,OAAO,EACP,WAAW,EACX,cAAc,GACjB,GAAG,KAAK;IAET,QACIA,KAAC,WAAW,EAAA,EAAA,QAAA,EAAA,CACRA,KAAC,UAAU,EAAA,EAAC,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAE,OAAO,EAAA,YAAA,EAAc,cAAc,aAClEC,GAAA,CAAC,iBAAiB,0BAAc,EAChCA,GAAA,CAAC,cAAc,EAAA,EAAA,aAAA,EAAA,IAAA,EAAA,CAAc,EAE7BA,IAAC,gBAAgB,EAAA,EAAC,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAA,CAAG,CAAA,EAAA,CACjC,EAEbA,GAAA,CAAC,eAAe,cACX,WAAW,EAAA,CACE,CAAA,EAAA,CACR;AAEtB;;;;"}
@@ -0,0 +1,68 @@
1
+ import styled from 'styled-components';
2
+
3
+ /**
4
+ * IdleControl styles
5
+ *
6
+ * @author: exode <hello@exode.ru>
7
+ */
8
+ const IdleWrapper = styled.div `
9
+ display: flex;
10
+ flex-direction: column;
11
+ align-items: center;
12
+ gap: 16px;
13
+ `;
14
+ const IdleButton = styled.button `
15
+ position: relative;
16
+ width: 72px;
17
+ height: 72px;
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ border-radius: 36px;
22
+ border: 0;
23
+ background: var(--background_content, #ffffff);
24
+ color: color-mix(in srgb, var(--accent, #3f8ae0) 80%, white 20%);
25
+ cursor: pointer;
26
+ transition: transform 180ms ease, box-shadow 180ms ease, background-color 180ms ease;
27
+
28
+ &:hover {
29
+ transform: translateY(-2px);
30
+ }
31
+
32
+ &:active {
33
+ transform: translateY(0);
34
+ }
35
+
36
+ &:focus-visible {
37
+ outline: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 70%, transparent);
38
+ outline-offset: 4px;
39
+ }
40
+ `;
41
+ const IdleButtonOutline = styled.span `
42
+ position: absolute;
43
+ inset: 0;
44
+ border-radius: 32px;
45
+ border: 1px solid color-mix(in srgb, var(--accent, #3f8ae0) 28%, transparent);
46
+ pointer-events: none;
47
+ `;
48
+ const IdleButtonGlow = styled.span `
49
+ position: absolute;
50
+ inset: 8px;
51
+ border-radius: 28px;
52
+ background: radial-gradient(
53
+ 120% 120% at 50% 50%,
54
+ color-mix(in srgb, var(--accent, #3f8ae0) 24%, transparent) 0%,
55
+ color-mix(in srgb, var(--accent, #3f8ae0) 10%, transparent) 65%,
56
+ transparent 100%
57
+ );
58
+ filter: blur(4px);
59
+ pointer-events: none;
60
+ `;
61
+ const IdleInstruction = styled.div `
62
+ font-size: 14px;
63
+ text-align: center;
64
+ color: color-mix(in srgb, var(--text_primary, #0a0a0a) 60%, transparent);
65
+ `;
66
+
67
+ export { IdleButton, IdleButtonGlow, IdleButtonOutline, IdleInstruction, IdleWrapper };
68
+ //# sourceMappingURL=IdleControl.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IdleControl.styled.js","sources":["../../../src/components/IdleControl/IdleControl.styled.ts"],"sourcesContent":["/**\n * IdleControl styles\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport styled from 'styled-components';\n\n\nexport const IdleWrapper = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n`;\n\nexport const IdleButton = styled.button`\n position: relative;\n width: 72px;\n height: 72px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 36px;\n border: 0;\n background: var(--background_content, #ffffff);\n color: color-mix(in srgb, var(--accent, #3f8ae0) 80%, white 20%);\n cursor: pointer;\n transition: transform 180ms ease, box-shadow 180ms ease, background-color 180ms ease;\n\n &:hover {\n transform: translateY(-2px);\n }\n\n &:active {\n transform: translateY(0);\n }\n\n &:focus-visible {\n outline: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 70%, transparent);\n outline-offset: 4px;\n }\n`;\n\nexport const IdleButtonOutline = styled.span`\n position: absolute;\n inset: 0;\n border-radius: 32px;\n border: 1px solid color-mix(in srgb, var(--accent, #3f8ae0) 28%, transparent);\n pointer-events: none;\n`;\n\nexport const IdleButtonGlow = styled.span`\n position: absolute;\n inset: 8px;\n border-radius: 28px;\n background: radial-gradient(\n 120% 120% at 50% 50%,\n color-mix(in srgb, var(--accent, #3f8ae0) 24%, transparent) 0%,\n color-mix(in srgb, var(--accent, #3f8ae0) 10%, transparent) 65%,\n transparent 100%\n );\n filter: blur(4px);\n pointer-events: none;\n`;\n\nexport const IdleInstruction = styled.div`\n font-size: 14px;\n text-align: center;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 60%, transparent);\n`;\n"],"names":[],"mappings":";;AAAA;;;;AAIG;AAKI,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;AAO9B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAM,iBAAiB,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;AAQrC,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;;;;;;;AAclC,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;"}
@@ -0,0 +1,80 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useMemo } from 'react';
3
+ import { Icon16Microphone, Icon16Pause, Icon16Play, Icon28DeleteOutlineAndroid, Icon28SendOutline } from '@vkontakte/icons';
4
+ import { WaveformBars } from '../WaveformBars/WaveformBars.js';
5
+ import { Wrapper, CancelButton, Track, CircleButton, Waveform, Timer, SendButton, SpinnerSlot, Spinner } from './RecordingShell.styled.js';
6
+
7
+ const RecordingShell = (props) => {
8
+ const { isPaused, isPlaying, isSending, isRecording, audioLevels, hasRecording, playbackTime, previewLevels, recordingTime, stopAriaLabel, playAriaLabel, sendAriaLabel, startAriaLabel, cancelAriaLabel, playbackDuration, pausePlaybackAriaLabel, pauseRecordingAriaLabel, resumeRecordingAriaLabel, onSend, onCancel, formatTime, onRecordToggle, onTogglePlayback, } = props;
9
+ const { lastDuration } = props;
10
+ const isPreview = !isRecording || isPaused;
11
+ const waveformValues = isRecording ? audioLevels : (hasRecording ? previewLevels : audioLevels);
12
+ const effectiveDuration = recordingTime
13
+ || playbackDuration
14
+ || lastDuration
15
+ || 1;
16
+ const progress = (isPreview && isPlaying)
17
+ ? Math.min(1, playbackTime / effectiveDuration)
18
+ : 0;
19
+ const displayTime = useMemo(() => {
20
+ if (isPlaying) {
21
+ return formatTime(playbackTime);
22
+ }
23
+ if (isRecording && !isPaused) {
24
+ return formatTime(recordingTime);
25
+ }
26
+ if (isRecording && isPaused) {
27
+ return formatTime(recordingTime);
28
+ }
29
+ if (lastDuration > 0) {
30
+ return formatTime(lastDuration);
31
+ }
32
+ if (hasRecording) {
33
+ if (playbackDuration > 0.5) {
34
+ return formatTime(playbackDuration);
35
+ }
36
+ if (recordingTime > 0) {
37
+ return formatTime(recordingTime);
38
+ }
39
+ }
40
+ return formatTime(0);
41
+ }, [
42
+ isPaused,
43
+ isPlaying,
44
+ formatTime,
45
+ isRecording,
46
+ lastDuration,
47
+ hasRecording,
48
+ playbackTime,
49
+ recordingTime,
50
+ playbackDuration,
51
+ ]);
52
+ const recordButtonDisabled = isSending;
53
+ const recordButtonIcon = !isRecording || isPaused
54
+ ? jsx(Icon16Microphone, { width: 24, height: 24 })
55
+ : jsx(Icon16Pause, { width: 24, height: 24 });
56
+ const recordButtonVariant = !isRecording || isPaused ? 'resume' : 'record';
57
+ const recordButtonAria = !isRecording
58
+ ? startAriaLabel
59
+ : isPaused
60
+ ? resumeRecordingAriaLabel
61
+ : pauseRecordingAriaLabel || stopAriaLabel;
62
+ const canShowPlayback = !isRecording || isPaused;
63
+ const playbackDisabled = !hasRecording && !isRecording;
64
+ const playbackIcon = isPlaying
65
+ ? jsx(Icon16Pause, { width: 24, height: 24 })
66
+ : jsx(Icon16Play, { width: 24, height: 24 });
67
+ const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
68
+ const sendDisabled = isSending || (!hasRecording && !isRecording);
69
+ return (jsxs(Wrapper, { children: [jsx(CancelButton, { 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, { scale: 42, minHeight: 6, divider: 100, progress: progress, values: waveformValues, styleInterpolator: ({ normalized, isPlayed, progress: p }) => ({
70
+ height: `${12 + normalized / 2}px`,
71
+ maxHeight: '34px',
72
+ opacity: p > 0
73
+ ? (isPlayed ? 0.80 : 0.22)
74
+ : 0.38 + Math.min(0.5, normalized / 120),
75
+ transition: 'opacity 80ms ease',
76
+ }) }) }), jsx(Timer, { children: displayTime })] }), jsx(SendButton, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsx(SpinnerSlot, { children: jsx(Spinner, {}) })) : (jsx(Icon28SendOutline, { width: 24, height: 24 })) })] }));
77
+ };
78
+
79
+ export { RecordingShell };
80
+ //# sourceMappingURL=RecordingShell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingShell.js","sources":["../../../src/components/RecordingShell/RecordingShell.tsx"],"sourcesContent":["/**\n * RecordingShell\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React, { useMemo } from 'react';\n\nimport {\n Icon16Microphone,\n Icon16Pause,\n Icon16Play,\n Icon28DeleteOutlineAndroid,\n Icon28SendOutline,\n} from '@vkontakte/icons';\n\nimport { WaveformBars } from '../WaveformBars/WaveformBars';\n\nimport {\n CancelButton,\n CircleButton,\n SendButton,\n Spinner,\n SpinnerSlot,\n Timer,\n Track,\n Waveform,\n Wrapper,\n} from './RecordingShell.styled';\n\n\ninterface Props {\n isRecording: boolean;\n isPaused: boolean;\n hasRecording: boolean;\n isPlaying: boolean;\n lastDuration: number;\n recordingTime: number;\n playbackTime: number;\n playbackDuration: number;\n audioLevels: number[];\n previewLevels: number[];\n isSending: boolean;\n cancelAriaLabel: string;\n stopAriaLabel: string;\n startAriaLabel: string;\n pauseRecordingAriaLabel: string;\n resumeRecordingAriaLabel: string;\n playAriaLabel: string;\n pausePlaybackAriaLabel: string;\n sendAriaLabel: string;\n onSend: () => void;\n onCancel: () => void;\n onRecordToggle: () => void;\n onTogglePlayback: () => void;\n formatTime: (seconds: number) => string;\n}\n\n\nconst RecordingShell = (props: Props) => {\n\n const {\n isPaused,\n isPlaying,\n isSending,\n isRecording,\n audioLevels,\n hasRecording,\n playbackTime,\n previewLevels,\n recordingTime,\n stopAriaLabel,\n playAriaLabel,\n sendAriaLabel,\n startAriaLabel,\n cancelAriaLabel,\n playbackDuration,\n pausePlaybackAriaLabel,\n pauseRecordingAriaLabel,\n resumeRecordingAriaLabel,\n onSend,\n onCancel,\n formatTime,\n onRecordToggle,\n onTogglePlayback,\n } = props;\n\n const { lastDuration } = props;\n\n const isPreview = !isRecording || isPaused;\n const waveformValues = isRecording ? audioLevels : (hasRecording ? previewLevels : audioLevels);\n\n const effectiveDuration = recordingTime\n || playbackDuration\n || lastDuration\n || 1;\n\n const progress = (isPreview && isPlaying)\n ? Math.min(1, playbackTime / effectiveDuration)\n : 0;\n\n const displayTime = useMemo(() => {\n if (isPlaying) {\n return formatTime(playbackTime);\n }\n\n if (isRecording && !isPaused) {\n return formatTime(recordingTime);\n }\n\n if (isRecording && isPaused) {\n return formatTime(recordingTime);\n }\n\n if (lastDuration > 0) {\n return formatTime(lastDuration);\n }\n\n if (hasRecording) {\n if (playbackDuration > 0.5) {\n return formatTime(playbackDuration);\n }\n\n if (recordingTime > 0) {\n return formatTime(recordingTime);\n }\n }\n\n return formatTime(0);\n }, [\n isPaused,\n isPlaying,\n formatTime,\n isRecording,\n lastDuration,\n hasRecording,\n playbackTime,\n recordingTime,\n playbackDuration,\n ]);\n\n const recordButtonDisabled = isSending;\n const recordButtonIcon = !isRecording || isPaused\n ? <Icon16Microphone width={24} height={24}/>\n : <Icon16Pause width={24} height={24}/>;\n const recordButtonVariant: 'record' | 'resume' = !isRecording || isPaused ? 'resume' : 'record';\n const recordButtonAria = !isRecording\n ? startAriaLabel\n : isPaused\n ? resumeRecordingAriaLabel\n : pauseRecordingAriaLabel || stopAriaLabel;\n\n const canShowPlayback = !isRecording || isPaused;\n const playbackDisabled = !hasRecording && !isRecording;\n const playbackIcon = isPlaying\n ? <Icon16Pause width={24} height={24}/>\n : <Icon16Play width={24} height={24}/>;\n const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;\n\n const sendDisabled = isSending || (!hasRecording && !isRecording);\n\n return (\n <Wrapper>\n <CancelButton type=\"button\" onClick={onCancel} aria-label={cancelAriaLabel}>\n <Icon28DeleteOutlineAndroid fill=\"var(--exode_red)\" width={24} height={24} style={{ minWidth: 24 }}/>\n </CancelButton>\n\n <Track>\n <CircleButton type=\"button\"\n $variant={recordButtonVariant}\n onClick={onRecordToggle}\n aria-label={recordButtonAria}\n disabled={recordButtonDisabled}>\n {recordButtonIcon}\n </CircleButton>\n\n {!canShowPlayback ? null : (\n <CircleButton type=\"button\"\n $variant=\"play\"\n onClick={onTogglePlayback}\n aria-label={playbackAria}\n disabled={playbackDisabled}>\n {playbackIcon}\n </CircleButton>\n )}\n\n <Waveform $expanded={canShowPlayback}>\n <WaveformBars scale={42}\n minHeight={6}\n divider={100}\n progress={progress}\n values={waveformValues}\n styleInterpolator={({ normalized, isPlayed, progress: p }) => ({\n height: `${12 + normalized / 2}px`,\n maxHeight: '34px',\n opacity: p > 0\n ? (isPlayed ? 0.80 : 0.22)\n : 0.38 + Math.min(0.5, normalized / 120),\n transition: 'opacity 80ms ease',\n })}/>\n </Waveform>\n\n <Timer>\n {displayTime}\n </Timer>\n </Track>\n\n <SendButton type=\"button\" onClick={onSend} disabled={sendDisabled} aria-label={sendAriaLabel}>\n {isSending ? (\n <SpinnerSlot>\n <Spinner/>\n </SpinnerSlot>\n ) : (\n <Icon28SendOutline width={24} height={24}/>\n )}\n </SendButton>\n </Wrapper>\n );\n};\n\n\nexport { RecordingShell };\n"],"names":["_jsx","_jsxs"],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ import styled from 'styled-components';
2
+
3
+ /**
4
+ * RecordingShell styles
5
+ *
6
+ * @author: exode <hello@exode.ru>
7
+ */
8
+ const Wrapper$1 = styled.div `
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 12px;
12
+ padding: 10px 14px;
13
+ border-radius: 9999px;
14
+ background: var(--background_hover, color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent));
15
+ `;
16
+ const CancelButton$1 = styled.button `
17
+ width: 38px;
18
+ height: 38px;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ border-radius: 19px;
23
+ background: color-mix(in srgb, var(--exode_red) 10%, transparent);
24
+ border: 0;
25
+ flex-shrink: 0;
26
+ color: color-mix(in srgb, var(--text_primary, #0a0a0a) 54%, transparent);
27
+ cursor: pointer;
28
+ transition: transform 160ms ease;
29
+
30
+ &:hover:not(:disabled) {
31
+ transform: translateY(-1px);
32
+ }
33
+
34
+ &:active:not(:disabled) {
35
+ transform: translateY(0);
36
+ }
37
+ `;
38
+ const Track = styled.div `
39
+ flex: 1;
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 12px;
43
+ padding: 8px 10px;
44
+ border-radius: 9999px;
45
+ background: color-mix(in srgb, var(--text_primary, #0a0a0a) 10%, transparent);
46
+ `;
47
+ const CircleButton = styled.button `
48
+ width: 38px;
49
+ height: 38px;
50
+ display: inline-flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ border-radius: 19px;
54
+ border: 0;
55
+ cursor: pointer;
56
+ transition: transform 160ms ease, color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
57
+ flex-shrink: 0;
58
+ background: var(--background_content, #ffffff);
59
+
60
+ color: ${({ $variant }) => {
61
+ if ($variant === 'record') {
62
+ return 'var(--exode_red, #ed1c24)';
63
+ }
64
+ if ($variant === 'resume') {
65
+ return 'color-mix(in srgb, var(--accent, #3f8ae0) 86%, white 14%)';
66
+ }
67
+ return 'color-mix(in srgb, var(--text_primary, #0a0a0a) 80%, transparent)';
68
+ }};
69
+
70
+ &:hover:not(:disabled) {
71
+ transform: translateY(-1px);
72
+ }
73
+
74
+ &:active:not(:disabled) {
75
+ transform: translateY(0);
76
+ }
77
+
78
+ &:focus-visible {
79
+ outline: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 70%, transparent);
80
+ outline-offset: 2px;
81
+ }
82
+
83
+ &:disabled {
84
+ cursor: not-allowed;
85
+ opacity: 0.45;
86
+ transform: none;
87
+ }
88
+ `;
89
+ const Waveform = styled.div `
90
+ position: relative;
91
+ flex: ${({ $expanded }) => ($expanded ? 1.4 : 1)};
92
+ height: 34px;
93
+ display: flex;
94
+ align-items: center;
95
+ overflow: hidden;
96
+ `;
97
+ const Timer$1 = styled.span `
98
+ text-align: right;
99
+ font-size: 13px;
100
+ font-weight: 600;
101
+ color: color-mix(in srgb, var(--text_primary, #0a0a0a) 86%, transparent);
102
+ `;
103
+ const SendButton$1 = styled.button `
104
+ width: 40px;
105
+ height: 40px;
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ border-radius: 20px;
110
+ border: 0;
111
+ cursor: pointer;
112
+ background: var(--accent, #3f8ae0);
113
+ color: var(--white, #ffffff);
114
+ transition: transform 160ms ease, opacity 160ms ease;
115
+
116
+ &:hover:not(:disabled) {
117
+ transform: translateY(-1px);
118
+ }
119
+
120
+ &:active:not(:disabled) {
121
+ transform: translateY(0);
122
+ }
123
+
124
+ &:disabled {
125
+ opacity: 0.45;
126
+ cursor: not-allowed;
127
+ }
128
+ `;
129
+ const SpinnerSlot = styled.div `
130
+ width: 22px;
131
+ height: 22px;
132
+ display: inline-flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ `;
136
+ const Spinner = styled.span `
137
+ width: 100%;
138
+ height: 100%;
139
+ display: inline-block;
140
+ box-sizing: border-box;
141
+ border-radius: 50%;
142
+ border: 2px solid rgba(255, 255, 255, 0.3);
143
+ border-top-color: var(--white, #ffffff);
144
+ border-right-color: var(--white, #ffffff);
145
+ animation: audioRecorderSpinner 0.75s linear infinite;
146
+
147
+ @keyframes audioRecorderSpinner {
148
+ from {
149
+ transform: rotate(0deg);
150
+ }
151
+
152
+ to {
153
+ transform: rotate(360deg);
154
+ }
155
+ }
156
+ `;
157
+
158
+ export { CancelButton$1 as CancelButton, CircleButton, SendButton$1 as SendButton, Spinner, SpinnerSlot, Timer$1 as Timer, Track, Waveform, Wrapper$1 as Wrapper };
159
+ //# sourceMappingURL=RecordingShell.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingShell.styled.js","sources":["../../../src/components/RecordingShell/RecordingShell.styled.ts"],"sourcesContent":["/**\n * RecordingShell styles\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport styled from 'styled-components';\n\n\nexport const Wrapper = styled.div`\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 14px;\n border-radius: 9999px;\n background: var(--background_hover, color-mix(in srgb, var(--text_primary, #0a0a0a) 6%, transparent));\n`;\n\nexport const CancelButton = styled.button`\n width: 38px;\n height: 38px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 19px;\n background: color-mix(in srgb, var(--exode_red) 10%, transparent);\n border: 0;\n flex-shrink: 0;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 54%, transparent);\n cursor: pointer;\n transition: transform 160ms ease;\n\n &:hover:not(:disabled) {\n transform: translateY(-1px);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n`;\n\nexport const Track = styled.div`\n flex: 1;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 10px;\n border-radius: 9999px;\n background: color-mix(in srgb, var(--text_primary, #0a0a0a) 10%, transparent);\n`;\n\nexport const CircleButton = styled.button<{ $variant: 'record' | 'resume' | 'play' }>`\n width: 38px;\n height: 38px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 19px;\n border: 0;\n cursor: pointer;\n transition: transform 160ms ease, color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;\n flex-shrink: 0;\n background: var(--background_content, #ffffff);\n\n color: ${({ $variant }) => {\n if ($variant === 'record') {\n return 'var(--exode_red, #ed1c24)';\n }\n\n if ($variant === 'resume') {\n return 'color-mix(in srgb, var(--accent, #3f8ae0) 86%, white 14%)';\n }\n\n return 'color-mix(in srgb, var(--text_primary, #0a0a0a) 80%, transparent)';\n }};\n\n &:hover:not(:disabled) {\n transform: translateY(-1px);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n\n &:focus-visible {\n outline: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 70%, transparent);\n outline-offset: 2px;\n }\n\n &:disabled {\n cursor: not-allowed;\n opacity: 0.45;\n transform: none;\n }\n`;\n\nexport const Waveform = styled.div<{ $expanded: boolean }>`\n position: relative;\n flex: ${({ $expanded }) => ($expanded ? 1.4 : 1)};\n height: 34px;\n display: flex;\n align-items: center;\n overflow: hidden;\n`;\n\nexport const Timer = styled.span`\n text-align: right;\n font-size: 13px;\n font-weight: 600;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 86%, transparent);\n`;\n\nexport const SendButton = styled.button`\n width: 40px;\n height: 40px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 20px;\n border: 0;\n cursor: pointer;\n background: var(--accent, #3f8ae0);\n color: var(--white, #ffffff);\n transition: transform 160ms ease, opacity 160ms ease;\n\n &:hover:not(:disabled) {\n transform: translateY(-1px);\n }\n\n &:active:not(:disabled) {\n transform: translateY(0);\n }\n\n &:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n }\n`;\n\nexport const SpinnerSlot = styled.div`\n width: 22px;\n height: 22px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n`;\n\nexport const Spinner = styled.span`\n width: 100%;\n height: 100%;\n display: inline-block;\n box-sizing: border-box;\n border-radius: 50%;\n border: 2px solid rgba(255, 255, 255, 0.3);\n border-top-color: var(--white, #ffffff);\n border-right-color: var(--white, #ffffff);\n animation: audioRecorderSpinner 0.75s linear infinite;\n\n @keyframes audioRecorderSpinner {\n from {\n transform: rotate(0deg);\n }\n\n to {\n transform: rotate(360deg);\n }\n }\n`;\n"],"names":["Wrapper","CancelButton","Timer","SendButton"],"mappings":";;AAAA;;;;AAIG;AAKI,MAAMA,SAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;AAS1B,MAAMC,cAAY,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;AAuBlC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;AAUxB,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAA4C;;;;;;;;;;;;;AAaxE,WAAA,EAAA,CAAC,EAAE,QAAQ,EAAE,KAAI;AACtB,IAAA,IAAI,QAAQ,KAAK,QAAQ,EAAE;AACvB,QAAA,OAAO,2BAA2B;IACtC;AAEA,IAAA,IAAI,QAAQ,KAAK,QAAQ,EAAE;AACvB,QAAA,OAAO,2DAA2D;IACtE;AAEA,IAAA,OAAO,mEAAmE;AAC9E,CAAC,CAAA;;;;;;;;;;;;;;;;;;;;;AAsBE,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAwB;;AAE9C,UAAA,EAAA,CAAC,EAAE,SAAS,EAAE,MAAM,SAAS,GAAG,GAAG,GAAG,CAAC,CAAC,CAAA;;;;;;AAO7C,MAAMC,OAAK,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;AAOzB,MAAMC,YAAU,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BhC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;AAQ9B,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;;;;;;;;;;;;;;;;;;"}