@exode-team/react-recorder 1.1.14 → 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 (81) hide show
  1. package/dist/components/AudioRecorder.js +50 -0
  2. package/dist/components/AudioRecorder.js.map +1 -0
  3. package/dist/components/AudioRecorder.messages.js +47 -0
  4. package/dist/components/AudioRecorder.messages.js.map +1 -0
  5. package/dist/components/AudioRecorder.styled.js +16 -0
  6. package/dist/components/AudioRecorder.styled.js.map +1 -0
  7. package/dist/components/IdleControl/IdleControl.js +12 -0
  8. package/dist/components/IdleControl/IdleControl.js.map +1 -0
  9. package/dist/components/IdleControl/IdleControl.styled.js +68 -0
  10. package/dist/components/IdleControl/IdleControl.styled.js.map +1 -0
  11. package/dist/components/RecordingShell/RecordingShell.js +80 -0
  12. package/dist/components/RecordingShell/RecordingShell.js.map +1 -0
  13. package/dist/components/RecordingShell/RecordingShell.styled.js +159 -0
  14. package/dist/components/RecordingShell/RecordingShell.styled.js.map +1 -0
  15. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js +151 -0
  16. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js.map +1 -0
  17. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js +92 -0
  18. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js.map +1 -0
  19. package/dist/components/VoiceRecorderInput/PreviewBars.js +52 -0
  20. package/dist/components/VoiceRecorderInput/PreviewBars.js.map +1 -0
  21. package/dist/components/VoiceRecorderInput/TimelineBars.js +31 -0
  22. package/dist/components/VoiceRecorderInput/TimelineBars.js.map +1 -0
  23. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js +51 -0
  24. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js.map +1 -0
  25. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js +175 -0
  26. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js.map +1 -0
  27. package/dist/components/WaveformBars/WaveformBars.js +29 -0
  28. package/dist/components/WaveformBars/WaveformBars.js.map +1 -0
  29. package/dist/components/WaveformBars/WaveformBars.styled.js +20 -0
  30. package/dist/components/WaveformBars/WaveformBars.styled.js.map +1 -0
  31. package/dist/constants.js +17 -0
  32. package/dist/constants.js.map +1 -0
  33. package/dist/hooks/useAudioBlobLoader.js +175 -0
  34. package/dist/hooks/useAudioBlobLoader.js.map +1 -0
  35. package/dist/hooks/useAudioPlayback.js +119 -0
  36. package/dist/hooks/useAudioPlayback.js.map +1 -0
  37. package/dist/hooks/useAudioRecorderHandlers.js +137 -0
  38. package/dist/hooks/useAudioRecorderHandlers.js.map +1 -0
  39. package/dist/hooks/useMediaRecorder.js +127 -0
  40. package/dist/hooks/useMediaRecorder.js.map +1 -0
  41. package/dist/hooks/useRecorderVisualization.js +94 -0
  42. package/dist/hooks/useRecorderVisualization.js.map +1 -0
  43. package/dist/hooks/useRecordingSender.js +53 -0
  44. package/dist/hooks/useRecordingSender.js.map +1 -0
  45. package/dist/hooks/useRecordingTiming.js +93 -0
  46. package/dist/hooks/useRecordingTiming.js.map +1 -0
  47. package/dist/hooks/useVoiceMessagePlayer.js +177 -0
  48. package/dist/hooks/useVoiceMessagePlayer.js.map +1 -0
  49. package/dist/hooks/useVoiceRecorder.js +153 -0
  50. package/dist/hooks/useVoiceRecorder.js.map +1 -0
  51. package/dist/hooks/useVoiceRecorderInput.js +163 -0
  52. package/dist/hooks/useVoiceRecorderInput.js.map +1 -0
  53. package/dist/index.js +13 -2659
  54. package/dist/index.js.map +1 -1
  55. package/dist/platform/animation-frame-adapter.js +14 -0
  56. package/dist/platform/animation-frame-adapter.js.map +1 -0
  57. package/dist/platform/audio-analyzer-adapter.js +74 -0
  58. package/dist/platform/audio-analyzer-adapter.js.map +1 -0
  59. package/dist/platform/audio-blob-fetcher.js +19 -0
  60. package/dist/platform/audio-blob-fetcher.js.map +1 -0
  61. package/dist/platform/audio-playback-adapter.js +114 -0
  62. package/dist/platform/audio-playback-adapter.js.map +1 -0
  63. package/dist/platform/audio-playback-manager.js +26 -0
  64. package/dist/platform/audio-playback-manager.js.map +1 -0
  65. package/dist/platform/media-recorder-adapter.js +23 -0
  66. package/dist/platform/media-recorder-adapter.js.map +1 -0
  67. package/dist/platform/microphone.js +24 -0
  68. package/dist/platform/microphone.js.map +1 -0
  69. package/dist/platform/mpeg-frame.js +60 -0
  70. package/dist/platform/mpeg-frame.js.map +1 -0
  71. package/dist/utils/format.js +13 -0
  72. package/dist/utils/format.js.map +1 -0
  73. package/dist/utils/math.js +16 -0
  74. package/dist/utils/math.js.map +1 -0
  75. package/dist/utils/pseudo-waveform.js +40 -0
  76. package/dist/utils/pseudo-waveform.js.map +1 -0
  77. package/dist/utils/voice-blob-cache.js +35 -0
  78. package/dist/utils/voice-blob-cache.js.map +1 -0
  79. package/dist/utils/voice-playback-chain.js +42 -0
  80. package/dist/utils/voice-playback-chain.js.map +1 -0
  81. package/package.json +2 -1
@@ -0,0 +1,151 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import React, { useRef, useState, useCallback, useImperativeHandle, useEffect, useLayoutEffect, useMemo } from 'react';
3
+ import { Icon24Pause, Icon24Play } from '@vkontakte/icons';
4
+ import { useVoiceMessagePlayer } from '../../hooks/useVoiceMessagePlayer.js';
5
+ import { VoicePlaybackChain } from '../../utils/voice-playback-chain.js';
6
+ import { PlayerWrapper, PlayButton, LoadingSpinner, ContentColumn, WaveformTrack, WaveformBar, Duration } from './VoiceMessagePlayer.styled.js';
7
+
8
+ const BAR_WIDTH = 3;
9
+ const BAR_GAP = 2;
10
+ const BAR_MIN_HEIGHT = 2;
11
+ const BAR_MAX_HEIGHT = 24;
12
+ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
13
+ const { src, onClick, onEnded, chainId, duration, className, waveformData, autoPreload = true, } = props;
14
+ const trackRef = useRef(null);
15
+ const wrapperRef = useRef(null);
16
+ const [trackWidth, setTrackWidth] = useState(0);
17
+ const handleEnded = useCallback(() => {
18
+ onEnded === null || onEnded === void 0 ? void 0 : onEnded();
19
+ if (chainId != null) {
20
+ VoicePlaybackChain.playNext(chainId);
21
+ }
22
+ }, [onEnded, chainId]);
23
+ const handleNextTrack = useCallback(() => {
24
+ if (chainId != null) {
25
+ VoicePlaybackChain.playNext(chainId);
26
+ }
27
+ }, [chainId]);
28
+ const handlePreviousTrack = useCallback(() => {
29
+ if (chainId != null) {
30
+ VoicePlaybackChain.playPrevious(chainId);
31
+ }
32
+ }, [chainId]);
33
+ const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, seekToTime, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
34
+ src,
35
+ duration,
36
+ waveformData,
37
+ preloadedBlob: props.preloadedBlob,
38
+ onEnded: handleEnded,
39
+ onNextTrack: handleNextTrack,
40
+ onPreviousTrack: handlePreviousTrack,
41
+ });
42
+ useImperativeHandle(ref, () => ({ play, prefetch, seekToTime }), [
43
+ play,
44
+ prefetch,
45
+ seekToTime,
46
+ ]);
47
+ /** Auto-preload audio when player enters viewport */
48
+ useEffect(() => {
49
+ if (!autoPreload || isLoaded || isLoading) {
50
+ return;
51
+ }
52
+ const el = wrapperRef.current;
53
+ if (!el) {
54
+ return;
55
+ }
56
+ const observer = new IntersectionObserver(([entry]) => {
57
+ if (entry.isIntersecting) {
58
+ prefetch();
59
+ observer.disconnect();
60
+ }
61
+ }, { threshold: 0.1 });
62
+ observer.observe(el);
63
+ return () => observer.disconnect();
64
+ }, [
65
+ isLoaded,
66
+ prefetch,
67
+ isLoading,
68
+ autoPreload,
69
+ ]);
70
+ /** Register in playback chain for sequential playback */
71
+ useEffect(() => {
72
+ if (chainId != null) {
73
+ return VoicePlaybackChain.register(chainId, () => play());
74
+ }
75
+ }, [chainId, play]);
76
+ const measureTrackWidth = () => {
77
+ if (trackRef.current) {
78
+ setTrackWidth(trackRef.current.clientWidth);
79
+ }
80
+ };
81
+ /** Sync recalc before paint — catches layout shifts */
82
+ useLayoutEffect(measureTrackWidth);
83
+ /** ResizeObserver for external size changes (window resize, container resize) */
84
+ useEffect(() => {
85
+ const el = trackRef.current;
86
+ if (!el) {
87
+ return;
88
+ }
89
+ let rafId = 0;
90
+ const observer = new ResizeObserver(() => {
91
+ cancelAnimationFrame(rafId);
92
+ rafId = requestAnimationFrame(measureTrackWidth);
93
+ });
94
+ observer.observe(el);
95
+ return () => {
96
+ cancelAnimationFrame(rafId);
97
+ observer.disconnect();
98
+ };
99
+ }, []);
100
+ const displayTime = (isPlaying || currentTime > 0)
101
+ ? formatTime(currentTime)
102
+ : formatTime(resolvedDuration);
103
+ const handleTrackClick = useCallback((e) => {
104
+ const track = trackRef.current;
105
+ if (!track) {
106
+ return;
107
+ }
108
+ const rect = track.getBoundingClientRect();
109
+ const clickProgress = (e.clientX - rect.left) / rect.width;
110
+ seekTo(Math.max(0, Math.min(1, clickProgress)));
111
+ }, [seekTo]);
112
+ /** Interpolate waveform levels to fill container */
113
+ const interpolatedBars = useMemo(() => {
114
+ var _a, _b;
115
+ const maxBars = trackWidth > 0
116
+ ? Math.floor(trackWidth / (BAR_WIDTH + BAR_GAP))
117
+ : 48;
118
+ if (!waveformLevels.length) {
119
+ return Array.from({ length: maxBars }, () => 0);
120
+ }
121
+ const raw = [];
122
+ for (let i = 0; i < maxBars; i++) {
123
+ const srcIndex = (i / maxBars) * waveformLevels.length;
124
+ const low = Math.floor(srcIndex);
125
+ const high = Math.min(low + 1, waveformLevels.length - 1);
126
+ const frac = srcIndex - low;
127
+ raw.push(((_a = waveformLevels[low]) !== null && _a !== void 0 ? _a : 0) * (1 - frac)
128
+ + ((_b = waveformLevels[high]) !== null && _b !== void 0 ? _b : 0) * frac);
129
+ }
130
+ /** Normalize to 0-100 range so bars use full height */
131
+ const peak = Math.max(...raw, 1);
132
+ return raw.map((v) => (v / peak) * 100);
133
+ }, [waveformLevels, trackWidth]);
134
+ return (jsxs(PlayerWrapper, { ref: wrapperRef, onClick: onClick, className: className, children: [jsx(PlayButton, { type: "button", disabled: isLoading || (!src && !props.preloadedBlob), onClick: isLoaded ? togglePlayback : preload, "aria-label": isPlaying ? 'Pause' : 'Play', children: isLoading ? (jsx(LoadingSpinner, {})) : isPlaying ? (jsx(Icon24Pause, {})) : (jsx(Icon24Play, {})) }), jsxs(ContentColumn, { children: [jsx(WaveformTrack, { ref: trackRef, onClick: handleTrackClick, children: interpolatedBars.map((level, i) => {
135
+ const normalized = Math.min(100, level);
136
+ const height = BAR_MIN_HEIGHT + (normalized / 100) * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT);
137
+ const barProgress = interpolatedBars.length > 0
138
+ ? i / interpolatedBars.length
139
+ : 0;
140
+ const isPlayed = progress > 0 && barProgress <= progress;
141
+ return (jsx(WaveformBar, { style: {
142
+ height: `${height}px`,
143
+ background: isPlayed
144
+ ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 75%, transparent)'
145
+ : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 25%, transparent)',
146
+ } }, i));
147
+ }) }), jsx(Duration, { children: displayTime })] })] }));
148
+ });
149
+
150
+ export { VoiceMessagePlayer };
151
+ //# sourceMappingURL=VoiceMessagePlayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoiceMessagePlayer.js","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.tsx"],"sourcesContent":["/**\n * VoiceMessagePlayer\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React, { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';\n\nimport { Icon24Pause, Icon24Play } from '@vkontakte/icons';\n\nimport { useVoiceMessagePlayer } from '../../hooks';\n\nimport { VoicePlaybackChain } from '../../utils/voice-playback-chain';\n\nimport {\n ContentColumn,\n Duration,\n LoadingSpinner,\n PlayButton,\n PlayerWrapper,\n WaveformBar,\n WaveformTrack,\n} from './VoiceMessagePlayer.styled';\n\n\nconst BAR_WIDTH = 3;\nconst BAR_GAP = 2;\nconst BAR_MIN_HEIGHT = 2;\nconst BAR_MAX_HEIGHT = 24;\n\nexport interface VoiceMessagePlayerRef {\n play: () => void;\n prefetch: () => void;\n seekToTime: (seconds: number) => void;\n}\n\ninterface Props {\n src: string;\n chainId?: number;\n duration?: number;\n autoPreload?: boolean;\n waveformData?: number[];\n className?: string;\n onEnded?: () => void;\n preloadedBlob?: Blob | null;\n onClick?: (e: React.MouseEvent) => void;\n}\n\n\nconst VoiceMessagePlayer = React.forwardRef<VoiceMessagePlayerRef, Props>((props, ref) => {\n\n const {\n src,\n onClick,\n onEnded,\n chainId,\n duration,\n className,\n waveformData,\n autoPreload = true,\n } = props;\n\n const trackRef = useRef<HTMLDivElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const [ trackWidth, setTrackWidth ] = useState(0);\n\n const handleEnded = useCallback(() => {\n onEnded?.();\n\n if (chainId != null) {\n VoicePlaybackChain.playNext(chainId);\n }\n }, [ onEnded, chainId ]);\n\n const handleNextTrack = useCallback(() => {\n if (chainId != null) {\n VoicePlaybackChain.playNext(chainId);\n }\n }, [ chainId ]);\n\n const handlePreviousTrack = useCallback(() => {\n if (chainId != null) {\n VoicePlaybackChain.playPrevious(chainId);\n }\n }, [ chainId ]);\n\n const {\n play,\n progress,\n isLoaded,\n isPlaying,\n isLoading,\n currentTime,\n waveformLevels,\n seekTo,\n seekToTime,\n prefetch,\n preload,\n formatTime,\n togglePlayback,\n duration: resolvedDuration,\n } = useVoiceMessagePlayer({\n src,\n duration,\n waveformData,\n preloadedBlob: props.preloadedBlob,\n onEnded: handleEnded,\n onNextTrack: handleNextTrack,\n onPreviousTrack: handlePreviousTrack,\n });\n\n useImperativeHandle(\n ref,\n () => ({ play, prefetch, seekToTime }),\n [\n play,\n prefetch,\n seekToTime,\n ],\n );\n\n /** Auto-preload audio when player enters viewport */\n useEffect(() => {\n if (!autoPreload || isLoaded || isLoading) {\n return;\n }\n\n const el = wrapperRef.current;\n\n if (!el) {\n return;\n }\n\n const observer = new IntersectionObserver(\n ([ entry ]) => {\n if (entry.isIntersecting) {\n prefetch();\n observer.disconnect();\n }\n },\n { threshold: 0.1 },\n );\n\n observer.observe(el);\n\n return () => observer.disconnect();\n }, [\n isLoaded,\n prefetch,\n isLoading,\n autoPreload,\n ]);\n\n /** Register in playback chain for sequential playback */\n useEffect(() => {\n if (chainId != null) {\n return VoicePlaybackChain.register(chainId, () => play());\n }\n }, [ chainId, play ]);\n\n const measureTrackWidth = () => {\n if (trackRef.current) {\n setTrackWidth(trackRef.current.clientWidth);\n }\n };\n\n /** Sync recalc before paint — catches layout shifts */\n useLayoutEffect(measureTrackWidth);\n\n /** ResizeObserver for external size changes (window resize, container resize) */\n useEffect(() => {\n const el = trackRef.current;\n if (!el) {\n return;\n }\n\n let rafId = 0;\n\n const observer = new ResizeObserver(() => {\n cancelAnimationFrame(rafId);\n rafId = requestAnimationFrame(measureTrackWidth);\n });\n\n observer.observe(el);\n\n return () => {\n cancelAnimationFrame(rafId);\n observer.disconnect();\n };\n }, []);\n\n const displayTime = (isPlaying || currentTime > 0)\n ? formatTime(currentTime)\n : formatTime(resolvedDuration);\n\n const handleTrackClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n const track = trackRef.current;\n\n if (!track) {\n return;\n }\n\n const rect = track.getBoundingClientRect();\n const clickProgress = (e.clientX - rect.left) / rect.width;\n\n seekTo(Math.max(0, Math.min(1, clickProgress)));\n }, [ seekTo ]);\n\n /** Interpolate waveform levels to fill container */\n const interpolatedBars = useMemo(() => {\n const maxBars = trackWidth > 0\n ? Math.floor(trackWidth / (BAR_WIDTH + BAR_GAP))\n : 48;\n\n if (!waveformLevels.length) {\n return Array.from({ length: maxBars }, () => 0);\n }\n\n const raw: number[] = [];\n\n for (let i = 0; i < maxBars; i++) {\n const srcIndex = (i / maxBars) * waveformLevels.length;\n const low = Math.floor(srcIndex);\n const high = Math.min(low + 1, waveformLevels.length - 1);\n const frac = srcIndex - low;\n\n raw.push(\n (waveformLevels[low] ?? 0) * (1 - frac)\n + (waveformLevels[high] ?? 0) * frac,\n );\n }\n\n /** Normalize to 0-100 range so bars use full height */\n const peak = Math.max(...raw, 1);\n\n return raw.map((v) => (v / peak) * 100);\n }, [ waveformLevels, trackWidth ]);\n\n return (\n <PlayerWrapper ref={wrapperRef} onClick={onClick} className={className}>\n <PlayButton type=\"button\"\n disabled={isLoading || (!src && !props.preloadedBlob)}\n onClick={isLoaded ? togglePlayback : preload}\n aria-label={isPlaying ? 'Pause' : 'Play'}>\n {isLoading ? (\n <LoadingSpinner/>\n ) : isPlaying ? (\n <Icon24Pause/>\n ) : (\n <Icon24Play/>\n )}\n </PlayButton>\n\n <ContentColumn>\n <WaveformTrack ref={trackRef} onClick={handleTrackClick}>\n {interpolatedBars.map((level, i) => {\n const normalized = Math.min(100, level);\n const height = BAR_MIN_HEIGHT + (normalized / 100) * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT);\n const barProgress = interpolatedBars.length > 0\n ? i / interpolatedBars.length\n : 0;\n const isPlayed = progress > 0 && barProgress <= progress;\n\n return (\n <WaveformBar key={i} style={{\n height: `${height}px`,\n background: isPlayed\n ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 75%, transparent)'\n : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 25%, transparent)',\n }}/>\n );\n })}\n </WaveformTrack>\n\n <Duration>{displayTime}</Duration>\n </ContentColumn>\n </PlayerWrapper>\n );\n});\n\n\nexport { VoiceMessagePlayer };\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import styled from 'styled-components';
2
+
3
+ /**
4
+ * VoiceMessagePlayer styles
5
+ *
6
+ * @author: exode <hello@exode.ru>
7
+ */
8
+ const PlayerWrapper = styled.div `
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 8px;
12
+ min-width: 200px;
13
+ padding: 2px 0;
14
+ user-select: none;
15
+ `;
16
+ const PlayButton = styled.button `
17
+ width: 44px;
18
+ height: 44px;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ border-radius: 22px;
23
+ border: 0;
24
+ cursor: pointer;
25
+ flex-shrink: 0;
26
+ background: var(--white, #ffffff);
27
+ color: var(--accent, #3f8ae0);
28
+ transition: transform 140ms ease, box-shadow 140ms ease;
29
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
30
+ padding: 0;
31
+
32
+ &:hover:not(:disabled) {
33
+ transform: scale(1.04);
34
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
35
+ }
36
+
37
+ &:active:not(:disabled) {
38
+ transform: scale(0.97);
39
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.08);
40
+ }
41
+
42
+ &:disabled {
43
+ opacity: 0.5;
44
+ cursor: not-allowed;
45
+ }
46
+ `;
47
+ const ContentColumn = styled.div `
48
+ display: flex;
49
+ flex-direction: column;
50
+ flex: 1;
51
+ min-width: 0;
52
+ gap: 4px;
53
+ `;
54
+ const WaveformTrack = styled.div `
55
+ height: 28px;
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 2px;
59
+ overflow: hidden;
60
+ cursor: pointer;
61
+ min-width: 0;
62
+ `;
63
+ const WaveformBar = styled.div `
64
+ width: 3px;
65
+ border-radius: 1.5px;
66
+ flex-shrink: 0;
67
+ transition: background 100ms ease;
68
+ `;
69
+ const Duration = styled.span `
70
+ font-size: 12px;
71
+ font-weight: 500;
72
+ line-height: 1;
73
+ color: color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent);
74
+ font-variant-numeric: tabular-nums;
75
+ `;
76
+ const LoadingSpinner = styled.span `
77
+ width: 18px;
78
+ height: 18px;
79
+ display: inline-block;
80
+ border-radius: 50%;
81
+ border: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 25%, transparent);
82
+ border-top-color: var(--accent, #3f8ae0);
83
+ animation: voicePlayerSpin 0.7s linear infinite;
84
+
85
+ @keyframes voicePlayerSpin {
86
+ from { transform: rotate(0deg); }
87
+ to { transform: rotate(360deg); }
88
+ }
89
+ `;
90
+
91
+ export { ContentColumn, Duration, LoadingSpinner, PlayButton, PlayerWrapper, WaveformBar, WaveformTrack };
92
+ //# sourceMappingURL=VoiceMessagePlayer.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoiceMessagePlayer.styled.js","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.ts"],"sourcesContent":["/**\n * VoiceMessagePlayer styles\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport styled from 'styled-components';\n\n\nexport const PlayerWrapper = styled.div`\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 200px;\n padding: 2px 0;\n user-select: none;\n`;\n\nexport const PlayButton = styled.button`\n width: 44px;\n height: 44px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 22px;\n border: 0;\n cursor: pointer;\n flex-shrink: 0;\n background: var(--white, #ffffff);\n color: var(--accent, #3f8ae0);\n transition: transform 140ms ease, box-shadow 140ms ease;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n padding: 0;\n\n &:hover:not(:disabled) {\n transform: scale(1.04);\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);\n }\n\n &:active:not(:disabled) {\n transform: scale(0.97);\n box-shadow: 0 0 2px rgba(0, 0, 0, 0.08);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n`;\n\nexport const ContentColumn = styled.div`\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 0;\n gap: 4px;\n`;\n\nexport const WaveformTrack = styled.div`\n height: 28px;\n display: flex;\n align-items: center;\n gap: 2px;\n overflow: hidden;\n cursor: pointer;\n min-width: 0;\n`;\n\nexport const WaveformBar = styled.div`\n width: 3px;\n border-radius: 1.5px;\n flex-shrink: 0;\n transition: background 100ms ease;\n`;\n\nexport const Duration = styled.span`\n font-size: 12px;\n font-weight: 500;\n line-height: 1;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent);\n font-variant-numeric: tabular-nums;\n`;\n\nexport const LoadingSpinner = styled.span`\n width: 18px;\n height: 18px;\n display: inline-block;\n border-radius: 50%;\n border: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 25%, transparent);\n border-top-color: var(--accent, #3f8ae0);\n animation: voicePlayerSpin 0.7s linear infinite;\n\n @keyframes voicePlayerSpin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n }\n`;"],"names":[],"mappings":";;AAAA;;;;AAIG;AAKI,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;AAShC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgChC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;AAQhC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;AAUhC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;AAO9B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;AAQ5B,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,52 @@
1
+ import { jsx, Fragment } from 'react/jsx-runtime';
2
+ import 'react';
3
+ import { TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_WIDTH, TIMELINE_BAR_MAX_HEIGHT } from '../../constants.js';
4
+
5
+ const PreviewBars = (props) => {
6
+ var _a, _b;
7
+ const { isPlaying, playbackTime, lastDuration, previewLevels, recordingTime, timelineLevels, visibleBarsCount, } = props;
8
+ const maxBars = visibleBarsCount;
9
+ const sourceLevels = timelineLevels.length > 0
10
+ ? timelineLevels
11
+ : previewLevels;
12
+ if (!sourceLevels.length) {
13
+ return (jsx(Fragment, { children: Array.from({ length: maxBars }, (_, i) => (jsx("div", { style: {
14
+ width: `${TIMELINE_BAR_WIDTH}px`,
15
+ height: `${TIMELINE_BAR_MIN_HEIGHT}px`,
16
+ borderRadius: '9999px',
17
+ flexShrink: 0,
18
+ background: 'color-mix(in srgb, var(--text_primary, #0a0a0a) 15%, transparent)',
19
+ } }, i))) }));
20
+ }
21
+ /** Interpolate source levels to fill container */
22
+ const bars = [];
23
+ for (let i = 0; i < maxBars; i++) {
24
+ const srcIndex = (i / maxBars) * sourceLevels.length;
25
+ const low = Math.floor(srcIndex);
26
+ const high = Math.min(low + 1, sourceLevels.length - 1);
27
+ const frac = srcIndex - low;
28
+ bars.push(((_a = sourceLevels[low]) !== null && _a !== void 0 ? _a : 0) * (1 - frac) + ((_b = sourceLevels[high]) !== null && _b !== void 0 ? _b : 0) * frac);
29
+ }
30
+ const totalDuration = recordingTime || lastDuration || 1;
31
+ const progress = isPlaying ? Math.min(1, playbackTime / totalDuration) : 0;
32
+ return (jsx(Fragment, { children: bars.map((level, i) => {
33
+ const normalized = Math.min(100, level);
34
+ const height = TIMELINE_BAR_MIN_HEIGHT + (normalized / 100) * (TIMELINE_BAR_MAX_HEIGHT - TIMELINE_BAR_MIN_HEIGHT);
35
+ const isPlayed = progress > 0 && (i / maxBars) <= progress;
36
+ return (jsx("div", { style: {
37
+ flexShrink: 0,
38
+ height: `${height}px`,
39
+ borderRadius: '9999px',
40
+ width: `${TIMELINE_BAR_WIDTH}px`,
41
+ background: !isPlaying
42
+ ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent)'
43
+ : isPlayed
44
+ ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 80%, transparent)'
45
+ : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 22%, transparent)',
46
+ transition: 'background 80ms ease',
47
+ } }, i));
48
+ }) }));
49
+ };
50
+
51
+ export { PreviewBars };
52
+ //# sourceMappingURL=PreviewBars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PreviewBars.js","sources":["../../../src/components/VoiceRecorderInput/PreviewBars.tsx"],"sourcesContent":["/**\n * PreviewBars — renders waveform bars in preview mode (paused/stopped)\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React from 'react';\n\nimport { TIMELINE_BAR_MAX_HEIGHT, TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_WIDTH } from '../../constants';\n\n\ninterface PreviewBarsProps {\n isPlaying: boolean;\n playbackTime: number;\n lastDuration: number;\n recordingTime: number;\n previewLevels: number[];\n timelineLevels: number[];\n visibleBarsCount: number;\n}\n\n\nconst PreviewBars = (props: PreviewBarsProps) => {\n\n const {\n isPlaying,\n playbackTime,\n lastDuration,\n previewLevels,\n recordingTime,\n timelineLevels,\n visibleBarsCount,\n } = props;\n\n const maxBars = visibleBarsCount;\n\n const sourceLevels = timelineLevels.length > 0\n ? timelineLevels\n : previewLevels;\n\n if (!sourceLevels.length) {\n return (\n <>\n {Array.from({ length: maxBars }, (_, i) => (\n <div key={i} style={{\n width: `${TIMELINE_BAR_WIDTH}px`,\n height: `${TIMELINE_BAR_MIN_HEIGHT}px`,\n borderRadius: '9999px',\n flexShrink: 0,\n background: 'color-mix(in srgb, var(--text_primary, #0a0a0a) 15%, transparent)',\n }}/>\n ))}\n </>\n );\n }\n\n /** Interpolate source levels to fill container */\n const bars: number[] = [];\n\n for (let i = 0; i < maxBars; i++) {\n const srcIndex = (i / maxBars) * sourceLevels.length;\n const low = Math.floor(srcIndex);\n\n const high = Math.min(low + 1, sourceLevels.length - 1);\n const frac = srcIndex - low;\n\n bars.push((sourceLevels[low] ?? 0) * (1 - frac) + (sourceLevels[high] ?? 0) * frac);\n }\n\n const totalDuration = recordingTime || lastDuration || 1;\n const progress = isPlaying ? Math.min(1, playbackTime / totalDuration) : 0;\n\n return (\n <>\n {bars.map((level, i) => {\n const normalized = Math.min(100, level);\n const height = TIMELINE_BAR_MIN_HEIGHT + (normalized / 100) * (TIMELINE_BAR_MAX_HEIGHT - TIMELINE_BAR_MIN_HEIGHT);\n const isPlayed = progress > 0 && (i / maxBars) <= progress;\n\n return (\n <div key={i} style={{\n flexShrink: 0,\n height: `${height}px`,\n borderRadius: '9999px',\n width: `${TIMELINE_BAR_WIDTH}px`,\n background: !isPlaying\n ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent)'\n : isPlayed\n ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 80%, transparent)'\n : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 22%, transparent)',\n transition: 'background 80ms ease',\n }}/>\n );\n })}\n </>\n );\n};\n\n\nexport { PreviewBars };\n"],"names":["_jsx","_Fragment"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { jsx, Fragment } from 'react/jsx-runtime';
2
+ import 'react';
3
+ import { TIMELINE_BAR_WIDTH, TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_MAX_HEIGHT } from '../../constants.js';
4
+
5
+ const TimelineBars = (props) => {
6
+ const { timelineLevels, visibleBarsCount } = props;
7
+ const maxBars = visibleBarsCount;
8
+ const visible = timelineLevels.slice(-maxBars);
9
+ const padded = [
10
+ ...visible,
11
+ ...Array.from({ length: Math.max(0, maxBars - visible.length) }, () => 0),
12
+ ];
13
+ return (jsx(Fragment, { children: padded.map((level, i) => {
14
+ const normalized = Math.min(100, level);
15
+ const height = TIMELINE_BAR_MIN_HEIGHT + (normalized / 100) * (TIMELINE_BAR_MAX_HEIGHT - TIMELINE_BAR_MIN_HEIGHT);
16
+ const hasValue = i < visible.length;
17
+ return (jsx("div", { style: {
18
+ width: `${TIMELINE_BAR_WIDTH}px`,
19
+ height: `${height}px`,
20
+ borderRadius: '9999px',
21
+ flexShrink: 0,
22
+ transition: 'height 120ms ease',
23
+ background: hasValue
24
+ ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent)'
25
+ : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 18%, transparent)',
26
+ } }, i));
27
+ }) }));
28
+ };
29
+
30
+ export { TimelineBars };
31
+ //# sourceMappingURL=TimelineBars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimelineBars.js","sources":["../../../src/components/VoiceRecorderInput/TimelineBars.tsx"],"sourcesContent":["/**\n * TimelineBars — renders waveform bars during active recording\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React from 'react';\n\nimport { TIMELINE_BAR_MAX_HEIGHT, TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_WIDTH } from '../../constants';\n\n\ninterface TimelineBarsProps {\n timelineLevels: number[];\n visibleBarsCount: number;\n}\n\n\nconst TimelineBars = (props: TimelineBarsProps) => {\n\n const { timelineLevels, visibleBarsCount } = props;\n\n const maxBars = visibleBarsCount;\n const visible = timelineLevels.slice(-maxBars);\n\n const padded = [\n ...visible,\n ...Array.from({ length: Math.max(0, maxBars - visible.length) }, () => 0),\n ];\n\n return (\n <>\n {padded.map((level, i) => {\n const normalized = Math.min(100, level);\n const height = TIMELINE_BAR_MIN_HEIGHT + (normalized / 100) * (TIMELINE_BAR_MAX_HEIGHT - TIMELINE_BAR_MIN_HEIGHT);\n const hasValue = i < visible.length;\n\n return (\n <div key={i} style={{\n width: `${TIMELINE_BAR_WIDTH}px`,\n height: `${height}px`,\n borderRadius: '9999px',\n flexShrink: 0,\n transition: 'height 120ms ease',\n background: hasValue\n ? 'color-mix(in srgb, var(--text_primary, #0a0a0a) 55%, transparent)'\n : 'color-mix(in srgb, var(--text_primary, #0a0a0a) 18%, transparent)',\n }}/>\n );\n })}\n </>\n );\n};\n\n\nexport { TimelineBars };\n"],"names":["_jsx","_Fragment"],"mappings":";;;;AAiBA,MAAM,YAAY,GAAG,CAAC,KAAwB,KAAI;AAE9C,IAAA,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,KAAK;IAElD,MAAM,OAAO,GAAG,gBAAgB;IAChC,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;AAE9C,IAAA,MAAM,MAAM,GAAG;AACX,QAAA,GAAG,OAAO;QACV,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;KAC5E;IAED,QACIA,GAAA,CAAAC,QAAA,EAAA,EAAA,QAAA,EACK,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,KAAI;YACrB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC;AACvC,YAAA,MAAM,MAAM,GAAG,uBAAuB,GAAG,CAAC,UAAU,GAAG,GAAG,KAAK,uBAAuB,GAAG,uBAAuB,CAAC;AACjH,YAAA,MAAM,QAAQ,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM;YAEnC,QACID,GAAA,CAAA,KAAA,EAAA,EAAa,KAAK,EAAE;oBAChB,KAAK,EAAE,CAAA,EAAG,kBAAkB,CAAA,EAAA,CAAI;oBAChC,MAAM,EAAE,CAAA,EAAG,MAAM,CAAA,EAAA,CAAI;AACrB,oBAAA,YAAY,EAAE,QAAQ;AACtB,oBAAA,UAAU,EAAE,CAAC;AACb,oBAAA,UAAU,EAAE,mBAAmB;AAC/B,oBAAA,UAAU,EAAE;AACR,0BAAE;AACF,0BAAE,mEAAmE;iBAC5E,EAAA,EATS,CAAC,CASP;QAEZ,CAAC,CAAC,EAAA,CACH;AAEX;;;;"}
@@ -0,0 +1,51 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { useRef, useState, useLayoutEffect, useEffect } from 'react';
3
+ import { Icon28DeleteOutlineAndroid, Icon20Voice, Icon16Pause, Icon16Play, Icon28SendOutline } from '@vkontakte/icons';
4
+ import { TIMELINE_BAR_WIDTH, TIMELINE_BAR_GAP } from '../../constants.js';
5
+ import { useVoiceRecorderInput } from '../../hooks/useVoiceRecorderInput.js';
6
+ import { PreviewBars } from './PreviewBars.js';
7
+ import { TimelineBars } from './TimelineBars.js';
8
+ import { Wrapper, CancelButton, RecordTrack, ButtonSlot, MicIndicator, PlayPauseButton, StopButton, WaveformArea, Timer, SendButton, SpinnerIcon } from './VoiceRecorderInput.styled.js';
9
+
10
+ const VoiceRecorderInput = (props) => {
11
+ const { className, ...inputOptions } = props;
12
+ const waveformRef = useRef(null);
13
+ const { recorder, isSending, isPreview, timelineLevels, lastDurationRef, handleResume, handlePause, handleCancel, handleSendRequest, } = useVoiceRecorderInput(inputOptions);
14
+ const { isPlaying, isRecording, recordingTime, hasRecording, previewLevels, playbackTime, formatTime, togglePlayback, } = recorder;
15
+ const [visibleBarsCount, setVisibleBarsCount] = useState(0);
16
+ const measureBars = () => {
17
+ const el = waveformRef.current;
18
+ if (!el) {
19
+ return;
20
+ }
21
+ setVisibleBarsCount(Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)));
22
+ };
23
+ /** Sync recalc before paint — catches layout shifts from button changes */
24
+ useLayoutEffect(measureBars);
25
+ /** ResizeObserver for external size changes (window resize, container resize) */
26
+ useEffect(() => {
27
+ const el = waveformRef.current;
28
+ if (!el) {
29
+ return;
30
+ }
31
+ let rafId = 0;
32
+ const observer = new ResizeObserver(() => {
33
+ cancelAnimationFrame(rafId);
34
+ rafId = requestAnimationFrame(measureBars);
35
+ });
36
+ observer.observe(el);
37
+ return () => {
38
+ cancelAnimationFrame(rafId);
39
+ observer.disconnect();
40
+ };
41
+ }, []);
42
+ const displayTime = (isPreview && isPlaying)
43
+ ? formatTime(playbackTime)
44
+ : formatTime(recordingTime || lastDurationRef.current);
45
+ 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
46
+ ? jsx(Icon16Pause, { width: 14, height: 14 })
47
+ : 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, {})) })] }));
48
+ };
49
+
50
+ export { VoiceRecorderInput };
51
+ //# sourceMappingURL=VoiceRecorderInput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoiceRecorderInput.js","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.tsx"],"sourcesContent":["/**\n * VoiceRecorderInput\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React, { useEffect, useLayoutEffect, useRef, useState } from 'react';\n\nimport { Icon16Pause, Icon16Play, Icon20Voice, Icon28DeleteOutlineAndroid, Icon28SendOutline } from '@vkontakte/icons';\n\nimport type { UploadStrategy } from '../../types';\nimport { TIMELINE_BAR_GAP, TIMELINE_BAR_WIDTH } from '../../constants';\n\nimport { useVoiceRecorderInput } from '../../hooks/useVoiceRecorderInput';\n\nimport { PreviewBars } from './PreviewBars';\nimport { TimelineBars } from './TimelineBars';\n\nimport {\n ButtonSlot,\n CancelButton,\n MicIndicator,\n PlayPauseButton,\n RecordTrack,\n SendButton,\n SpinnerIcon,\n StopButton,\n Timer,\n WaveformArea,\n Wrapper,\n} from './VoiceRecorderInput.styled';\n\n\ninterface Props {\n uploader?: UploadStrategy<unknown>;\n onUploaded?: (file: unknown) => void | Promise<void>;\n onCancel?: () => void;\n onSend?: (file: File, duration: number) => void | Promise<void>;\n onError?: (error: unknown) => void;\n onPermissionDenied?: (error: unknown) => void;\n autoStart?: boolean;\n mimeType?: string;\n fileName?: string;\n className?: string;\n}\n\n\nconst VoiceRecorderInput = (props: Props) => {\n\n const { className, ...inputOptions } = props;\n\n const waveformRef = useRef<HTMLDivElement>(null);\n\n const {\n recorder,\n isSending,\n isPreview,\n timelineLevels,\n lastDurationRef,\n handleResume,\n handlePause,\n handleCancel,\n handleSendRequest,\n } = useVoiceRecorderInput(inputOptions);\n\n const {\n isPlaying,\n isRecording,\n recordingTime,\n hasRecording,\n previewLevels,\n playbackTime,\n formatTime,\n togglePlayback,\n } = recorder;\n\n const [ visibleBarsCount, setVisibleBarsCount ] = useState(0);\n\n const measureBars = () => {\n const el = waveformRef.current;\n if (!el) {\n return;\n }\n setVisibleBarsCount(\n Math.floor(el.clientWidth / (TIMELINE_BAR_WIDTH + TIMELINE_BAR_GAP)),\n );\n };\n\n /** Sync recalc before paint — catches layout shifts from button changes */\n useLayoutEffect(measureBars);\n\n /** ResizeObserver for external size changes (window resize, container resize) */\n useEffect(() => {\n const el = waveformRef.current;\n if (!el) {\n return;\n }\n\n let rafId = 0;\n\n const observer = new ResizeObserver(() => {\n cancelAnimationFrame(rafId);\n rafId = requestAnimationFrame(measureBars);\n });\n\n observer.observe(el);\n\n return () => {\n cancelAnimationFrame(rafId);\n observer.disconnect();\n };\n }, []);\n\n const displayTime = (isPreview && isPlaying)\n ? formatTime(playbackTime)\n : formatTime(recordingTime || lastDurationRef.current);\n\n return (\n <Wrapper className={className}>\n <CancelButton type=\"button\" onClick={handleCancel} aria-label=\"Cancel\">\n <Icon28DeleteOutlineAndroid fill=\"var(--exode_red)\" width={28} height={28} style={{ minWidth: 28 }}/>\n </CancelButton>\n\n <RecordTrack>\n <ButtonSlot>\n {isPreview ? (\n <>\n <MicIndicator type=\"button\" onClick={handleResume} aria-label=\"Resume recording\">\n <Icon20Voice width={14} height={14}/>\n </MicIndicator>\n\n <PlayPauseButton type=\"button\"\n onClick={togglePlayback}\n aria-label={isPlaying ? 'Pause' : 'Play'}>\n {isPlaying\n ? <Icon16Pause width={14} height={14}/>\n : <Icon16Play width={14} height={14}/>\n }\n </PlayPauseButton>\n </>\n ) : (\n <StopButton type=\"button\" onClick={handlePause} aria-label=\"Pause recording\">\n <Icon16Pause width={14} height={14}/>\n </StopButton>\n )}\n </ButtonSlot>\n\n <WaveformArea ref={waveformRef}>\n {isPreview ? (\n <PreviewBars timelineLevels={timelineLevels}\n previewLevels={previewLevels}\n isPlaying={isPlaying}\n playbackTime={playbackTime}\n recordingTime={recordingTime}\n lastDuration={lastDurationRef.current}\n visibleBarsCount={visibleBarsCount}/>\n ) : (\n <TimelineBars timelineLevels={timelineLevels} visibleBarsCount={visibleBarsCount}/>\n )}\n </WaveformArea>\n\n <Timer>\n {displayTime}\n </Timer>\n </RecordTrack>\n\n <SendButton type=\"button\"\n onClick={handleSendRequest}\n disabled={isSending || (!hasRecording && !isRecording)}\n aria-label=\"Send\">\n {isSending ? (\n <SpinnerIcon/>\n ) : (\n <Icon28SendOutline/>\n )}\n </SendButton>\n </Wrapper>\n );\n};\n\n\nexport { VoiceRecorderInput };\n"],"names":["_jsxs","_jsx","_Fragment"],"mappings":"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"}
@@ -0,0 +1,175 @@
1
+ import styled from 'styled-components';
2
+
3
+ /**
4
+ * VoiceRecorderInput styles
5
+ *
6
+ * @author: exode <hello@exode.ru>
7
+ */
8
+ const Wrapper = styled.div `
9
+ display: flex;
10
+ align-items: center;
11
+ width: 100%;
12
+ gap: 6px;
13
+ padding: 0 4px;
14
+ color: var(--text_primary, #0a0a0a);
15
+ `;
16
+ const CancelButton = styled.button `
17
+ width: 32px;
18
+ height: 32px;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ border-radius: 16px;
23
+ background: transparent;
24
+ border: 0;
25
+ margin-right: 4px;
26
+ flex-shrink: 0;
27
+ color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);
28
+ cursor: pointer;
29
+ transition: color 160ms ease, background-color 160ms ease;
30
+ `;
31
+ const RecordTrack = styled.div `
32
+ flex: 1;
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 4px;
36
+ padding: 4px 10px 4px 4px;
37
+ border-radius: 9999px;
38
+ background: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);
39
+ min-width: 0;
40
+ `;
41
+ /** Container for control buttons — auto-sized, no fixed width */
42
+ const ButtonSlot = styled.div `
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 4px;
46
+ padding: 2px;
47
+ flex-shrink: 0;
48
+ `;
49
+ const StopButton = styled.button `
50
+ width: 26px;
51
+ height: 26px;
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ border-radius: 13px;
56
+ border: 0;
57
+ flex-shrink: 0;
58
+ cursor: pointer;
59
+ background: var(--background_content, #ffffff);
60
+ color: var(--exode_red, #ed1c24);
61
+ transition: transform 120ms ease;
62
+
63
+ &:hover:not(:disabled) {
64
+ transform: scale(1.05);
65
+ }
66
+
67
+ &:active:not(:disabled) {
68
+ transform: scale(0.96);
69
+ }
70
+
71
+ &:disabled {
72
+ opacity: 0.4;
73
+ cursor: not-allowed;
74
+ }
75
+ `;
76
+ const WaveformArea = styled.div `
77
+ flex: 1;
78
+ height: 28px;
79
+ display: flex;
80
+ align-items: flex-end;
81
+ justify-content: flex-start;
82
+ gap: 2px;
83
+ overflow: hidden;
84
+ min-width: 0;
85
+ padding-bottom: 2px;
86
+ `;
87
+ const Timer = styled.span `
88
+ font-size: 13px;
89
+ font-weight: 500;
90
+ flex-shrink: 0;
91
+ color: color-mix(in srgb, var(--text_primary, #0a0a0a) 60%, transparent);
92
+ font-variant-numeric: tabular-nums;
93
+ `;
94
+ const SendButton = styled.button `
95
+ width: 32px;
96
+ height: 32px;
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ border-radius: 16px;
101
+ border: 0;
102
+ flex-shrink: 0;
103
+ cursor: pointer;
104
+ background: transparent;
105
+ color: var(--accent, #3f8ae0);
106
+ transition: opacity 140ms ease;
107
+ padding: 0;
108
+
109
+ &:hover:not(:disabled) {
110
+ opacity: 0.75;
111
+ }
112
+
113
+ &:disabled {
114
+ opacity: 0.35;
115
+ cursor: not-allowed;
116
+ }
117
+ `;
118
+ const MicIndicator = styled.button `
119
+ width: 26px;
120
+ height: 26px;
121
+ display: inline-flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ border-radius: 13px;
125
+ border: 0;
126
+ flex-shrink: 0;
127
+ cursor: pointer;
128
+ background: var(--background_content, #ffffff);
129
+ color: color-mix(in srgb, var(--accent, #3f8ae0) 86%, white 14%);
130
+ transition: background-color 140ms ease, transform 120ms ease;
131
+
132
+ &:active {
133
+ transform: scale(0.94);
134
+ }
135
+ `;
136
+ const PlayPauseButton = styled.button `
137
+ width: 26px;
138
+ height: 26px;
139
+ display: inline-flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ border-radius: 13px;
143
+ border: 0;
144
+ flex-shrink: 0;
145
+ cursor: pointer;
146
+ background: var(--background_content, #ffffff);
147
+ color: var(--text_primary, #0a0a0a);
148
+ transition: transform 120ms ease;
149
+
150
+ &:hover {
151
+ transform: scale(1.06);
152
+ }
153
+
154
+ &:active {
155
+ transform: scale(0.94);
156
+ }
157
+ `;
158
+ const SpinnerIcon = styled.span `
159
+ width: 20px;
160
+ height: 20px;
161
+ display: inline-block;
162
+ border-radius: 50%;
163
+ border: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 25%, transparent);
164
+ border-top-color: var(--accent, #3f8ae0);
165
+ border-right-color: var(--accent, #3f8ae0);
166
+ animation: voiceRecorderSpin 0.75s linear infinite;
167
+
168
+ @keyframes voiceRecorderSpin {
169
+ from { transform: rotate(0deg); }
170
+ to { transform: rotate(360deg); }
171
+ }
172
+ `;
173
+
174
+ export { ButtonSlot, CancelButton, MicIndicator, PlayPauseButton, RecordTrack, SendButton, SpinnerIcon, StopButton, Timer, WaveformArea, Wrapper };
175
+ //# sourceMappingURL=VoiceRecorderInput.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoiceRecorderInput.styled.js","sources":["../../../src/components/VoiceRecorderInput/VoiceRecorderInput.styled.ts"],"sourcesContent":["/**\n * VoiceRecorderInput 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 width: 100%;\n gap: 6px;\n padding: 0 4px;\n color: var(--text_primary, #0a0a0a);\n`;\n\nexport const CancelButton = styled.button`\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 16px;\n background: transparent;\n border: 0;\n margin-right: 4px;\n flex-shrink: 0;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 45%, transparent);\n cursor: pointer;\n transition: color 160ms ease, background-color 160ms ease;\n`;\n\nexport const RecordTrack = styled.div`\n flex: 1;\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 10px 4px 4px;\n border-radius: 9999px;\n background: color-mix(in srgb, var(--text_primary, #0a0a0a) 8%, transparent);\n min-width: 0;\n`;\n\n/** Container for control buttons — auto-sized, no fixed width */\nexport const ButtonSlot = styled.div`\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n flex-shrink: 0;\n`;\n\nexport const StopButton = styled.button`\n width: 26px;\n height: 26px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 13px;\n border: 0;\n flex-shrink: 0;\n cursor: pointer;\n background: var(--background_content, #ffffff);\n color: var(--exode_red, #ed1c24);\n transition: transform 120ms ease;\n\n &:hover:not(:disabled) {\n transform: scale(1.05);\n }\n\n &:active:not(:disabled) {\n transform: scale(0.96);\n }\n\n &:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n`;\n\nexport const WaveformArea = styled.div`\n flex: 1;\n height: 28px;\n display: flex;\n align-items: flex-end;\n justify-content: flex-start;\n gap: 2px;\n overflow: hidden;\n min-width: 0;\n padding-bottom: 2px;\n`;\n\nexport const Timer = styled.span`\n font-size: 13px;\n font-weight: 500;\n flex-shrink: 0;\n color: color-mix(in srgb, var(--text_primary, #0a0a0a) 60%, transparent);\n font-variant-numeric: tabular-nums;\n`;\n\nexport const SendButton = styled.button`\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 16px;\n border: 0;\n flex-shrink: 0;\n cursor: pointer;\n background: transparent;\n color: var(--accent, #3f8ae0);\n transition: opacity 140ms ease;\n padding: 0;\n\n &:hover:not(:disabled) {\n opacity: 0.75;\n }\n\n &:disabled {\n opacity: 0.35;\n cursor: not-allowed;\n }\n`;\n\nexport const MicIndicator = styled.button`\n width: 26px;\n height: 26px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 13px;\n border: 0;\n flex-shrink: 0;\n cursor: pointer;\n background: var(--background_content, #ffffff);\n color: color-mix(in srgb, var(--accent, #3f8ae0) 86%, white 14%);\n transition: background-color 140ms ease, transform 120ms ease;\n\n &:active {\n transform: scale(0.94);\n }\n`;\n\nexport const PlayPauseButton = styled.button`\n width: 26px;\n height: 26px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 13px;\n border: 0;\n flex-shrink: 0;\n cursor: pointer;\n background: var(--background_content, #ffffff);\n color: var(--text_primary, #0a0a0a);\n transition: transform 120ms ease;\n\n &:hover {\n transform: scale(1.06);\n }\n\n &:active {\n transform: scale(0.94);\n }\n`;\n\nexport const SpinnerIcon = styled.span`\n width: 20px;\n height: 20px;\n display: inline-block;\n border-radius: 50%;\n border: 2px solid color-mix(in srgb, var(--accent, #3f8ae0) 25%, transparent);\n border-top-color: var(--accent, #3f8ae0);\n border-right-color: var(--accent, #3f8ae0);\n animation: voiceRecorderSpin 0.75s linear infinite;\n\n @keyframes voiceRecorderSpin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n }\n`;\n"],"names":[],"mappings":";;AAAA;;;;AAIG;AAKI,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;AAS1B,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;AAgBlC,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;AAWrC;AACO,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;AAQ7B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;AAY/B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;AAQzB,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;AAyBhC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;AAmBlC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;AAuBrC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAA;;;;;;;;;;;;;;;;;;"}