@fanvue/ui 3.15.0 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +16 -89
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs +54 -0
- package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs.map +1 -0
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs +5 -5
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs.map +1 -1
- package/dist/cjs/components/ChatInput/ChatInput.cjs +6 -11
- package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -1
- package/dist/cjs/components/ChatMessage/ChatMessage.cjs +228 -0
- package/dist/cjs/components/ChatMessage/ChatMessage.cjs.map +1 -0
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +160 -71
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/cjs/components/Icons/DenseGridViewIcon.cjs +52 -0
- package/dist/cjs/components/Icons/DenseGridViewIcon.cjs.map +1 -0
- package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs +66 -0
- package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs.map +1 -0
- package/dist/cjs/components/Select/Select.cjs +61 -17
- package/dist/cjs/components/Select/Select.cjs.map +1 -1
- package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs +53 -0
- package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs.map +1 -0
- package/dist/cjs/components/VoiceNote/VoiceNote.cjs +305 -0
- package/dist/cjs/components/VoiceNote/VoiceNote.cjs.map +1 -0
- package/dist/cjs/index.cjs +12 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/utils/audioWaveform.cjs +76 -0
- package/dist/cjs/utils/audioWaveform.cjs.map +1 -0
- package/dist/cjs/utils/useAudioPlayback.cjs +123 -0
- package/dist/cjs/utils/useAudioPlayback.cjs.map +1 -0
- package/dist/cjs/utils/useFittedBarCount.cjs +42 -0
- package/dist/cjs/utils/useFittedBarCount.cjs.map +1 -0
- package/dist/cjs/utils/useWaveformPeaks.cjs +48 -0
- package/dist/cjs/utils/useWaveformPeaks.cjs.map +1 -0
- package/dist/cjs/utils/useWaveformSeek.cjs +100 -0
- package/dist/cjs/utils/useWaveformSeek.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.mjs +7 -80
- package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -1
- package/dist/components/AudioRecordButton/AudioRecordButton.mjs +37 -0
- package/dist/components/AudioRecordButton/AudioRecordButton.mjs.map +1 -0
- package/dist/components/AudioUpload/AudioUpload.mjs +5 -5
- package/dist/components/AudioUpload/AudioUpload.mjs.map +1 -1
- package/dist/components/ChatInput/ChatInput.mjs +6 -11
- package/dist/components/ChatInput/ChatInput.mjs.map +1 -1
- package/dist/components/ChatMessage/ChatMessage.mjs +211 -0
- package/dist/components/ChatMessage/ChatMessage.mjs.map +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.mjs +161 -72
- package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
- package/dist/components/Icons/DenseGridViewIcon.mjs +35 -0
- package/dist/components/Icons/DenseGridViewIcon.mjs.map +1 -0
- package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs +49 -0
- package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs.map +1 -0
- package/dist/components/Select/Select.mjs +61 -17
- package/dist/components/Select/Select.mjs.map +1 -1
- package/dist/components/SubscribeButton/SubscribeButton.mjs +36 -0
- package/dist/components/SubscribeButton/SubscribeButton.mjs.map +1 -0
- package/dist/components/VoiceNote/VoiceNote.mjs +288 -0
- package/dist/components/VoiceNote/VoiceNote.mjs.map +1 -0
- package/dist/index.d.ts +321 -4
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -1
- package/dist/styles/base.css +11 -2
- package/dist/styles/theme.css +1 -1
- package/dist/utils/audioWaveform.mjs +76 -0
- package/dist/utils/audioWaveform.mjs.map +1 -0
- package/dist/utils/useAudioPlayback.mjs +106 -0
- package/dist/utils/useAudioPlayback.mjs.map +1 -0
- package/dist/utils/useFittedBarCount.mjs +25 -0
- package/dist/utils/useFittedBarCount.mjs.map +1 -0
- package/dist/utils/useWaveformPeaks.mjs +31 -0
- package/dist/utils/useWaveformPeaks.mjs.map +1 -0
- package/dist/utils/useWaveformSeek.mjs +83 -0
- package/dist/utils/useWaveformSeek.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { resamplePeaks, formatTime } from "../../utils/audioWaveform.mjs";
|
|
5
|
+
import { cn } from "../../utils/cn.mjs";
|
|
6
|
+
import { useAudioPlayback } from "../../utils/useAudioPlayback.mjs";
|
|
7
|
+
import { useFittedBarCount } from "../../utils/useFittedBarCount.mjs";
|
|
8
|
+
import { useWaveformPeaks } from "../../utils/useWaveformPeaks.mjs";
|
|
9
|
+
import { useWaveformSeek } from "../../utils/useWaveformSeek.mjs";
|
|
10
|
+
import { IconButton } from "../IconButton/IconButton.mjs";
|
|
11
|
+
import { CloseIcon } from "../Icons/CloseIcon.mjs";
|
|
12
|
+
import { PauseIcon } from "../Icons/PauseIcon.mjs";
|
|
13
|
+
import { PlayIcon } from "../Icons/PlayIcon.mjs";
|
|
14
|
+
const DEFAULT_WAVEFORM = [
|
|
15
|
+
0.15,
|
|
16
|
+
0.46,
|
|
17
|
+
0.85,
|
|
18
|
+
0.23,
|
|
19
|
+
1,
|
|
20
|
+
0.85,
|
|
21
|
+
0.62,
|
|
22
|
+
0.62,
|
|
23
|
+
0.62,
|
|
24
|
+
0.85,
|
|
25
|
+
0.85,
|
|
26
|
+
0.62,
|
|
27
|
+
0.62,
|
|
28
|
+
1,
|
|
29
|
+
1,
|
|
30
|
+
0.62,
|
|
31
|
+
0.62,
|
|
32
|
+
0.62,
|
|
33
|
+
0.23,
|
|
34
|
+
0.23,
|
|
35
|
+
0.15,
|
|
36
|
+
0.46,
|
|
37
|
+
0.85,
|
|
38
|
+
0.23,
|
|
39
|
+
1,
|
|
40
|
+
0.46,
|
|
41
|
+
0.85,
|
|
42
|
+
0.62,
|
|
43
|
+
0.23,
|
|
44
|
+
0.23,
|
|
45
|
+
0.62,
|
|
46
|
+
0.62,
|
|
47
|
+
0.85,
|
|
48
|
+
0.85,
|
|
49
|
+
0.62,
|
|
50
|
+
0.62,
|
|
51
|
+
0.85,
|
|
52
|
+
1,
|
|
53
|
+
0.85,
|
|
54
|
+
1,
|
|
55
|
+
0.62,
|
|
56
|
+
0.62,
|
|
57
|
+
0.62,
|
|
58
|
+
0.23,
|
|
59
|
+
0.23,
|
|
60
|
+
0.62,
|
|
61
|
+
0.23,
|
|
62
|
+
0.23,
|
|
63
|
+
0.23,
|
|
64
|
+
0.62,
|
|
65
|
+
0.85,
|
|
66
|
+
0.62,
|
|
67
|
+
0.85,
|
|
68
|
+
1,
|
|
69
|
+
0.85,
|
|
70
|
+
0.85,
|
|
71
|
+
1,
|
|
72
|
+
0.85,
|
|
73
|
+
0.23,
|
|
74
|
+
0.23,
|
|
75
|
+
0.23,
|
|
76
|
+
0.46,
|
|
77
|
+
0.62,
|
|
78
|
+
0.23,
|
|
79
|
+
0.23
|
|
80
|
+
];
|
|
81
|
+
const MIN_BAR_HEIGHT = 4;
|
|
82
|
+
const MAX_BAR_HEIGHT = {
|
|
83
|
+
default: 26,
|
|
84
|
+
small: 16
|
|
85
|
+
};
|
|
86
|
+
const CONTROL_SIZE = {
|
|
87
|
+
default: "48",
|
|
88
|
+
small: "32"
|
|
89
|
+
};
|
|
90
|
+
const BAR_WIDTH_PX = 4;
|
|
91
|
+
const BAR_GAP_PX = 4;
|
|
92
|
+
const DEFAULT_BAR_COUNT = 40;
|
|
93
|
+
function activeBarClass(negative) {
|
|
94
|
+
return negative ? "bg-messages-waveform-listening-negative-active" : "bg-messages-waveform-listening-active";
|
|
95
|
+
}
|
|
96
|
+
function inactiveBarClass(negative) {
|
|
97
|
+
return negative ? "bg-messages-waveform-listening-negative-inactive" : "bg-messages-waveform-listening-inactive";
|
|
98
|
+
}
|
|
99
|
+
function restingBarClass(negative) {
|
|
100
|
+
return negative ? "bg-messages-waveform-listening-negative-default" : "bg-messages-waveform-default";
|
|
101
|
+
}
|
|
102
|
+
function WaveformBars({ bars, variant, size, negative, progress }) {
|
|
103
|
+
const hasProgress = progress !== void 0;
|
|
104
|
+
const activeCount = hasProgress ? Math.round(progress * bars.length) : 0;
|
|
105
|
+
const maxBarHeight = MAX_BAR_HEIGHT[size];
|
|
106
|
+
return /* @__PURE__ */ jsx(Fragment, { children: bars.map((amplitude, index) => {
|
|
107
|
+
const height = variant === "flat" ? MIN_BAR_HEIGHT : Math.max(MIN_BAR_HEIGHT, Math.round(amplitude * maxBarHeight));
|
|
108
|
+
const colorClass = !hasProgress ? restingBarClass(negative) : index < activeCount ? activeBarClass(negative) : inactiveBarClass(negative);
|
|
109
|
+
return /* @__PURE__ */ jsx(
|
|
110
|
+
"span",
|
|
111
|
+
{
|
|
112
|
+
className: cn("w-1 shrink-0 rounded-[2px]", colorClass),
|
|
113
|
+
style: { height }
|
|
114
|
+
},
|
|
115
|
+
index
|
|
116
|
+
);
|
|
117
|
+
}) });
|
|
118
|
+
}
|
|
119
|
+
function WaveformMeta({ time, fileName, textColor }) {
|
|
120
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
121
|
+
time && /* @__PURE__ */ jsx("span", { className: cn("typography-body-small-14px-regular shrink-0", textColor), children: time }),
|
|
122
|
+
fileName && /* @__PURE__ */ jsx(
|
|
123
|
+
"span",
|
|
124
|
+
{
|
|
125
|
+
className: cn(
|
|
126
|
+
"typography-body-small-14px-regular min-w-0 max-w-[50%] truncate",
|
|
127
|
+
textColor
|
|
128
|
+
),
|
|
129
|
+
children: fileName
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
] });
|
|
133
|
+
}
|
|
134
|
+
const VoiceNote = React.forwardRef(
|
|
135
|
+
({
|
|
136
|
+
className,
|
|
137
|
+
src,
|
|
138
|
+
duration,
|
|
139
|
+
waveform = DEFAULT_WAVEFORM,
|
|
140
|
+
variant = "default",
|
|
141
|
+
size = "default",
|
|
142
|
+
negative = false,
|
|
143
|
+
progress,
|
|
144
|
+
playing,
|
|
145
|
+
defaultPlaying = false,
|
|
146
|
+
onPlayPause,
|
|
147
|
+
onEnded,
|
|
148
|
+
time,
|
|
149
|
+
fileName,
|
|
150
|
+
showControls = true,
|
|
151
|
+
showTimestamp = true,
|
|
152
|
+
showRemove = false,
|
|
153
|
+
onRemove,
|
|
154
|
+
playButtonLabel,
|
|
155
|
+
removeButtonLabel = "Remove",
|
|
156
|
+
"aria-label": ariaLabel = "Voice note",
|
|
157
|
+
...props
|
|
158
|
+
}, ref) => {
|
|
159
|
+
const isPlayer = src !== void 0;
|
|
160
|
+
const playback = useAudioPlayback({
|
|
161
|
+
src,
|
|
162
|
+
duration,
|
|
163
|
+
playing,
|
|
164
|
+
defaultPlaying,
|
|
165
|
+
onEnded,
|
|
166
|
+
onPlay: React.useCallback(() => onPlayPause?.(true), [onPlayPause]),
|
|
167
|
+
onPause: React.useCallback(() => onPlayPause?.(false), [onPlayPause])
|
|
168
|
+
});
|
|
169
|
+
const decodedPeaks = useWaveformPeaks(src);
|
|
170
|
+
const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);
|
|
171
|
+
const isControlled = playing !== void 0;
|
|
172
|
+
const trackRef = React.useRef(null);
|
|
173
|
+
const fittedBarCount = useFittedBarCount(trackRef, {
|
|
174
|
+
barWidthPx: BAR_WIDTH_PX,
|
|
175
|
+
gapPx: BAR_GAP_PX,
|
|
176
|
+
fallback: DEFAULT_BAR_COUNT
|
|
177
|
+
});
|
|
178
|
+
const seekProps = useWaveformSeek({
|
|
179
|
+
trackRef,
|
|
180
|
+
displayDuration: playback.displayDuration,
|
|
181
|
+
currentTime: playback.currentTime,
|
|
182
|
+
seekTo: playback.seekTo,
|
|
183
|
+
setSeeking: playback.setSeeking
|
|
184
|
+
});
|
|
185
|
+
const isPlaying = isPlayer ? playback.isPlaying : playing ?? internalPlaying;
|
|
186
|
+
const handlePlayPause = () => {
|
|
187
|
+
if (isPlayer) {
|
|
188
|
+
playback.toggle();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const next = !isPlaying;
|
|
192
|
+
if (!isControlled) setInternalPlaying(next);
|
|
193
|
+
onPlayPause?.(next);
|
|
194
|
+
};
|
|
195
|
+
const bars = resamplePeaks(isPlayer ? decodedPeaks : waveform, fittedBarCount);
|
|
196
|
+
const progressValue = isPlayer ? playback.progress : progress;
|
|
197
|
+
const textColor = negative ? "text-content-primary-inverted" : "text-content-primary";
|
|
198
|
+
const playerTime = playback.hasStarted ? playback.currentTime : playback.displayDuration;
|
|
199
|
+
const displayTime = isPlayer ? formatTime(playerTime) : time;
|
|
200
|
+
const controlLabel = playButtonLabel ?? (isPlaying ? "Pause" : "Play");
|
|
201
|
+
const trackAria = isPlayer ? seekProps : { "aria-hidden": true };
|
|
202
|
+
return (
|
|
203
|
+
// biome-ignore lint/a11y/useSemanticElements: <fieldset> would break the public HTMLDivElement ref/props API
|
|
204
|
+
/* @__PURE__ */ jsxs(
|
|
205
|
+
"div",
|
|
206
|
+
{
|
|
207
|
+
ref,
|
|
208
|
+
role: "group",
|
|
209
|
+
"aria-label": ariaLabel,
|
|
210
|
+
className: cn("flex items-center gap-3", size === "small" ? "h-8" : "h-12", className),
|
|
211
|
+
...props,
|
|
212
|
+
children: [
|
|
213
|
+
showControls && /* @__PURE__ */ jsx(
|
|
214
|
+
IconButton,
|
|
215
|
+
{
|
|
216
|
+
variant: "secondary",
|
|
217
|
+
size: CONTROL_SIZE[size],
|
|
218
|
+
negative,
|
|
219
|
+
icon: isPlaying ? /* @__PURE__ */ jsx(PauseIcon, {}) : /* @__PURE__ */ jsx(PlayIcon, {}),
|
|
220
|
+
"aria-label": controlLabel,
|
|
221
|
+
onClick: handlePlayPause
|
|
222
|
+
}
|
|
223
|
+
),
|
|
224
|
+
/* @__PURE__ */ jsx(
|
|
225
|
+
"div",
|
|
226
|
+
{
|
|
227
|
+
ref: trackRef,
|
|
228
|
+
"data-testid": "voice-note-waveform",
|
|
229
|
+
className: cn(
|
|
230
|
+
"flex h-full min-w-0 flex-1 items-center gap-1 overflow-hidden",
|
|
231
|
+
isPlayer && "cursor-pointer touch-none select-none focus-visible:shadow-focus-ring focus-visible:outline-none"
|
|
232
|
+
),
|
|
233
|
+
...trackAria,
|
|
234
|
+
children: /* @__PURE__ */ jsx(
|
|
235
|
+
WaveformBars,
|
|
236
|
+
{
|
|
237
|
+
bars,
|
|
238
|
+
variant,
|
|
239
|
+
size,
|
|
240
|
+
negative,
|
|
241
|
+
progress: progressValue
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
),
|
|
246
|
+
/* @__PURE__ */ jsx(
|
|
247
|
+
WaveformMeta,
|
|
248
|
+
{
|
|
249
|
+
time: showTimestamp ? displayTime : void 0,
|
|
250
|
+
fileName,
|
|
251
|
+
textColor
|
|
252
|
+
}
|
|
253
|
+
),
|
|
254
|
+
showRemove && /* @__PURE__ */ jsx(
|
|
255
|
+
IconButton,
|
|
256
|
+
{
|
|
257
|
+
variant: "tertiary",
|
|
258
|
+
size: CONTROL_SIZE[size],
|
|
259
|
+
negative,
|
|
260
|
+
icon: /* @__PURE__ */ jsx(CloseIcon, {}),
|
|
261
|
+
"aria-label": removeButtonLabel,
|
|
262
|
+
onClick: onRemove
|
|
263
|
+
}
|
|
264
|
+
),
|
|
265
|
+
isPlayer && // biome-ignore lint/a11y/useMediaCaption: a UI voice note / short recording, not narrated content needing captions
|
|
266
|
+
/* @__PURE__ */ jsx(
|
|
267
|
+
"audio",
|
|
268
|
+
{
|
|
269
|
+
ref: playback.audioRef,
|
|
270
|
+
src,
|
|
271
|
+
preload: "metadata",
|
|
272
|
+
className: "hidden",
|
|
273
|
+
onLoadedMetadata: playback.audioHandlers.onLoadedMetadata,
|
|
274
|
+
onTimeUpdate: playback.audioHandlers.onTimeUpdate,
|
|
275
|
+
onEnded: playback.audioHandlers.onEnded
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
VoiceNote.displayName = "VoiceNote";
|
|
285
|
+
export {
|
|
286
|
+
VoiceNote
|
|
287
|
+
};
|
|
288
|
+
//# sourceMappingURL=VoiceNote.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceNote.mjs","sources":["../../../src/components/VoiceNote/VoiceNote.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { formatTime, resamplePeaks } from \"@/utils/audioWaveform\";\nimport { cn } from \"@/utils/cn\";\nimport { useAudioPlayback } from \"@/utils/useAudioPlayback\";\nimport { useFittedBarCount } from \"@/utils/useFittedBarCount\";\nimport { useWaveformPeaks } from \"@/utils/useWaveformPeaks\";\nimport { useWaveformSeek } from \"@/utils/useWaveformSeek\";\nimport { IconButton, type IconButtonSize } from \"../IconButton/IconButton\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { PauseIcon } from \"../Icons/PauseIcon\";\nimport { PlayIcon } from \"../Icons/PlayIcon\";\n\n/** Visual style of the waveform. */\nexport type VoiceNoteVariant = \"default\" | \"flat\";\n\n/** Size preset for the voice note. */\nexport type VoiceNoteSize = \"default\" | \"small\";\n\n/**\n * Default waveform amplitudes (0–1), taken from the Figma reference so the\n * resting state renders 1:1 when no `waveform` data (or `src`) is supplied.\n */\nconst DEFAULT_WAVEFORM = [\n 0.15, 0.46, 0.85, 0.23, 1, 0.85, 0.62, 0.62, 0.62, 0.85, 0.85, 0.62, 0.62, 1, 1, 0.62, 0.62, 0.62,\n 0.23, 0.23, 0.15, 0.46, 0.85, 0.23, 1, 0.46, 0.85, 0.62, 0.23, 0.23, 0.62, 0.62, 0.85, 0.85, 0.62,\n 0.62, 0.85, 1, 0.85, 1, 0.62, 0.62, 0.62, 0.23, 0.23, 0.62, 0.23, 0.23, 0.23, 0.62, 0.85, 0.62,\n 0.85, 1, 0.85, 0.85, 1, 0.85, 0.23, 0.23, 0.23, 0.46, 0.62, 0.23, 0.23,\n];\n\nconst MIN_BAR_HEIGHT = 4;\nconst MAX_BAR_HEIGHT: Record<VoiceNoteSize, number> = {\n default: 26,\n small: 16,\n};\n\nconst CONTROL_SIZE: Record<VoiceNoteSize, IconButtonSize> = {\n default: \"48\",\n small: \"32\",\n};\n\n/** Rendered width of a single waveform bar, in pixels (`w-1`). */\nconst BAR_WIDTH_PX = 4;\n/** Gap between waveform bars, in pixels (`gap-1`). */\nconst BAR_GAP_PX = 4;\n/** Bar count before the track is measured (SSR, jsdom, or zero-width layout). */\nconst DEFAULT_BAR_COUNT = 40;\n\nfunction activeBarClass(negative: boolean): string {\n return negative\n ? \"bg-messages-waveform-listening-negative-active\"\n : \"bg-messages-waveform-listening-active\";\n}\n\nfunction inactiveBarClass(negative: boolean): string {\n return negative\n ? \"bg-messages-waveform-listening-negative-inactive\"\n : \"bg-messages-waveform-listening-inactive\";\n}\n\nfunction restingBarClass(negative: boolean): string {\n return negative\n ? \"bg-messages-waveform-listening-negative-default\"\n : \"bg-messages-waveform-default\";\n}\n\ninterface WaveformBarsProps {\n bars: number[];\n variant: VoiceNoteVariant;\n size: VoiceNoteSize;\n negative: boolean;\n progress: number | undefined;\n}\n\nfunction WaveformBars({ bars, variant, size, negative, progress }: WaveformBarsProps) {\n const hasProgress = progress !== undefined;\n const activeCount = hasProgress ? Math.round(progress * bars.length) : 0;\n const maxBarHeight = MAX_BAR_HEIGHT[size];\n\n return (\n <>\n {bars.map((amplitude, index) => {\n const height =\n variant === \"flat\"\n ? MIN_BAR_HEIGHT\n : Math.max(MIN_BAR_HEIGHT, Math.round(amplitude * maxBarHeight));\n const colorClass = !hasProgress\n ? restingBarClass(negative)\n : index < activeCount\n ? activeBarClass(negative)\n : inactiveBarClass(negative);\n\n return (\n <span\n // biome-ignore lint/suspicious/noArrayIndexKey: bars are a fixed positional sequence\n key={index}\n className={cn(\"w-1 shrink-0 rounded-[2px]\", colorClass)}\n style={{ height }}\n />\n );\n })}\n </>\n );\n}\n\ninterface WaveformMetaProps {\n time: string | undefined;\n fileName: string | undefined;\n textColor: string;\n}\n\nfunction WaveformMeta({ time, fileName, textColor }: WaveformMetaProps) {\n return (\n <>\n {time && (\n <span className={cn(\"typography-body-small-14px-regular shrink-0\", textColor)}>{time}</span>\n )}\n {fileName && (\n <span\n className={cn(\n \"typography-body-small-14px-regular min-w-0 max-w-[50%] truncate\",\n textColor,\n )}\n >\n {fileName}\n </span>\n )}\n </>\n );\n}\n\nexport interface VoiceNoteProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onPlay\" | \"onPause\"> {\n /**\n * URL of the audio to play. When set, the component manages a real `<audio>`\n * element: it decodes the waveform, plays/pauses, tracks live progress and is\n * seekable — `waveform`/`progress` are derived automatically. Leave unset for\n * a presentational, fully-controlled voice note.\n */\n src?: string;\n /** Fallback total duration (seconds), used until the media's metadata loads. */\n duration?: number;\n /** Amplitude values (0–1), one per bar. Ignored when `src` is set. Falls back to a built-in pattern. */\n waveform?: number[];\n /** Visual style; `flat` renders a simplified dotted preview. @default \"default\" */\n variant?: VoiceNoteVariant;\n /** Size preset. @default \"default\" */\n size?: VoiceNoteSize;\n /** Dark-surface treatment for use on message bubbles. @default false */\n negative?: boolean;\n /**\n * Playback progress (0–1) for the presentational mode. When set, the waveform\n * splits into played/unplayed bars (the \"Listening\" state). Ignored when `src`\n * is set (progress comes from the media element).\n */\n progress?: number;\n /** Whether audio is playing (controlled) — toggles the play/pause icon. */\n playing?: boolean;\n /** Initial playing state (uncontrolled). @default false */\n defaultPlaying?: boolean;\n /** Called with the next playing state when the play/pause control is pressed. */\n onPlayPause?: (playing: boolean) => void;\n /** Called when audio playback reaches the end (only in `src` mode). */\n onEnded?: () => void;\n /** Timestamp or duration label, e.g. \"0:05\". Ignored when `src` is set (derived from the media). */\n time?: string;\n /** File name shown when the audio is an uploaded file rather than a voice note. */\n fileName?: string;\n /** Show the play/pause control. @default true */\n showControls?: boolean;\n /** Show the timestamp label. @default true */\n showTimestamp?: boolean;\n /** Show a remove button (calls {@link VoiceNoteProps.onRemove}). @default false */\n showRemove?: boolean;\n /** Called when the remove button is pressed. */\n onRemove?: () => void;\n /** Accessible name for the play/pause control. Defaults to \"Play\"/\"Pause\". */\n playButtonLabel?: string;\n /** Accessible name for the remove button. @default \"Remove\" */\n removeButtonLabel?: string;\n /** Accessible name for the whole player. @default \"Voice note\" */\n \"aria-label\"?: string;\n}\n\n/**\n * A voice-note audio player: a play/pause control, an amplitude waveform, and a\n * timestamp — for voice messages and audio attachments in a conversation.\n *\n * Two modes: pass `src` for a self-contained player that decodes the waveform,\n * plays real audio, tracks live progress and is seekable; or omit `src` and\n * drive it with `playing`/`progress` + `onPlayPause` for a presentational,\n * fully-controlled voice note. `flat` renders a compact dotted preview and\n * `negative` adapts it to dark message bubbles.\n *\n * @example\n * ```tsx\n * <VoiceNote src=\"https://example.com/note.mp3\" duration={5} />\n * <VoiceNote time=\"0:05\" progress={0.4} playing onPlayPause={toggle} />\n * ```\n */\nexport const VoiceNote = React.forwardRef<HTMLDivElement, VoiceNoteProps>(\n (\n {\n className,\n src,\n duration,\n waveform = DEFAULT_WAVEFORM,\n variant = \"default\",\n size = \"default\",\n negative = false,\n progress,\n playing,\n defaultPlaying = false,\n onPlayPause,\n onEnded,\n time,\n fileName,\n showControls = true,\n showTimestamp = true,\n showRemove = false,\n onRemove,\n playButtonLabel,\n removeButtonLabel = \"Remove\",\n \"aria-label\": ariaLabel = \"Voice note\",\n ...props\n },\n ref,\n ) => {\n const isPlayer = src !== undefined;\n\n const playback = useAudioPlayback({\n src,\n duration,\n playing,\n defaultPlaying,\n onEnded,\n onPlay: React.useCallback(() => onPlayPause?.(true), [onPlayPause]),\n onPause: React.useCallback(() => onPlayPause?.(false), [onPlayPause]),\n });\n const decodedPeaks = useWaveformPeaks(src);\n\n const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);\n const isControlled = playing !== undefined;\n\n const trackRef = React.useRef<HTMLDivElement>(null);\n const fittedBarCount = useFittedBarCount(trackRef, {\n barWidthPx: BAR_WIDTH_PX,\n gapPx: BAR_GAP_PX,\n fallback: DEFAULT_BAR_COUNT,\n });\n const seekProps = useWaveformSeek({\n trackRef,\n displayDuration: playback.displayDuration,\n currentTime: playback.currentTime,\n seekTo: playback.seekTo,\n setSeeking: playback.setSeeking,\n });\n\n const isPlaying = isPlayer ? playback.isPlaying : (playing ?? internalPlaying);\n\n const handlePlayPause = () => {\n if (isPlayer) {\n playback.toggle();\n return;\n }\n const next = !isPlaying;\n if (!isControlled) setInternalPlaying(next);\n onPlayPause?.(next);\n };\n\n const bars = resamplePeaks(isPlayer ? decodedPeaks : waveform, fittedBarCount);\n const progressValue = isPlayer ? playback.progress : progress;\n const textColor = negative ? \"text-content-primary-inverted\" : \"text-content-primary\";\n\n const playerTime = playback.hasStarted ? playback.currentTime : playback.displayDuration;\n const displayTime = isPlayer ? formatTime(playerTime) : time;\n const controlLabel = playButtonLabel ?? (isPlaying ? \"Pause\" : \"Play\");\n const trackAria = isPlayer ? seekProps : ({ \"aria-hidden\": true } as const);\n\n return (\n // biome-ignore lint/a11y/useSemanticElements: <fieldset> would break the public HTMLDivElement ref/props API\n <div\n ref={ref}\n role=\"group\"\n aria-label={ariaLabel}\n className={cn(\"flex items-center gap-3\", size === \"small\" ? \"h-8\" : \"h-12\", className)}\n {...props}\n >\n {showControls && (\n <IconButton\n variant=\"secondary\"\n size={CONTROL_SIZE[size]}\n negative={negative}\n icon={isPlaying ? <PauseIcon /> : <PlayIcon />}\n aria-label={controlLabel}\n onClick={handlePlayPause}\n />\n )}\n\n <div\n ref={trackRef}\n data-testid=\"voice-note-waveform\"\n className={cn(\n \"flex h-full min-w-0 flex-1 items-center gap-1 overflow-hidden\",\n isPlayer &&\n \"cursor-pointer touch-none select-none focus-visible:shadow-focus-ring focus-visible:outline-none\",\n )}\n {...trackAria}\n >\n <WaveformBars\n bars={bars}\n variant={variant}\n size={size}\n negative={negative}\n progress={progressValue}\n />\n </div>\n\n <WaveformMeta\n time={showTimestamp ? displayTime : undefined}\n fileName={fileName}\n textColor={textColor}\n />\n\n {showRemove && (\n <IconButton\n variant=\"tertiary\"\n size={CONTROL_SIZE[size]}\n negative={negative}\n icon={<CloseIcon />}\n aria-label={removeButtonLabel}\n onClick={onRemove}\n />\n )}\n\n {isPlayer && (\n // biome-ignore lint/a11y/useMediaCaption: a UI voice note / short recording, not narrated content needing captions\n <audio\n ref={playback.audioRef}\n src={src}\n preload=\"metadata\"\n className=\"hidden\"\n onLoadedMetadata={playback.audioHandlers.onLoadedMetadata}\n onTimeUpdate={playback.audioHandlers.onTimeUpdate}\n onEnded={playback.audioHandlers.onEnded}\n />\n )}\n </div>\n );\n },\n);\n\nVoiceNote.displayName = \"VoiceNote\";\n"],"names":[],"mappings":"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"}
|