@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.
Files changed (72) hide show
  1. package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +16 -89
  2. package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  3. package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs +54 -0
  4. package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs.map +1 -0
  5. package/dist/cjs/components/AudioUpload/AudioUpload.cjs +5 -5
  6. package/dist/cjs/components/AudioUpload/AudioUpload.cjs.map +1 -1
  7. package/dist/cjs/components/ChatInput/ChatInput.cjs +6 -11
  8. package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -1
  9. package/dist/cjs/components/ChatMessage/ChatMessage.cjs +228 -0
  10. package/dist/cjs/components/ChatMessage/ChatMessage.cjs.map +1 -0
  11. package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +160 -71
  12. package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  13. package/dist/cjs/components/Icons/DenseGridViewIcon.cjs +52 -0
  14. package/dist/cjs/components/Icons/DenseGridViewIcon.cjs.map +1 -0
  15. package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs +66 -0
  16. package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs.map +1 -0
  17. package/dist/cjs/components/Select/Select.cjs +61 -17
  18. package/dist/cjs/components/Select/Select.cjs.map +1 -1
  19. package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs +53 -0
  20. package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs.map +1 -0
  21. package/dist/cjs/components/VoiceNote/VoiceNote.cjs +305 -0
  22. package/dist/cjs/components/VoiceNote/VoiceNote.cjs.map +1 -0
  23. package/dist/cjs/index.cjs +12 -0
  24. package/dist/cjs/index.cjs.map +1 -1
  25. package/dist/cjs/utils/audioWaveform.cjs +76 -0
  26. package/dist/cjs/utils/audioWaveform.cjs.map +1 -0
  27. package/dist/cjs/utils/useAudioPlayback.cjs +123 -0
  28. package/dist/cjs/utils/useAudioPlayback.cjs.map +1 -0
  29. package/dist/cjs/utils/useFittedBarCount.cjs +42 -0
  30. package/dist/cjs/utils/useFittedBarCount.cjs.map +1 -0
  31. package/dist/cjs/utils/useWaveformPeaks.cjs +48 -0
  32. package/dist/cjs/utils/useWaveformPeaks.cjs.map +1 -0
  33. package/dist/cjs/utils/useWaveformSeek.cjs +100 -0
  34. package/dist/cjs/utils/useWaveformSeek.cjs.map +1 -0
  35. package/dist/components/AudioPlayer/AudioPlayer.mjs +7 -80
  36. package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -1
  37. package/dist/components/AudioRecordButton/AudioRecordButton.mjs +37 -0
  38. package/dist/components/AudioRecordButton/AudioRecordButton.mjs.map +1 -0
  39. package/dist/components/AudioUpload/AudioUpload.mjs +5 -5
  40. package/dist/components/AudioUpload/AudioUpload.mjs.map +1 -1
  41. package/dist/components/ChatInput/ChatInput.mjs +6 -11
  42. package/dist/components/ChatInput/ChatInput.mjs.map +1 -1
  43. package/dist/components/ChatMessage/ChatMessage.mjs +211 -0
  44. package/dist/components/ChatMessage/ChatMessage.mjs.map +1 -0
  45. package/dist/components/DropdownMenu/DropdownMenu.mjs +161 -72
  46. package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
  47. package/dist/components/Icons/DenseGridViewIcon.mjs +35 -0
  48. package/dist/components/Icons/DenseGridViewIcon.mjs.map +1 -0
  49. package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs +49 -0
  50. package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs.map +1 -0
  51. package/dist/components/Select/Select.mjs +61 -17
  52. package/dist/components/Select/Select.mjs.map +1 -1
  53. package/dist/components/SubscribeButton/SubscribeButton.mjs +36 -0
  54. package/dist/components/SubscribeButton/SubscribeButton.mjs.map +1 -0
  55. package/dist/components/VoiceNote/VoiceNote.mjs +288 -0
  56. package/dist/components/VoiceNote/VoiceNote.mjs.map +1 -0
  57. package/dist/index.d.ts +321 -4
  58. package/dist/index.mjs +12 -0
  59. package/dist/index.mjs.map +1 -1
  60. package/dist/styles/base.css +11 -2
  61. package/dist/styles/theme.css +1 -1
  62. package/dist/utils/audioWaveform.mjs +76 -0
  63. package/dist/utils/audioWaveform.mjs.map +1 -0
  64. package/dist/utils/useAudioPlayback.mjs +106 -0
  65. package/dist/utils/useAudioPlayback.mjs.map +1 -0
  66. package/dist/utils/useFittedBarCount.mjs +25 -0
  67. package/dist/utils/useFittedBarCount.mjs.map +1 -0
  68. package/dist/utils/useWaveformPeaks.mjs +31 -0
  69. package/dist/utils/useWaveformPeaks.mjs.map +1 -0
  70. package/dist/utils/useWaveformSeek.mjs +83 -0
  71. package/dist/utils/useWaveformSeek.mjs.map +1 -0
  72. package/package.json +1 -1
@@ -3,7 +3,9 @@
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const jsxRuntime = require("react/jsx-runtime");
5
5
  const React = require("react");
6
+ const audioWaveform = require("../../utils/audioWaveform.cjs");
6
7
  const cn = require("../../utils/cn.cjs");
8
+ const useFittedBarCount = require("../../utils/useFittedBarCount.cjs");
7
9
  const PauseIcon = require("../Icons/PauseIcon.cjs");
8
10
  const PlayIcon = require("../Icons/PlayIcon.cjs");
9
11
  function _interopNamespaceDefault(e) {
@@ -28,72 +30,8 @@ const BAR_GAP_PX = 4;
28
30
  const MIN_BAR_HEIGHT_PX = 4;
29
31
  const MAX_BAR_HEIGHT_PX = 26;
30
32
  const DEFAULT_BAR_COUNT = 24;
31
- const RAW_PEAK_COUNT = 128;
32
33
  const SEEK_STEP_SECONDS = 1;
33
34
  const SEEK_STEP_LARGE_SECONDS = 5;
34
- function formatTime(seconds) {
35
- if (seconds === void 0 || !Number.isFinite(seconds)) return "--:--";
36
- const totalSeconds = Math.max(0, Math.floor(seconds));
37
- const minutes = Math.floor(totalSeconds / 60);
38
- const remainingSeconds = totalSeconds % 60;
39
- return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
40
- }
41
- function hashString(value) {
42
- let hash = 2166136261;
43
- for (let i = 0; i < value.length; i++) {
44
- hash ^= value.charCodeAt(i);
45
- hash = Math.imul(hash, 16777619);
46
- }
47
- return hash >>> 0;
48
- }
49
- function createSeededRandom(seed) {
50
- let state = seed;
51
- return () => {
52
- state = state + 1831565813 | 0;
53
- let t = Math.imul(state ^ state >>> 15, 1 | state);
54
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
55
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
56
- };
57
- }
58
- function generateFallbackPeaks(src, count) {
59
- const random = createSeededRandom(hashString(src));
60
- return Array.from({ length: count }, () => 0.2 + random() * 0.8);
61
- }
62
- function computePeaksFromChannelData(channelData, count) {
63
- const blockSize = Math.max(1, Math.floor(channelData.length / count));
64
- const peaks = [];
65
- for (let i = 0; i < count; i++) {
66
- const start = i * blockSize;
67
- let max = 0;
68
- for (let j = 0; j < blockSize && start + j < channelData.length; j++) {
69
- max = Math.max(max, Math.abs(channelData[start + j] ?? 0));
70
- }
71
- peaks.push(max);
72
- }
73
- return peaks;
74
- }
75
- function resamplePeaks(peaks, barCount) {
76
- if (peaks.length === 0 || barCount <= 0) return [];
77
- const step = peaks.length / barCount;
78
- return Array.from(
79
- { length: barCount },
80
- (_, i) => peaks[Math.min(peaks.length - 1, Math.floor(i * step))] ?? 0
81
- );
82
- }
83
- async function decodeAudioPeaks(src, signal) {
84
- const AudioContextCtor = window.AudioContext ?? window.webkitAudioContext;
85
- if (!AudioContextCtor) throw new Error("WebAudio unsupported");
86
- const response = await fetch(src, { signal });
87
- const arrayBuffer = await response.arrayBuffer();
88
- const audioContext = new AudioContextCtor();
89
- try {
90
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
91
- return computePeaksFromChannelData(audioBuffer.getChannelData(0), RAW_PEAK_COUNT);
92
- } finally {
93
- if (audioContext.state !== "closed") audioContext.close().catch(() => {
94
- });
95
- }
96
- }
97
35
  const AudioPlayer = React__namespace.forwardRef(
98
36
  ({
99
37
  className,
@@ -114,9 +52,13 @@ const AudioPlayer = React__namespace.forwardRef(
114
52
  const playing = isControlled ? controlledPlaying : internalPlaying;
115
53
  const [currentTime, setCurrentTime] = React__namespace.useState(0);
116
54
  const [mediaDuration, setMediaDuration] = React__namespace.useState(void 0);
117
- const [barCount, setBarCount] = React__namespace.useState(DEFAULT_BAR_COUNT);
55
+ const barCount = useFittedBarCount.useFittedBarCount(trackRef, {
56
+ barWidthPx: BAR_WIDTH_PX,
57
+ gapPx: BAR_GAP_PX,
58
+ fallback: DEFAULT_BAR_COUNT
59
+ });
118
60
  const [peaks, setPeaks] = React__namespace.useState(
119
- () => generateFallbackPeaks(src, RAW_PEAK_COUNT)
61
+ () => audioWaveform.generateFallbackPeaks(src, audioWaveform.RAW_PEAK_COUNT)
120
62
  );
121
63
  const [isDragging, setIsDragging] = React__namespace.useState(false);
122
64
  const displayDuration = mediaDuration ?? duration;
@@ -135,7 +77,7 @@ const AudioPlayer = React__namespace.forwardRef(
135
77
  }
136
78
  setCurrentTime(0);
137
79
  setMediaDuration(void 0);
138
- setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));
80
+ setPeaks(audioWaveform.generateFallbackPeaks(src, audioWaveform.RAW_PEAK_COUNT));
139
81
  if (playingRef.current) {
140
82
  if (!isControlledRef.current) setInternalPlaying(false);
141
83
  onPauseRef.current?.();
@@ -144,31 +86,16 @@ const AudioPlayer = React__namespace.forwardRef(
144
86
  React__namespace.useEffect(() => {
145
87
  let cancelled = false;
146
88
  const abortController = new AbortController();
147
- decodeAudioPeaks(src, abortController.signal).then((decodedPeaks) => {
89
+ audioWaveform.decodeAudioPeaks(src, abortController.signal).then((decodedPeaks) => {
148
90
  if (!cancelled) setPeaks(decodedPeaks);
149
91
  }).catch(() => {
150
- if (!cancelled) setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));
92
+ if (!cancelled) setPeaks(audioWaveform.generateFallbackPeaks(src, audioWaveform.RAW_PEAK_COUNT));
151
93
  });
152
94
  return () => {
153
95
  cancelled = true;
154
96
  abortController.abort();
155
97
  };
156
98
  }, [src]);
157
- React__namespace.useEffect(() => {
158
- const track = trackRef.current;
159
- if (!track) return;
160
- const updateFromWidth = (width) => {
161
- if (width <= 0) return;
162
- setBarCount(Math.max(1, Math.floor((width + BAR_GAP_PX) / (BAR_WIDTH_PX + BAR_GAP_PX))));
163
- };
164
- updateFromWidth(track.getBoundingClientRect().width);
165
- const observer = new ResizeObserver((entries) => {
166
- const entry = entries[0];
167
- if (entry) updateFromWidth(entry.contentRect.width);
168
- });
169
- observer.observe(track);
170
- return () => observer.disconnect();
171
- }, []);
172
99
  React__namespace.useEffect(() => {
173
100
  const audio = audioRef.current;
174
101
  if (!audio) return;
@@ -254,7 +181,7 @@ const AudioPlayer = React__namespace.forwardRef(
254
181
  break;
255
182
  }
256
183
  };
257
- const bars = resamplePeaks(peaks, barCount);
184
+ const bars = audioWaveform.resamplePeaks(peaks, barCount);
258
185
  const progressFraction = displayDuration && displayDuration > 0 ? Math.min(Math.max(currentTime / displayDuration, 0), 1) : 0;
259
186
  const playedBarCount = hasStarted ? Math.round(progressFraction * bars.length) : bars.length;
260
187
  const barData = bars.map((amplitude, index) => {
@@ -304,7 +231,7 @@ const AudioPlayer = React__namespace.forwardRef(
304
231
  "aria-valuemin": 0,
305
232
  "aria-valuemax": displayDuration,
306
233
  "aria-valuenow": Math.round(currentTime),
307
- "aria-valuetext": displayDuration !== void 0 ? `${formatTime(currentTime)} of ${formatTime(displayDuration)}` : formatTime(currentTime),
234
+ "aria-valuetext": displayDuration !== void 0 ? `${audioWaveform.formatTime(currentTime)} of ${audioWaveform.formatTime(displayDuration)}` : audioWaveform.formatTime(currentTime),
308
235
  onPointerDown: handlePointerDown,
309
236
  onKeyDown: handleKeyDown,
310
237
  className: cn.cn(
@@ -318,12 +245,12 @@ const AudioPlayer = React__namespace.forwardRef(
318
245
  }
319
246
  ),
320
247
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typography-body-small-14px-semibold flex shrink-0 items-center gap-0.5 whitespace-nowrap", children: hasStarted ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
321
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-content-primary", children: formatTime(currentTime) }),
248
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-content-primary", children: audioWaveform.formatTime(currentTime) }),
322
249
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-content-secondary", children: [
323
250
  "/",
324
- formatTime(displayDuration)
251
+ audioWaveform.formatTime(displayDuration)
325
252
  ] })
326
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-content-primary", children: formatTime(displayDuration) }) })
253
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-content-primary", children: audioWaveform.formatTime(displayDuration) }) })
327
254
  ] }),
328
255
  /* @__PURE__ */ jsxRuntime.jsx(
329
256
  "audio",
@@ -1 +1 @@
1
- {"version":3,"file":"AudioPlayer.cjs","sources":["../../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { PauseIcon } from \"../Icons/PauseIcon\";\nimport { PlayIcon } from \"../Icons/PlayIcon\";\n\n/** Width of a single waveform bar, in pixels. */\nconst BAR_WIDTH_PX = 4;\n/** Gap between waveform bars, in pixels. */\nconst BAR_GAP_PX = 4;\n/** Shortest a waveform bar can render, in pixels. */\nconst MIN_BAR_HEIGHT_PX = 4;\n/** Tallest a waveform bar can render, in pixels — matches the 40px row height. */\nconst MAX_BAR_HEIGHT_PX = 26;\n/** Bar count used before the container has been measured (e.g. during SSR or in jsdom, where layout is unavailable). */\nconst DEFAULT_BAR_COUNT = 24;\n/** Number of amplitude samples kept internally, resampled to the rendered bar count. */\nconst RAW_PEAK_COUNT = 128;\n/** Seconds moved per Arrow key press while seeking. */\nconst SEEK_STEP_SECONDS = 1;\n/** Seconds moved per Page Up/Down press while seeking. */\nconst SEEK_STEP_LARGE_SECONDS = 5;\n\n/** Height of the audio player row, in pixels. Matches both the Vault card overlay and the \"Generated Audio\" modal row in Figma — both use an identical 32px play button and 40px row. */\nexport type AudioPlayerSize = \"40\";\n\nexport interface AudioPlayerProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onPlay\" | \"onPause\" | \"onEnded\"> {\n /** URL of the audio file to play. */\n src: string;\n /**\n * Total duration in seconds, used to render the timestamp before the\n * media's own metadata has loaded (and as a fallback if it never does).\n */\n duration?: number;\n /**\n * Whether playback is active (controlled). Note: once `onEnded` fires, the\n * browser has already stopped native playback. To loop or replay under\n * controlled usage, toggle this prop `false` then `true` (rather than\n * leaving it `true`) — the sync effect only calls `play()` again when this\n * value actually changes.\n */\n playing?: boolean;\n /** Initial playback state (uncontrolled). @default false */\n defaultPlaying?: boolean;\n /** Called when playback starts. */\n onPlay?: () => void;\n /** Called when playback is paused. */\n onPause?: () => void;\n /** Called when playback reaches the end of the media. */\n onEnded?: () => void;\n /** Height of the player row, in pixels. @default \"40\" */\n size?: AudioPlayerSize;\n}\n\nfunction formatTime(seconds: number | undefined): string {\n if (seconds === undefined || !Number.isFinite(seconds)) return \"--:--\";\n const totalSeconds = Math.max(0, Math.floor(seconds));\n const minutes = Math.floor(totalSeconds / 60);\n const remainingSeconds = totalSeconds % 60;\n return `${minutes}:${String(remainingSeconds).padStart(2, \"0\")}`;\n}\n\n/** Deterministic 32-bit string hash (FNV-1a), used to seed the fallback waveform. */\nfunction hashString(value: string): number {\n let hash = 2166136261;\n for (let i = 0; i < value.length; i++) {\n hash ^= value.charCodeAt(i);\n hash = Math.imul(hash, 16777619);\n }\n return hash >>> 0;\n}\n\n/** Seeded PRNG (mulberry32) — deterministic across runs, unlike `Math.random`. */\nfunction createSeededRandom(seed: number): () => number {\n let state = seed;\n return () => {\n state = (state + 0x6d2b79f5) | 0;\n let t = Math.imul(state ^ (state >>> 15), 1 | state);\n t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n}\n\n/** Deterministic placeholder amplitudes used when audio can't be decoded (network/CORS/format failure). */\nfunction generateFallbackPeaks(src: string, count: number): number[] {\n const random = createSeededRandom(hashString(src));\n return Array.from({ length: count }, () => 0.2 + random() * 0.8);\n}\n\n/** Downsamples decoded PCM data to `count` peak (max-abs) amplitudes. */\nfunction computePeaksFromChannelData(channelData: Float32Array, count: number): number[] {\n const blockSize = Math.max(1, Math.floor(channelData.length / count));\n const peaks: number[] = [];\n for (let i = 0; i < count; i++) {\n const start = i * blockSize;\n let max = 0;\n for (let j = 0; j < blockSize && start + j < channelData.length; j++) {\n max = Math.max(max, Math.abs(channelData[start + j] ?? 0));\n }\n peaks.push(max);\n }\n return peaks;\n}\n\n/** Resamples a peaks array to the number of bars that currently fit the container. */\nfunction resamplePeaks(peaks: number[], barCount: number): number[] {\n if (peaks.length === 0 || barCount <= 0) return [];\n const step = peaks.length / barCount;\n return Array.from(\n { length: barCount },\n (_, i) => peaks[Math.min(peaks.length - 1, Math.floor(i * step))] ?? 0,\n );\n}\n\n/** Decodes `src` via WebAudio and returns downsampled peak amplitudes. Throws on any failure. */\nasync function decodeAudioPeaks(src: string, signal: AbortSignal): Promise<number[]> {\n const AudioContextCtor =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!AudioContextCtor) throw new Error(\"WebAudio unsupported\");\n\n const response = await fetch(src, { signal });\n const arrayBuffer = await response.arrayBuffer();\n const audioContext = new AudioContextCtor();\n try {\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n return computePeaksFromChannelData(audioBuffer.getChannelData(0), RAW_PEAK_COUNT);\n } finally {\n if (audioContext.state !== \"closed\") audioContext.close().catch(() => {});\n }\n}\n\n/**\n * A compact playback control for a single audio clip: a play/pause toggle, a\n * static amplitude waveform that doubles as a seek scrubber, and elapsed/total\n * timestamps. Designed to sit as an overlay on media thumbnails (Vault cards)\n * as well as inline rows (the AI Voice Message \"Generated Audio\" row).\n *\n * The waveform is seeded from the decoded audio when possible, falling back\n * to a deterministic (not random) placeholder derived from `src` so the\n * result is stable across renders, SSR, and Chromatic snapshots.\n *\n * @example\n * ```tsx\n * <AudioPlayer src=\"https://example.com/clip.mp3\" duration={5} />\n * ```\n */\nexport const AudioPlayer = React.forwardRef<HTMLDivElement, AudioPlayerProps>(\n (\n {\n className,\n src,\n duration,\n playing: controlledPlaying,\n defaultPlaying = false,\n onPlay,\n onPause,\n onEnded,\n size = \"40\",\n ...props\n },\n ref,\n ) => {\n const audioRef = React.useRef<HTMLAudioElement>(null);\n const trackRef = React.useRef<HTMLDivElement>(null);\n\n const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);\n const isControlled = controlledPlaying !== undefined;\n const playing = isControlled ? controlledPlaying : internalPlaying;\n\n const [currentTime, setCurrentTime] = React.useState(0);\n const [mediaDuration, setMediaDuration] = React.useState<number | undefined>(undefined);\n const [barCount, setBarCount] = React.useState(DEFAULT_BAR_COUNT);\n const [peaks, setPeaks] = React.useState<number[]>(() =>\n generateFallbackPeaks(src, RAW_PEAK_COUNT),\n );\n const [isDragging, setIsDragging] = React.useState(false);\n\n const displayDuration = mediaDuration ?? duration;\n const hasStarted = playing || currentTime > 0;\n\n // Latest-value refs so the src-change effect below can react to a src swap\n // without re-running whenever `playing`/`isControlled`/`onPause` change.\n const playingRef = React.useRef(playing);\n playingRef.current = playing;\n const isControlledRef = React.useRef(isControlled);\n isControlledRef.current = isControlled;\n const onPauseRef = React.useRef(onPause);\n onPauseRef.current = onPause;\n\n // Reset transient playback state when the source changes. Skips the\n // initial mount (there's nothing to reset yet) and, if playback was\n // active, stops it — the browser silently drops playback on a source\n // swap, so the UI (and any controlled parent) must be told via onPause.\n const isFirstSrcRenderRef = React.useRef(true);\n React.useEffect(() => {\n if (isFirstSrcRenderRef.current) {\n isFirstSrcRenderRef.current = false;\n return;\n }\n setCurrentTime(0);\n setMediaDuration(undefined);\n setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));\n if (playingRef.current) {\n if (!isControlledRef.current) setInternalPlaying(false);\n onPauseRef.current?.();\n }\n }, [src]);\n\n // Decode the audio to derive real waveform amplitudes, falling back to a\n // deterministic placeholder on any failure (network, CORS, unsupported format).\n React.useEffect(() => {\n let cancelled = false;\n const abortController = new AbortController();\n\n decodeAudioPeaks(src, abortController.signal)\n .then((decodedPeaks) => {\n if (!cancelled) setPeaks(decodedPeaks);\n })\n .catch(() => {\n if (!cancelled) setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));\n });\n\n return () => {\n cancelled = true;\n abortController.abort();\n };\n }, [src]);\n\n // Measure the track to decide how many bars fit; falls back to DEFAULT_BAR_COUNT\n // when layout is unavailable (SSR, jsdom).\n React.useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n\n const updateFromWidth = (width: number) => {\n if (width <= 0) return;\n setBarCount(Math.max(1, Math.floor((width + BAR_GAP_PX) / (BAR_WIDTH_PX + BAR_GAP_PX))));\n };\n\n updateFromWidth(track.getBoundingClientRect().width);\n\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (entry) updateFromWidth(entry.contentRect.width);\n });\n observer.observe(track);\n return () => observer.disconnect();\n }, []);\n\n // Sync the controlled/uncontrolled `playing` state to the media element.\n React.useEffect(() => {\n const audio = audioRef.current;\n if (!audio) return;\n if (playing) {\n audio.play().catch(() => {\n if (!isControlled) setInternalPlaying(false);\n onPauseRef.current?.();\n });\n } else {\n audio.pause();\n }\n }, [playing, isControlled]);\n\n const handleToggle = () => {\n const next = !playing;\n if (!isControlled) setInternalPlaying(next);\n if (next) onPlay?.();\n else onPause?.();\n };\n\n const seekTo = React.useCallback(\n (time: number) => {\n if (displayDuration === undefined) return;\n const clamped = Math.min(Math.max(time, 0), displayDuration);\n setCurrentTime(clamped);\n const audio = audioRef.current;\n if (audio) audio.currentTime = clamped;\n },\n [displayDuration],\n );\n\n const seekFromClientX = React.useCallback(\n (clientX: number) => {\n const track = trackRef.current;\n if (!track || displayDuration === undefined) return;\n const rect = track.getBoundingClientRect();\n if (rect.width <= 0) return;\n const fraction = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1);\n seekTo(fraction * displayDuration);\n },\n [displayDuration, seekTo],\n );\n\n // Track drag outside the element via window listeners rather than pointer\n // capture, which some environments (and jsdom) don't fully implement.\n React.useEffect(() => {\n if (!isDragging) return;\n const handleWindowPointerMove = (event: PointerEvent) => seekFromClientX(event.clientX);\n const handleWindowPointerUp = () => setIsDragging(false);\n window.addEventListener(\"pointermove\", handleWindowPointerMove);\n window.addEventListener(\"pointerup\", handleWindowPointerUp);\n return () => {\n window.removeEventListener(\"pointermove\", handleWindowPointerMove);\n window.removeEventListener(\"pointerup\", handleWindowPointerUp);\n };\n }, [isDragging, seekFromClientX]);\n\n const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n seekFromClientX(event.clientX);\n setIsDragging(true);\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (displayDuration === undefined) return;\n switch (event.key) {\n case \"ArrowRight\":\n case \"ArrowUp\":\n event.preventDefault();\n seekTo(currentTime + SEEK_STEP_SECONDS);\n break;\n case \"ArrowLeft\":\n case \"ArrowDown\":\n event.preventDefault();\n seekTo(currentTime - SEEK_STEP_SECONDS);\n break;\n case \"PageUp\":\n event.preventDefault();\n seekTo(currentTime + SEEK_STEP_LARGE_SECONDS);\n break;\n case \"PageDown\":\n event.preventDefault();\n seekTo(currentTime - SEEK_STEP_LARGE_SECONDS);\n break;\n case \"Home\":\n event.preventDefault();\n seekTo(0);\n break;\n case \"End\":\n event.preventDefault();\n seekTo(displayDuration);\n break;\n default:\n break;\n }\n };\n\n const bars = resamplePeaks(peaks, barCount);\n const progressFraction =\n displayDuration && displayDuration > 0\n ? Math.min(Math.max(currentTime / displayDuration, 0), 1)\n : 0;\n const playedBarCount = hasStarted ? Math.round(progressFraction * bars.length) : bars.length;\n const barData = bars.map((amplitude, index) => {\n const isPlayed = index < playedBarCount;\n return {\n heightPx: MIN_BAR_HEIGHT_PX + amplitude * (MAX_BAR_HEIGHT_PX - MIN_BAR_HEIGHT_PX),\n className: cn(\n \"w-1 shrink-0 rounded-3xs bg-messages-waveform-default\",\n \"motion-safe:transition-opacity motion-safe:duration-150 motion-safe:ease-in-out\",\n isPlayed ? \"opacity-100\" : \"opacity-50\",\n ),\n };\n });\n\n return (\n <div\n ref={ref}\n data-size={size}\n className={cn(\"flex w-full items-center gap-3\", className)}\n {...props}\n >\n <button\n type=\"button\"\n onClick={handleToggle}\n aria-label={playing ? \"Pause\" : \"Play\"}\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-full\",\n \"bg-buttons-secondary-default text-icons-primary backdrop-blur-[40px]\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"hover:bg-buttons-secondary-hover\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n )}\n >\n {playing ? <PauseIcon size={16} filled /> : <PlayIcon size={16} filled />}\n </button>\n\n <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n <div\n ref={trackRef}\n role=\"slider\"\n tabIndex={0}\n aria-label=\"Seek\"\n aria-orientation=\"horizontal\"\n aria-valuemin={0}\n aria-valuemax={displayDuration}\n aria-valuenow={Math.round(currentTime)}\n aria-valuetext={\n displayDuration !== undefined\n ? `${formatTime(currentTime)} of ${formatTime(displayDuration)}`\n : formatTime(currentTime)\n }\n onPointerDown={handlePointerDown}\n onKeyDown={handleKeyDown}\n className={cn(\n \"flex h-10 min-w-0 flex-1 cursor-pointer touch-none select-none items-center gap-1 overflow-hidden\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n )}\n >\n {barData.map((bar, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: bars are a fixed-count, non-reorderable list\n <span key={index} className={bar.className} style={{ height: bar.heightPx }} />\n ))}\n </div>\n\n <span className=\"typography-body-small-14px-semibold flex shrink-0 items-center gap-0.5 whitespace-nowrap\">\n {hasStarted ? (\n <>\n <span className=\"text-content-primary\">{formatTime(currentTime)}</span>\n <span className=\"text-content-secondary\">/{formatTime(displayDuration)}</span>\n </>\n ) : (\n <span className=\"text-content-primary\">{formatTime(displayDuration)}</span>\n )}\n </span>\n </div>\n\n {/* biome-ignore lint/a11y/useMediaCaption: this is a UI sound clip (voice note / short recording), not spoken/narrated content requiring captions. */}\n <audio\n ref={audioRef}\n src={src}\n preload=\"metadata\"\n className=\"hidden\"\n onLoadedMetadata={(event) => {\n const { duration: nativeDuration } = event.currentTarget;\n setMediaDuration(Number.isFinite(nativeDuration) ? nativeDuration : undefined);\n }}\n onTimeUpdate={(event) => {\n if (isDragging) return;\n setCurrentTime(event.currentTarget.currentTime);\n }}\n onEnded={() => {\n setCurrentTime(0);\n if (!isControlled) setInternalPlaying(false);\n onEnded?.();\n }}\n />\n </div>\n );\n },\n);\n\nAudioPlayer.displayName = \"AudioPlayer\";\n"],"names":["React","cn","jsxs","jsx","PauseIcon","PlayIcon","Fragment"],"mappings":"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"}
1
+ {"version":3,"file":"AudioPlayer.cjs","sources":["../../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["import * as React from \"react\";\nimport {\n decodeAudioPeaks,\n formatTime,\n generateFallbackPeaks,\n RAW_PEAK_COUNT,\n resamplePeaks,\n} from \"../../utils/audioWaveform\";\nimport { cn } from \"../../utils/cn\";\nimport { useFittedBarCount } from \"../../utils/useFittedBarCount\";\nimport { PauseIcon } from \"../Icons/PauseIcon\";\nimport { PlayIcon } from \"../Icons/PlayIcon\";\n\n/** Width of a single waveform bar, in pixels. */\nconst BAR_WIDTH_PX = 4;\n/** Gap between waveform bars, in pixels. */\nconst BAR_GAP_PX = 4;\n/** Shortest a waveform bar can render, in pixels. */\nconst MIN_BAR_HEIGHT_PX = 4;\n/** Tallest a waveform bar can render, in pixels — matches the 40px row height. */\nconst MAX_BAR_HEIGHT_PX = 26;\n/** Bar count used before the container has been measured (e.g. during SSR or in jsdom, where layout is unavailable). */\nconst DEFAULT_BAR_COUNT = 24;\n/** Seconds moved per Arrow key press while seeking. */\nconst SEEK_STEP_SECONDS = 1;\n/** Seconds moved per Page Up/Down press while seeking. */\nconst SEEK_STEP_LARGE_SECONDS = 5;\n\n/** Height of the audio player row, in pixels. Matches both the Vault card overlay and the \"Generated Audio\" modal row in Figma — both use an identical 32px play button and 40px row. */\nexport type AudioPlayerSize = \"40\";\n\nexport interface AudioPlayerProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onPlay\" | \"onPause\" | \"onEnded\"> {\n /** URL of the audio file to play. */\n src: string;\n /**\n * Total duration in seconds, used to render the timestamp before the\n * media's own metadata has loaded (and as a fallback if it never does).\n */\n duration?: number;\n /**\n * Whether playback is active (controlled). Note: once `onEnded` fires, the\n * browser has already stopped native playback. To loop or replay under\n * controlled usage, toggle this prop `false` then `true` (rather than\n * leaving it `true`) — the sync effect only calls `play()` again when this\n * value actually changes.\n */\n playing?: boolean;\n /** Initial playback state (uncontrolled). @default false */\n defaultPlaying?: boolean;\n /** Called when playback starts. */\n onPlay?: () => void;\n /** Called when playback is paused. */\n onPause?: () => void;\n /** Called when playback reaches the end of the media. */\n onEnded?: () => void;\n /** Height of the player row, in pixels. @default \"40\" */\n size?: AudioPlayerSize;\n}\n\n/**\n * A compact playback control for a single audio clip: a play/pause toggle, a\n * static amplitude waveform that doubles as a seek scrubber, and elapsed/total\n * timestamps. Designed to sit as an overlay on media thumbnails (Vault cards)\n * as well as inline rows (the AI Voice Message \"Generated Audio\" row).\n *\n * The waveform is seeded from the decoded audio when possible, falling back\n * to a deterministic (not random) placeholder derived from `src` so the\n * result is stable across renders, SSR, and Chromatic snapshots.\n *\n * @example\n * ```tsx\n * <AudioPlayer src=\"https://example.com/clip.mp3\" duration={5} />\n * ```\n */\nexport const AudioPlayer = React.forwardRef<HTMLDivElement, AudioPlayerProps>(\n (\n {\n className,\n src,\n duration,\n playing: controlledPlaying,\n defaultPlaying = false,\n onPlay,\n onPause,\n onEnded,\n size = \"40\",\n ...props\n },\n ref,\n ) => {\n const audioRef = React.useRef<HTMLAudioElement>(null);\n const trackRef = React.useRef<HTMLDivElement>(null);\n\n const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);\n const isControlled = controlledPlaying !== undefined;\n const playing = isControlled ? controlledPlaying : internalPlaying;\n\n const [currentTime, setCurrentTime] = React.useState(0);\n const [mediaDuration, setMediaDuration] = React.useState<number | undefined>(undefined);\n const barCount = useFittedBarCount(trackRef, {\n barWidthPx: BAR_WIDTH_PX,\n gapPx: BAR_GAP_PX,\n fallback: DEFAULT_BAR_COUNT,\n });\n const [peaks, setPeaks] = React.useState<number[]>(() =>\n generateFallbackPeaks(src, RAW_PEAK_COUNT),\n );\n const [isDragging, setIsDragging] = React.useState(false);\n\n const displayDuration = mediaDuration ?? duration;\n const hasStarted = playing || currentTime > 0;\n\n // Latest-value refs so the src-change effect below can react to a src swap\n // without re-running whenever `playing`/`isControlled`/`onPause` change.\n const playingRef = React.useRef(playing);\n playingRef.current = playing;\n const isControlledRef = React.useRef(isControlled);\n isControlledRef.current = isControlled;\n const onPauseRef = React.useRef(onPause);\n onPauseRef.current = onPause;\n\n // Reset transient playback state when the source changes. Skips the\n // initial mount (there's nothing to reset yet) and, if playback was\n // active, stops it — the browser silently drops playback on a source\n // swap, so the UI (and any controlled parent) must be told via onPause.\n const isFirstSrcRenderRef = React.useRef(true);\n React.useEffect(() => {\n if (isFirstSrcRenderRef.current) {\n isFirstSrcRenderRef.current = false;\n return;\n }\n setCurrentTime(0);\n setMediaDuration(undefined);\n setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));\n if (playingRef.current) {\n if (!isControlledRef.current) setInternalPlaying(false);\n onPauseRef.current?.();\n }\n }, [src]);\n\n // Decode the audio to derive real waveform amplitudes, falling back to a\n // deterministic placeholder on any failure (network, CORS, unsupported format).\n React.useEffect(() => {\n let cancelled = false;\n const abortController = new AbortController();\n\n decodeAudioPeaks(src, abortController.signal)\n .then((decodedPeaks) => {\n if (!cancelled) setPeaks(decodedPeaks);\n })\n .catch(() => {\n if (!cancelled) setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));\n });\n\n return () => {\n cancelled = true;\n abortController.abort();\n };\n }, [src]);\n\n // Sync the controlled/uncontrolled `playing` state to the media element.\n React.useEffect(() => {\n const audio = audioRef.current;\n if (!audio) return;\n if (playing) {\n audio.play().catch(() => {\n if (!isControlled) setInternalPlaying(false);\n onPauseRef.current?.();\n });\n } else {\n audio.pause();\n }\n }, [playing, isControlled]);\n\n const handleToggle = () => {\n const next = !playing;\n if (!isControlled) setInternalPlaying(next);\n if (next) onPlay?.();\n else onPause?.();\n };\n\n const seekTo = React.useCallback(\n (time: number) => {\n if (displayDuration === undefined) return;\n const clamped = Math.min(Math.max(time, 0), displayDuration);\n setCurrentTime(clamped);\n const audio = audioRef.current;\n if (audio) audio.currentTime = clamped;\n },\n [displayDuration],\n );\n\n const seekFromClientX = React.useCallback(\n (clientX: number) => {\n const track = trackRef.current;\n if (!track || displayDuration === undefined) return;\n const rect = track.getBoundingClientRect();\n if (rect.width <= 0) return;\n const fraction = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1);\n seekTo(fraction * displayDuration);\n },\n [displayDuration, seekTo],\n );\n\n // Track drag outside the element via window listeners rather than pointer\n // capture, which some environments (and jsdom) don't fully implement.\n React.useEffect(() => {\n if (!isDragging) return;\n const handleWindowPointerMove = (event: PointerEvent) => seekFromClientX(event.clientX);\n const handleWindowPointerUp = () => setIsDragging(false);\n window.addEventListener(\"pointermove\", handleWindowPointerMove);\n window.addEventListener(\"pointerup\", handleWindowPointerUp);\n return () => {\n window.removeEventListener(\"pointermove\", handleWindowPointerMove);\n window.removeEventListener(\"pointerup\", handleWindowPointerUp);\n };\n }, [isDragging, seekFromClientX]);\n\n const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n seekFromClientX(event.clientX);\n setIsDragging(true);\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (displayDuration === undefined) return;\n switch (event.key) {\n case \"ArrowRight\":\n case \"ArrowUp\":\n event.preventDefault();\n seekTo(currentTime + SEEK_STEP_SECONDS);\n break;\n case \"ArrowLeft\":\n case \"ArrowDown\":\n event.preventDefault();\n seekTo(currentTime - SEEK_STEP_SECONDS);\n break;\n case \"PageUp\":\n event.preventDefault();\n seekTo(currentTime + SEEK_STEP_LARGE_SECONDS);\n break;\n case \"PageDown\":\n event.preventDefault();\n seekTo(currentTime - SEEK_STEP_LARGE_SECONDS);\n break;\n case \"Home\":\n event.preventDefault();\n seekTo(0);\n break;\n case \"End\":\n event.preventDefault();\n seekTo(displayDuration);\n break;\n default:\n break;\n }\n };\n\n const bars = resamplePeaks(peaks, barCount);\n const progressFraction =\n displayDuration && displayDuration > 0\n ? Math.min(Math.max(currentTime / displayDuration, 0), 1)\n : 0;\n const playedBarCount = hasStarted ? Math.round(progressFraction * bars.length) : bars.length;\n const barData = bars.map((amplitude, index) => {\n const isPlayed = index < playedBarCount;\n return {\n heightPx: MIN_BAR_HEIGHT_PX + amplitude * (MAX_BAR_HEIGHT_PX - MIN_BAR_HEIGHT_PX),\n className: cn(\n \"w-1 shrink-0 rounded-3xs bg-messages-waveform-default\",\n \"motion-safe:transition-opacity motion-safe:duration-150 motion-safe:ease-in-out\",\n isPlayed ? \"opacity-100\" : \"opacity-50\",\n ),\n };\n });\n\n return (\n <div\n ref={ref}\n data-size={size}\n className={cn(\"flex w-full items-center gap-3\", className)}\n {...props}\n >\n <button\n type=\"button\"\n onClick={handleToggle}\n aria-label={playing ? \"Pause\" : \"Play\"}\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-full\",\n \"bg-buttons-secondary-default text-icons-primary backdrop-blur-[40px]\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"hover:bg-buttons-secondary-hover\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n )}\n >\n {playing ? <PauseIcon size={16} filled /> : <PlayIcon size={16} filled />}\n </button>\n\n <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n <div\n ref={trackRef}\n role=\"slider\"\n tabIndex={0}\n aria-label=\"Seek\"\n aria-orientation=\"horizontal\"\n aria-valuemin={0}\n aria-valuemax={displayDuration}\n aria-valuenow={Math.round(currentTime)}\n aria-valuetext={\n displayDuration !== undefined\n ? `${formatTime(currentTime)} of ${formatTime(displayDuration)}`\n : formatTime(currentTime)\n }\n onPointerDown={handlePointerDown}\n onKeyDown={handleKeyDown}\n className={cn(\n \"flex h-10 min-w-0 flex-1 cursor-pointer touch-none select-none items-center gap-1 overflow-hidden\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n )}\n >\n {barData.map((bar, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: bars are a fixed-count, non-reorderable list\n <span key={index} className={bar.className} style={{ height: bar.heightPx }} />\n ))}\n </div>\n\n <span className=\"typography-body-small-14px-semibold flex shrink-0 items-center gap-0.5 whitespace-nowrap\">\n {hasStarted ? (\n <>\n <span className=\"text-content-primary\">{formatTime(currentTime)}</span>\n <span className=\"text-content-secondary\">/{formatTime(displayDuration)}</span>\n </>\n ) : (\n <span className=\"text-content-primary\">{formatTime(displayDuration)}</span>\n )}\n </span>\n </div>\n\n {/* biome-ignore lint/a11y/useMediaCaption: this is a UI sound clip (voice note / short recording), not spoken/narrated content requiring captions. */}\n <audio\n ref={audioRef}\n src={src}\n preload=\"metadata\"\n className=\"hidden\"\n onLoadedMetadata={(event) => {\n const { duration: nativeDuration } = event.currentTarget;\n setMediaDuration(Number.isFinite(nativeDuration) ? nativeDuration : undefined);\n }}\n onTimeUpdate={(event) => {\n if (isDragging) return;\n setCurrentTime(event.currentTarget.currentTime);\n }}\n onEnded={() => {\n setCurrentTime(0);\n if (!isControlled) setInternalPlaying(false);\n onEnded?.();\n }}\n />\n </div>\n );\n },\n);\n\nAudioPlayer.displayName = \"AudioPlayer\";\n"],"names":["React","useFittedBarCount","generateFallbackPeaks","RAW_PEAK_COUNT","decodeAudioPeaks","resamplePeaks","cn","jsxs","jsx","PauseIcon","PlayIcon","formatTime","Fragment"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
+ const jsxRuntime = require("react/jsx-runtime");
5
+ const React = require("react");
6
+ const IconButton = require("../IconButton/IconButton.cjs");
7
+ const MicrophoneIcon = require("../Icons/MicrophoneIcon.cjs");
8
+ const StopIcon = require("../Icons/StopIcon.cjs");
9
+ function _interopNamespaceDefault(e) {
10
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
11
+ if (e) {
12
+ for (const k in e) {
13
+ if (k !== "default") {
14
+ const d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: () => e[k]
18
+ });
19
+ }
20
+ }
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+ const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
26
+ const STATUS_VARIANT = {
27
+ idle: "primary",
28
+ recording: "error"
29
+ };
30
+ const STATUS_ICON = {
31
+ idle: /* @__PURE__ */ jsxRuntime.jsx(MicrophoneIcon.MicrophoneIcon, {}),
32
+ recording: /* @__PURE__ */ jsxRuntime.jsx(StopIcon.StopIcon, {})
33
+ };
34
+ const STATUS_LABEL = {
35
+ idle: "Record",
36
+ recording: "Stop recording"
37
+ };
38
+ const AudioRecordButton = React__namespace.forwardRef(
39
+ ({ status = "idle", "aria-label": ariaLabel, ...props }, ref) => {
40
+ return /* @__PURE__ */ jsxRuntime.jsx(
41
+ IconButton.IconButton,
42
+ {
43
+ ref,
44
+ variant: STATUS_VARIANT[status],
45
+ icon: STATUS_ICON[status],
46
+ "aria-label": ariaLabel ?? STATUS_LABEL[status],
47
+ ...props
48
+ }
49
+ );
50
+ }
51
+ );
52
+ AudioRecordButton.displayName = "AudioRecordButton";
53
+ exports.AudioRecordButton = AudioRecordButton;
54
+ //# sourceMappingURL=AudioRecordButton.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioRecordButton.cjs","sources":["../../../../src/components/AudioRecordButton/AudioRecordButton.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { IconButton, type IconButtonProps, type IconButtonVariant } from \"../IconButton/IconButton\";\nimport { MicrophoneIcon } from \"../Icons/MicrophoneIcon\";\nimport { StopIcon } from \"../Icons/StopIcon\";\n\n/**\n * Recording state of the button. Drives the icon and colour: `idle` shows the\n * microphone to start a recording, `recording` shows the stop glyph with the\n * destructive treatment to end it.\n */\nexport type AudioRecordButtonStatus = \"idle\" | \"recording\";\n\nconst STATUS_VARIANT: Record<AudioRecordButtonStatus, IconButtonVariant> = {\n idle: \"primary\",\n recording: \"error\",\n};\n\nconst STATUS_ICON: Record<AudioRecordButtonStatus, React.ReactNode> = {\n idle: <MicrophoneIcon />,\n recording: <StopIcon />,\n};\n\nconst STATUS_LABEL: Record<AudioRecordButtonStatus, string> = {\n idle: \"Record\",\n recording: \"Stop recording\",\n};\n\nexport interface AudioRecordButtonProps\n extends Omit<IconButtonProps, \"icon\" | \"variant\" | \"aria-label\"> {\n /** Recording state; drives the icon and colour. @default \"idle\" */\n status?: AudioRecordButtonStatus;\n /**\n * Accessible name. Falls back to a status-aware default (\"Record\" /\n * \"Stop recording\") when omitted so screen readers announce the action.\n */\n \"aria-label\"?: string;\n}\n\n/**\n * A circular icon button that toggles audio recording. It owns the record\n * lifecycle so consumers flip a single `status` prop instead of swapping an\n * {@link IconButton}'s icon and colour: `idle` shows the microphone to start,\n * `recording` turns destructive with a stop glyph to end. Used by\n * {@link AudioUpload} for its in-progress stop control.\n *\n * @example\n * ```tsx\n * <AudioRecordButton status={recording ? \"recording\" : \"idle\"} onClick={toggle} />\n * ```\n */\nexport const AudioRecordButton = React.forwardRef<HTMLButtonElement, AudioRecordButtonProps>(\n ({ status = \"idle\", \"aria-label\": ariaLabel, ...props }, ref) => {\n return (\n <IconButton\n ref={ref}\n variant={STATUS_VARIANT[status]}\n icon={STATUS_ICON[status]}\n aria-label={ariaLabel ?? STATUS_LABEL[status]}\n {...props}\n />\n );\n },\n);\n\nAudioRecordButton.displayName = \"AudioRecordButton\";\n"],"names":["MicrophoneIcon","StopIcon","React","jsx","IconButton"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAYA,MAAM,iBAAqE;AAAA,EACzE,MAAM;AAAA,EACN,WAAW;AACb;AAEA,MAAM,cAAgE;AAAA,EACpE,qCAAOA,eAAAA,gBAAA,EAAe;AAAA,EACtB,0CAAYC,SAAAA,UAAA,CAAA,CAAS;AACvB;AAEA,MAAM,eAAwD;AAAA,EAC5D,MAAM;AAAA,EACN,WAAW;AACb;AAyBO,MAAM,oBAAoBC,iBAAM;AAAA,EACrC,CAAC,EAAE,SAAS,QAAQ,cAAc,WAAW,GAAG,MAAA,GAAS,QAAQ;AAC/D,WACEC,2BAAAA;AAAAA,MAACC,WAAAA;AAAAA,MAAA;AAAA,QACC;AAAA,QACA,SAAS,eAAe,MAAM;AAAA,QAC9B,MAAM,YAAY,MAAM;AAAA,QACxB,cAAY,aAAa,aAAa,MAAM;AAAA,QAC3C,GAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAGV;AACF;AAEA,kBAAkB,cAAc;;"}
@@ -4,9 +4,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const jsxRuntime = require("react/jsx-runtime");
5
5
  const React = require("react");
6
6
  const cn = require("../../utils/cn.cjs");
7
+ const AudioRecordButton = require("../AudioRecordButton/AudioRecordButton.cjs");
7
8
  const Button = require("../Button/Button.cjs");
8
9
  const MicrophoneIcon = require("../Icons/MicrophoneIcon.cjs");
9
- const StopIcon = require("../Icons/StopIcon.cjs");
10
10
  const UploadCloudIcon = require("../Icons/UploadCloudIcon.cjs");
11
11
  const AudioWaveform = require("./AudioWaveform.cjs");
12
12
  const audioUtils = require("./audioUtils.cjs");
@@ -198,14 +198,14 @@ const AudioUpload = React__namespace.forwardRef(
198
198
  }
199
199
  ) }),
200
200
  /* @__PURE__ */ jsxRuntime.jsx(
201
- "button",
201
+ AudioRecordButton.AudioRecordButton,
202
202
  {
203
203
  ref: stopButtonRef,
204
- type: "button",
204
+ status: "recording",
205
+ size: "40",
205
206
  onClick: handleStopClick,
206
- className: "mt-1 flex size-11 items-center justify-center rounded-full bg-error-content text-content-always-white transition-colors hover:bg-error-content/80 focus:shadow-focus-ring focus-visible:outline-none",
207
207
  "aria-label": stopButtonAriaLabel,
208
- children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon.StopIcon, { className: "size-5" })
208
+ className: "mt-1"
209
209
  }
210
210
  )
211
211
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"AudioUpload.cjs","sources":["../../../../src/components/AudioUpload/AudioUpload.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button/Button\";\nimport { MicrophoneIcon } from \"../Icons/MicrophoneIcon\";\nimport { StopIcon } from \"../Icons/StopIcon\";\nimport { UploadCloudIcon } from \"../Icons/UploadCloudIcon\";\nimport { AudioWaveform } from \"./AudioWaveform\";\nimport { type AudioValidationError, formatAudioTime, validateAudioFile } from \"./audioUtils\";\nimport {\n DEFAULT_ACCEPTED_TYPES,\n DEFAULT_MAX_FILE_SIZE,\n DEFAULT_MAX_RECORDING_DURATION,\n DEFAULT_MIN_RECORDING_DURATION,\n} from \"./constants\";\nimport { useAudioRecorder } from \"./useAudioRecorder\";\n\n/** A file that was rejected during drop or browse, along with the reasons. */\nexport interface AudioFileRejection {\n /** The rejected file. */\n file: File;\n /** One or more validation errors explaining why the file was rejected. */\n errors: AudioValidationError[];\n}\n\nexport interface AudioUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onDrop\"> {\n /** Maximum file size in bytes. @default 10_485_760 (10MB) */\n maxFileSize?: number;\n /** Accepted audio MIME types. @default DEFAULT_ACCEPTED_TYPES */\n accept?: readonly string[];\n /** Maximum number of files per drop. @default 1 */\n maxFiles?: number;\n /** Whether to show the record audio button. @default true */\n allowRecording?: boolean;\n /** Maximum recording duration in seconds. @default 30 */\n maxRecordingDuration?: number;\n /** Minimum recording duration in seconds. @default 5 */\n minRecordingDuration?: number;\n\n /** Called when valid files are accepted via drop or browse */\n onFilesAccepted?: (files: File[]) => void;\n /** Called when files are rejected (wrong type, too large, etc.) */\n onFilesRejected?: (rejections: AudioFileRejection[]) => void;\n /** Called when a recording completes and meets minimum duration */\n onRecordingComplete?: (blob: Blob, durationMs: number) => void;\n /** Called when recording is stopped but does not meet minimum duration */\n onRecordingTooShort?: (durationMs: number, minDurationMs: number) => void;\n /** Called when microphone permission is denied or unavailable */\n onPermissionError?: (error: Error) => void;\n /** Called when an unexpected recording error occurs */\n onRecordingError?: (error: Error) => void;\n\n /** Upload area title text. @default \"Click to upload, or drag & drop\" */\n uploadTitle?: string;\n /** Upload area description text. @default \"Audio files only, up to 10MB each\" */\n uploadDescription?: string;\n /** Separator text between upload and record. @default \"or\" */\n separatorText?: string;\n /** Record button label. @default \"Record audio\" */\n recordButtonLabel?: string;\n /** Stop recording button aria-label. @default \"Stop recording\" */\n stopButtonAriaLabel?: string;\n\n /** Whether the component is disabled. @default false */\n disabled?: boolean;\n}\n\nfunction partitionFiles(\n files: File[],\n maxFileSize: number,\n accept: readonly string[],\n maxFiles: number,\n): { accepted: File[]; rejected: AudioFileRejection[] } {\n const accepted: File[] = [];\n const rejected: AudioFileRejection[] = [];\n\n for (const file of files) {\n const errors = validateAudioFile(file, { maxFileSize, acceptedTypes: accept });\n if (errors.length > 0) {\n rejected.push({ file, errors });\n } else {\n accepted.push(file);\n }\n }\n\n if (maxFiles > 0 && accepted.length > maxFiles) {\n const excess = accepted.splice(maxFiles);\n for (const file of excess) {\n rejected.push({\n file,\n errors: [{ code: \"too-many-files\", message: `Too many files. Maximum is ${maxFiles}` }],\n });\n }\n }\n\n return { accepted, rejected };\n}\n\n/**\n * Audio file upload with drag-and-drop and optional in-browser recording.\n * Supports file validation, multiple files, and real-time waveform visualization during recording.\n *\n * @example\n * ```tsx\n * <AudioUpload\n * onFilesAccepted={(files) => console.log(files)}\n * onRecordingComplete={(blob, duration) => console.log(blob, duration)}\n * />\n * ```\n */\nexport const AudioUpload = React.forwardRef<HTMLDivElement, AudioUploadProps>(\n (\n {\n className,\n maxFileSize = DEFAULT_MAX_FILE_SIZE,\n accept = DEFAULT_ACCEPTED_TYPES,\n maxFiles = 1,\n allowRecording = true,\n maxRecordingDuration = DEFAULT_MAX_RECORDING_DURATION,\n minRecordingDuration = DEFAULT_MIN_RECORDING_DURATION,\n onFilesAccepted,\n onFilesRejected,\n onRecordingComplete,\n onRecordingTooShort,\n onPermissionError,\n onRecordingError,\n uploadTitle = \"Click to upload, or drag & drop\",\n uploadDescription = \"Audio files only, up to 10MB each\",\n separatorText = \"or\",\n recordButtonLabel = \"Record audio\",\n stopButtonAriaLabel = \"Stop recording\",\n disabled = false,\n ...props\n },\n ref,\n ) => {\n const inputId = React.useId();\n const descriptionId = React.useId();\n const [isDragActive, setIsDragActive] = React.useState(false);\n const stopButtonRef = React.useRef<HTMLButtonElement>(null);\n\n const {\n isRecording,\n elapsedMs,\n startRecording,\n stopRecording,\n analyserNode,\n isSupported: isRecordingSupported,\n } = useAudioRecorder({\n maxDuration: maxRecordingDuration,\n minDuration: minRecordingDuration,\n onComplete: onRecordingComplete,\n onTooShort: onRecordingTooShort,\n onPermissionError,\n onError: onRecordingError,\n });\n\n const acceptString = accept.join(\",\");\n\n // Move focus to stop button when recording starts\n React.useEffect(() => {\n if (isRecording) {\n stopButtonRef.current?.focus();\n }\n }, [isRecording]);\n\n const validateAndAcceptFiles = React.useCallback(\n (files: FileList | File[]) => {\n const { accepted, rejected } = partitionFiles(\n Array.from(files),\n maxFileSize,\n accept,\n maxFiles,\n );\n if (accepted.length > 0) onFilesAccepted?.(accepted);\n if (rejected.length > 0) onFilesRejected?.(rejected);\n },\n [maxFileSize, accept, maxFiles, onFilesAccepted, onFilesRejected],\n );\n\n const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragActive(false);\n\n if (disabled) return;\n\n const { files } = e.dataTransfer;\n if (files.length > 0) {\n validateAndAcceptFiles(files);\n }\n };\n\n const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n if (!disabled) {\n setIsDragActive(true);\n }\n };\n\n const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragActive(false);\n };\n\n const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const { files } = e.target;\n if (files && files.length > 0) {\n validateAndAcceptFiles(files);\n }\n // Reset input so same file can be selected again\n e.target.value = \"\";\n };\n\n const handleRecordClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n startRecording();\n };\n\n const handleStopClick = () => {\n stopRecording();\n };\n\n if (isRecording) {\n const formattedElapsed = formatAudioTime(elapsedMs);\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=\"Audio recording in progress\"\n data-testid=\"audio-upload\"\n data-state=\"recording\"\n className={cn(\n \"flex flex-col items-center justify-center gap-2 rounded-md bg-surface-secondary px-4 py-3\",\n className,\n )}\n {...props}\n >\n <div className=\"flex flex-1 flex-col items-center gap-2\">\n <div\n className=\"flex size-[72px] items-center justify-center rounded-full bg-buttons-primary-default\"\n aria-hidden=\"true\"\n >\n <MicrophoneIcon className=\"size-5 text-content-primary-inverted\" />\n </div>\n\n <p\n role=\"timer\"\n aria-label=\"Recording time\"\n className=\"typography-body-default-16px-regular text-content-primary\"\n >\n {formattedElapsed} / {formatAudioTime(maxRecordingDuration * 1000)}\n </p>\n </div>\n\n <div className=\"flex w-full items-center gap-2.5\" aria-hidden=\"true\">\n <AudioWaveform\n analyserNode={analyserNode}\n isRecording={isRecording}\n className=\"flex-1\"\n />\n </div>\n\n <button\n ref={stopButtonRef}\n type=\"button\"\n onClick={handleStopClick}\n className=\"mt-1 flex size-11 items-center justify-center rounded-full bg-error-content text-content-always-white transition-colors hover:bg-error-content/80 focus:shadow-focus-ring focus-visible:outline-none\"\n aria-label={stopButtonAriaLabel}\n >\n <StopIcon className=\"size-5\" />\n </button>\n </div>\n );\n }\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=\"Audio upload\"\n data-testid=\"audio-upload\"\n data-state=\"idle\"\n aria-disabled={disabled || undefined}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n className={cn(\n \"flex flex-col items-center justify-center gap-2 rounded-md bg-surface-secondary px-4 py-3 transition-colors\",\n isDragActive && \"bg-brand-primary-muted ring-2 ring-brand-primary-default\",\n disabled && \"pointer-events-none opacity-50\",\n className,\n )}\n {...props}\n >\n <input\n id={inputId}\n type=\"file\"\n accept={acceptString}\n multiple={maxFiles > 1}\n onChange={handleFileInputChange}\n className=\"peer sr-only\"\n disabled={disabled}\n aria-describedby={descriptionId}\n />\n\n <label\n htmlFor={inputId}\n className=\"flex cursor-pointer flex-col items-center gap-2 rounded-xs px-2 py-1 peer-focus-visible:shadow-focus-ring\"\n >\n <UploadCloudIcon className=\"size-5 text-content-primary\" />\n\n <span className=\"typography-body-default-16px-semibold text-center text-content-primary\">\n {uploadTitle}\n </span>\n\n <span\n id={descriptionId}\n className=\"typography-body-small-14px-regular text-center text-content-primary\"\n >\n {uploadDescription}\n </span>\n </label>\n\n {allowRecording && isRecordingSupported && (\n <>\n <p className=\"typography-body-small-14px-regular text-center text-content-primary\">\n {separatorText}\n </p>\n\n <Button\n variant=\"brand\"\n size=\"40\"\n leftIcon={<MicrophoneIcon className=\"size-5\" />}\n onClick={handleRecordClick}\n disabled={disabled}\n type=\"button\"\n >\n {recordButtonLabel}\n </Button>\n </>\n )}\n </div>\n );\n },\n);\n\nAudioUpload.displayName = \"AudioUpload\";\n"],"names":["validateAudioFile","React","DEFAULT_MAX_FILE_SIZE","DEFAULT_ACCEPTED_TYPES","DEFAULT_MAX_RECORDING_DURATION","DEFAULT_MIN_RECORDING_DURATION","useAudioRecorder","formatAudioTime","jsxs","cn","jsx","MicrophoneIcon","AudioWaveform","StopIcon","UploadCloudIcon","Fragment","Button"],"mappings":"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"}
1
+ {"version":3,"file":"AudioUpload.cjs","sources":["../../../../src/components/AudioUpload/AudioUpload.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { AudioRecordButton } from \"../AudioRecordButton/AudioRecordButton\";\nimport { Button } from \"../Button/Button\";\nimport { MicrophoneIcon } from \"../Icons/MicrophoneIcon\";\nimport { UploadCloudIcon } from \"../Icons/UploadCloudIcon\";\nimport { AudioWaveform } from \"./AudioWaveform\";\nimport { type AudioValidationError, formatAudioTime, validateAudioFile } from \"./audioUtils\";\nimport {\n DEFAULT_ACCEPTED_TYPES,\n DEFAULT_MAX_FILE_SIZE,\n DEFAULT_MAX_RECORDING_DURATION,\n DEFAULT_MIN_RECORDING_DURATION,\n} from \"./constants\";\nimport { useAudioRecorder } from \"./useAudioRecorder\";\n\n/** A file that was rejected during drop or browse, along with the reasons. */\nexport interface AudioFileRejection {\n /** The rejected file. */\n file: File;\n /** One or more validation errors explaining why the file was rejected. */\n errors: AudioValidationError[];\n}\n\nexport interface AudioUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onDrop\"> {\n /** Maximum file size in bytes. @default 10_485_760 (10MB) */\n maxFileSize?: number;\n /** Accepted audio MIME types. @default DEFAULT_ACCEPTED_TYPES */\n accept?: readonly string[];\n /** Maximum number of files per drop. @default 1 */\n maxFiles?: number;\n /** Whether to show the record audio button. @default true */\n allowRecording?: boolean;\n /** Maximum recording duration in seconds. @default 30 */\n maxRecordingDuration?: number;\n /** Minimum recording duration in seconds. @default 5 */\n minRecordingDuration?: number;\n\n /** Called when valid files are accepted via drop or browse */\n onFilesAccepted?: (files: File[]) => void;\n /** Called when files are rejected (wrong type, too large, etc.) */\n onFilesRejected?: (rejections: AudioFileRejection[]) => void;\n /** Called when a recording completes and meets minimum duration */\n onRecordingComplete?: (blob: Blob, durationMs: number) => void;\n /** Called when recording is stopped but does not meet minimum duration */\n onRecordingTooShort?: (durationMs: number, minDurationMs: number) => void;\n /** Called when microphone permission is denied or unavailable */\n onPermissionError?: (error: Error) => void;\n /** Called when an unexpected recording error occurs */\n onRecordingError?: (error: Error) => void;\n\n /** Upload area title text. @default \"Click to upload, or drag & drop\" */\n uploadTitle?: string;\n /** Upload area description text. @default \"Audio files only, up to 10MB each\" */\n uploadDescription?: string;\n /** Separator text between upload and record. @default \"or\" */\n separatorText?: string;\n /** Record button label. @default \"Record audio\" */\n recordButtonLabel?: string;\n /** Stop recording button aria-label. @default \"Stop recording\" */\n stopButtonAriaLabel?: string;\n\n /** Whether the component is disabled. @default false */\n disabled?: boolean;\n}\n\nfunction partitionFiles(\n files: File[],\n maxFileSize: number,\n accept: readonly string[],\n maxFiles: number,\n): { accepted: File[]; rejected: AudioFileRejection[] } {\n const accepted: File[] = [];\n const rejected: AudioFileRejection[] = [];\n\n for (const file of files) {\n const errors = validateAudioFile(file, { maxFileSize, acceptedTypes: accept });\n if (errors.length > 0) {\n rejected.push({ file, errors });\n } else {\n accepted.push(file);\n }\n }\n\n if (maxFiles > 0 && accepted.length > maxFiles) {\n const excess = accepted.splice(maxFiles);\n for (const file of excess) {\n rejected.push({\n file,\n errors: [{ code: \"too-many-files\", message: `Too many files. Maximum is ${maxFiles}` }],\n });\n }\n }\n\n return { accepted, rejected };\n}\n\n/**\n * Audio file upload with drag-and-drop and optional in-browser recording.\n * Supports file validation, multiple files, and real-time waveform visualization during recording.\n *\n * @example\n * ```tsx\n * <AudioUpload\n * onFilesAccepted={(files) => console.log(files)}\n * onRecordingComplete={(blob, duration) => console.log(blob, duration)}\n * />\n * ```\n */\nexport const AudioUpload = React.forwardRef<HTMLDivElement, AudioUploadProps>(\n (\n {\n className,\n maxFileSize = DEFAULT_MAX_FILE_SIZE,\n accept = DEFAULT_ACCEPTED_TYPES,\n maxFiles = 1,\n allowRecording = true,\n maxRecordingDuration = DEFAULT_MAX_RECORDING_DURATION,\n minRecordingDuration = DEFAULT_MIN_RECORDING_DURATION,\n onFilesAccepted,\n onFilesRejected,\n onRecordingComplete,\n onRecordingTooShort,\n onPermissionError,\n onRecordingError,\n uploadTitle = \"Click to upload, or drag & drop\",\n uploadDescription = \"Audio files only, up to 10MB each\",\n separatorText = \"or\",\n recordButtonLabel = \"Record audio\",\n stopButtonAriaLabel = \"Stop recording\",\n disabled = false,\n ...props\n },\n ref,\n ) => {\n const inputId = React.useId();\n const descriptionId = React.useId();\n const [isDragActive, setIsDragActive] = React.useState(false);\n const stopButtonRef = React.useRef<HTMLButtonElement>(null);\n\n const {\n isRecording,\n elapsedMs,\n startRecording,\n stopRecording,\n analyserNode,\n isSupported: isRecordingSupported,\n } = useAudioRecorder({\n maxDuration: maxRecordingDuration,\n minDuration: minRecordingDuration,\n onComplete: onRecordingComplete,\n onTooShort: onRecordingTooShort,\n onPermissionError,\n onError: onRecordingError,\n });\n\n const acceptString = accept.join(\",\");\n\n // Move focus to stop button when recording starts\n React.useEffect(() => {\n if (isRecording) {\n stopButtonRef.current?.focus();\n }\n }, [isRecording]);\n\n const validateAndAcceptFiles = React.useCallback(\n (files: FileList | File[]) => {\n const { accepted, rejected } = partitionFiles(\n Array.from(files),\n maxFileSize,\n accept,\n maxFiles,\n );\n if (accepted.length > 0) onFilesAccepted?.(accepted);\n if (rejected.length > 0) onFilesRejected?.(rejected);\n },\n [maxFileSize, accept, maxFiles, onFilesAccepted, onFilesRejected],\n );\n\n const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragActive(false);\n\n if (disabled) return;\n\n const { files } = e.dataTransfer;\n if (files.length > 0) {\n validateAndAcceptFiles(files);\n }\n };\n\n const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n if (!disabled) {\n setIsDragActive(true);\n }\n };\n\n const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragActive(false);\n };\n\n const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const { files } = e.target;\n if (files && files.length > 0) {\n validateAndAcceptFiles(files);\n }\n // Reset input so same file can be selected again\n e.target.value = \"\";\n };\n\n const handleRecordClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n startRecording();\n };\n\n const handleStopClick = () => {\n stopRecording();\n };\n\n if (isRecording) {\n const formattedElapsed = formatAudioTime(elapsedMs);\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=\"Audio recording in progress\"\n data-testid=\"audio-upload\"\n data-state=\"recording\"\n className={cn(\n \"flex flex-col items-center justify-center gap-2 rounded-md bg-surface-secondary px-4 py-3\",\n className,\n )}\n {...props}\n >\n <div className=\"flex flex-1 flex-col items-center gap-2\">\n <div\n className=\"flex size-[72px] items-center justify-center rounded-full bg-buttons-primary-default\"\n aria-hidden=\"true\"\n >\n <MicrophoneIcon className=\"size-5 text-content-primary-inverted\" />\n </div>\n\n <p\n role=\"timer\"\n aria-label=\"Recording time\"\n className=\"typography-body-default-16px-regular text-content-primary\"\n >\n {formattedElapsed} / {formatAudioTime(maxRecordingDuration * 1000)}\n </p>\n </div>\n\n <div className=\"flex w-full items-center gap-2.5\" aria-hidden=\"true\">\n <AudioWaveform\n analyserNode={analyserNode}\n isRecording={isRecording}\n className=\"flex-1\"\n />\n </div>\n\n <AudioRecordButton\n ref={stopButtonRef}\n status=\"recording\"\n size=\"40\"\n onClick={handleStopClick}\n aria-label={stopButtonAriaLabel}\n className=\"mt-1\"\n />\n </div>\n );\n }\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=\"Audio upload\"\n data-testid=\"audio-upload\"\n data-state=\"idle\"\n aria-disabled={disabled || undefined}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n className={cn(\n \"flex flex-col items-center justify-center gap-2 rounded-md bg-surface-secondary px-4 py-3 transition-colors\",\n isDragActive && \"bg-brand-primary-muted ring-2 ring-brand-primary-default\",\n disabled && \"pointer-events-none opacity-50\",\n className,\n )}\n {...props}\n >\n <input\n id={inputId}\n type=\"file\"\n accept={acceptString}\n multiple={maxFiles > 1}\n onChange={handleFileInputChange}\n className=\"peer sr-only\"\n disabled={disabled}\n aria-describedby={descriptionId}\n />\n\n <label\n htmlFor={inputId}\n className=\"flex cursor-pointer flex-col items-center gap-2 rounded-xs px-2 py-1 peer-focus-visible:shadow-focus-ring\"\n >\n <UploadCloudIcon className=\"size-5 text-content-primary\" />\n\n <span className=\"typography-body-default-16px-semibold text-center text-content-primary\">\n {uploadTitle}\n </span>\n\n <span\n id={descriptionId}\n className=\"typography-body-small-14px-regular text-center text-content-primary\"\n >\n {uploadDescription}\n </span>\n </label>\n\n {allowRecording && isRecordingSupported && (\n <>\n <p className=\"typography-body-small-14px-regular text-center text-content-primary\">\n {separatorText}\n </p>\n\n <Button\n variant=\"brand\"\n size=\"40\"\n leftIcon={<MicrophoneIcon className=\"size-5\" />}\n onClick={handleRecordClick}\n disabled={disabled}\n type=\"button\"\n >\n {recordButtonLabel}\n </Button>\n </>\n )}\n </div>\n );\n },\n);\n\nAudioUpload.displayName = \"AudioUpload\";\n"],"names":["validateAudioFile","React","DEFAULT_MAX_FILE_SIZE","DEFAULT_ACCEPTED_TYPES","DEFAULT_MAX_RECORDING_DURATION","DEFAULT_MIN_RECORDING_DURATION","useAudioRecorder","formatAudioTime","jsxs","cn","jsx","MicrophoneIcon","AudioWaveform","AudioRecordButton","UploadCloudIcon","Fragment","Button"],"mappings":"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"}
@@ -326,22 +326,17 @@ function InlineSelect({
326
326
  className: cn.cn(
327
327
  "flex w-full items-start gap-2 rounded-sm px-3 py-2 text-left outline-none",
328
328
  "focus-visible:shadow-focus-ring",
329
- isSelected ? "typography-body-default-16px-semibold bg-buttons-primary-default text-content-primary-inverted" : "typography-body-default-16px-regular text-content-primary hover:bg-neutral-alphas-50"
329
+ "typography-body-default-16px-regular text-content-primary hover:bg-neutral-alphas-50",
330
+ // bg-interaction-hover aliases to the same token as the
331
+ // plain hover background above, so it can't distinguish
332
+ // the selected option from a hovered-but-unselected one.
333
+ isSelected && ["bg-neutral-alphas-100", "hover:bg-neutral-alphas-200"]
330
334
  ),
331
335
  children: [
332
336
  option.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center pt-1 [&>svg]:size-4", children: option.icon }),
333
337
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
334
338
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: option.menuLabel ?? option.label }),
335
- option.description && /* @__PURE__ */ jsxRuntime.jsx(
336
- "span",
337
- {
338
- className: cn.cn(
339
- "typography-body-small-14px-regular truncate",
340
- isSelected ? "text-content-primary-inverted" : "text-content-secondary"
341
- ),
342
- children: option.description
343
- }
344
- )
339
+ option.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typography-body-small-14px-regular truncate text-content-secondary", children: option.description })
345
340
  ] }),
346
341
  isSelected && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center pt-1", children: /* @__PURE__ */ jsxRuntime.jsx(SelectedTick, {}) })
347
342
  ]