@fanvue/ui 3.16.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 (35) 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/VoiceNote/VoiceNote.cjs +305 -0
  4. package/dist/cjs/components/VoiceNote/VoiceNote.cjs.map +1 -0
  5. package/dist/cjs/index.cjs +2 -0
  6. package/dist/cjs/index.cjs.map +1 -1
  7. package/dist/cjs/utils/audioWaveform.cjs +76 -0
  8. package/dist/cjs/utils/audioWaveform.cjs.map +1 -0
  9. package/dist/cjs/utils/useAudioPlayback.cjs +123 -0
  10. package/dist/cjs/utils/useAudioPlayback.cjs.map +1 -0
  11. package/dist/cjs/utils/useFittedBarCount.cjs +42 -0
  12. package/dist/cjs/utils/useFittedBarCount.cjs.map +1 -0
  13. package/dist/cjs/utils/useWaveformPeaks.cjs +48 -0
  14. package/dist/cjs/utils/useWaveformPeaks.cjs.map +1 -0
  15. package/dist/cjs/utils/useWaveformSeek.cjs +100 -0
  16. package/dist/cjs/utils/useWaveformSeek.cjs.map +1 -0
  17. package/dist/components/AudioPlayer/AudioPlayer.mjs +7 -80
  18. package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -1
  19. package/dist/components/VoiceNote/VoiceNote.mjs +288 -0
  20. package/dist/components/VoiceNote/VoiceNote.mjs.map +1 -0
  21. package/dist/index.d.ts +76 -0
  22. package/dist/index.mjs +2 -0
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/styles/base.css +11 -2
  25. package/dist/utils/audioWaveform.mjs +76 -0
  26. package/dist/utils/audioWaveform.mjs.map +1 -0
  27. package/dist/utils/useAudioPlayback.mjs +106 -0
  28. package/dist/utils/useAudioPlayback.mjs.map +1 -0
  29. package/dist/utils/useFittedBarCount.mjs +25 -0
  30. package/dist/utils/useFittedBarCount.mjs.map +1 -0
  31. package/dist/utils/useWaveformPeaks.mjs +31 -0
  32. package/dist/utils/useWaveformPeaks.mjs.map +1 -0
  33. package/dist/utils/useWaveformSeek.mjs +83 -0
  34. package/dist/utils/useWaveformSeek.mjs.map +1 -0
  35. 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,305 @@
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 audioWaveform = require("../../utils/audioWaveform.cjs");
7
+ const cn = require("../../utils/cn.cjs");
8
+ const useAudioPlayback = require("../../utils/useAudioPlayback.cjs");
9
+ const useFittedBarCount = require("../../utils/useFittedBarCount.cjs");
10
+ const useWaveformPeaks = require("../../utils/useWaveformPeaks.cjs");
11
+ const useWaveformSeek = require("../../utils/useWaveformSeek.cjs");
12
+ const IconButton = require("../IconButton/IconButton.cjs");
13
+ const CloseIcon = require("../Icons/CloseIcon.cjs");
14
+ const PauseIcon = require("../Icons/PauseIcon.cjs");
15
+ const PlayIcon = require("../Icons/PlayIcon.cjs");
16
+ function _interopNamespaceDefault(e) {
17
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
18
+ if (e) {
19
+ for (const k in e) {
20
+ if (k !== "default") {
21
+ const d = Object.getOwnPropertyDescriptor(e, k);
22
+ Object.defineProperty(n, k, d.get ? d : {
23
+ enumerable: true,
24
+ get: () => e[k]
25
+ });
26
+ }
27
+ }
28
+ }
29
+ n.default = e;
30
+ return Object.freeze(n);
31
+ }
32
+ const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
33
+ const DEFAULT_WAVEFORM = [
34
+ 0.15,
35
+ 0.46,
36
+ 0.85,
37
+ 0.23,
38
+ 1,
39
+ 0.85,
40
+ 0.62,
41
+ 0.62,
42
+ 0.62,
43
+ 0.85,
44
+ 0.85,
45
+ 0.62,
46
+ 0.62,
47
+ 1,
48
+ 1,
49
+ 0.62,
50
+ 0.62,
51
+ 0.62,
52
+ 0.23,
53
+ 0.23,
54
+ 0.15,
55
+ 0.46,
56
+ 0.85,
57
+ 0.23,
58
+ 1,
59
+ 0.46,
60
+ 0.85,
61
+ 0.62,
62
+ 0.23,
63
+ 0.23,
64
+ 0.62,
65
+ 0.62,
66
+ 0.85,
67
+ 0.85,
68
+ 0.62,
69
+ 0.62,
70
+ 0.85,
71
+ 1,
72
+ 0.85,
73
+ 1,
74
+ 0.62,
75
+ 0.62,
76
+ 0.62,
77
+ 0.23,
78
+ 0.23,
79
+ 0.62,
80
+ 0.23,
81
+ 0.23,
82
+ 0.23,
83
+ 0.62,
84
+ 0.85,
85
+ 0.62,
86
+ 0.85,
87
+ 1,
88
+ 0.85,
89
+ 0.85,
90
+ 1,
91
+ 0.85,
92
+ 0.23,
93
+ 0.23,
94
+ 0.23,
95
+ 0.46,
96
+ 0.62,
97
+ 0.23,
98
+ 0.23
99
+ ];
100
+ const MIN_BAR_HEIGHT = 4;
101
+ const MAX_BAR_HEIGHT = {
102
+ default: 26,
103
+ small: 16
104
+ };
105
+ const CONTROL_SIZE = {
106
+ default: "48",
107
+ small: "32"
108
+ };
109
+ const BAR_WIDTH_PX = 4;
110
+ const BAR_GAP_PX = 4;
111
+ const DEFAULT_BAR_COUNT = 40;
112
+ function activeBarClass(negative) {
113
+ return negative ? "bg-messages-waveform-listening-negative-active" : "bg-messages-waveform-listening-active";
114
+ }
115
+ function inactiveBarClass(negative) {
116
+ return negative ? "bg-messages-waveform-listening-negative-inactive" : "bg-messages-waveform-listening-inactive";
117
+ }
118
+ function restingBarClass(negative) {
119
+ return negative ? "bg-messages-waveform-listening-negative-default" : "bg-messages-waveform-default";
120
+ }
121
+ function WaveformBars({ bars, variant, size, negative, progress }) {
122
+ const hasProgress = progress !== void 0;
123
+ const activeCount = hasProgress ? Math.round(progress * bars.length) : 0;
124
+ const maxBarHeight = MAX_BAR_HEIGHT[size];
125
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: bars.map((amplitude, index) => {
126
+ const height = variant === "flat" ? MIN_BAR_HEIGHT : Math.max(MIN_BAR_HEIGHT, Math.round(amplitude * maxBarHeight));
127
+ const colorClass = !hasProgress ? restingBarClass(negative) : index < activeCount ? activeBarClass(negative) : inactiveBarClass(negative);
128
+ return /* @__PURE__ */ jsxRuntime.jsx(
129
+ "span",
130
+ {
131
+ className: cn.cn("w-1 shrink-0 rounded-[2px]", colorClass),
132
+ style: { height }
133
+ },
134
+ index
135
+ );
136
+ }) });
137
+ }
138
+ function WaveformMeta({ time, fileName, textColor }) {
139
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
140
+ time && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn.cn("typography-body-small-14px-regular shrink-0", textColor), children: time }),
141
+ fileName && /* @__PURE__ */ jsxRuntime.jsx(
142
+ "span",
143
+ {
144
+ className: cn.cn(
145
+ "typography-body-small-14px-regular min-w-0 max-w-[50%] truncate",
146
+ textColor
147
+ ),
148
+ children: fileName
149
+ }
150
+ )
151
+ ] });
152
+ }
153
+ const VoiceNote = React__namespace.forwardRef(
154
+ ({
155
+ className,
156
+ src,
157
+ duration,
158
+ waveform = DEFAULT_WAVEFORM,
159
+ variant = "default",
160
+ size = "default",
161
+ negative = false,
162
+ progress,
163
+ playing,
164
+ defaultPlaying = false,
165
+ onPlayPause,
166
+ onEnded,
167
+ time,
168
+ fileName,
169
+ showControls = true,
170
+ showTimestamp = true,
171
+ showRemove = false,
172
+ onRemove,
173
+ playButtonLabel,
174
+ removeButtonLabel = "Remove",
175
+ "aria-label": ariaLabel = "Voice note",
176
+ ...props
177
+ }, ref) => {
178
+ const isPlayer = src !== void 0;
179
+ const playback = useAudioPlayback.useAudioPlayback({
180
+ src,
181
+ duration,
182
+ playing,
183
+ defaultPlaying,
184
+ onEnded,
185
+ onPlay: React__namespace.useCallback(() => onPlayPause?.(true), [onPlayPause]),
186
+ onPause: React__namespace.useCallback(() => onPlayPause?.(false), [onPlayPause])
187
+ });
188
+ const decodedPeaks = useWaveformPeaks.useWaveformPeaks(src);
189
+ const [internalPlaying, setInternalPlaying] = React__namespace.useState(defaultPlaying);
190
+ const isControlled = playing !== void 0;
191
+ const trackRef = React__namespace.useRef(null);
192
+ const fittedBarCount = useFittedBarCount.useFittedBarCount(trackRef, {
193
+ barWidthPx: BAR_WIDTH_PX,
194
+ gapPx: BAR_GAP_PX,
195
+ fallback: DEFAULT_BAR_COUNT
196
+ });
197
+ const seekProps = useWaveformSeek.useWaveformSeek({
198
+ trackRef,
199
+ displayDuration: playback.displayDuration,
200
+ currentTime: playback.currentTime,
201
+ seekTo: playback.seekTo,
202
+ setSeeking: playback.setSeeking
203
+ });
204
+ const isPlaying = isPlayer ? playback.isPlaying : playing ?? internalPlaying;
205
+ const handlePlayPause = () => {
206
+ if (isPlayer) {
207
+ playback.toggle();
208
+ return;
209
+ }
210
+ const next = !isPlaying;
211
+ if (!isControlled) setInternalPlaying(next);
212
+ onPlayPause?.(next);
213
+ };
214
+ const bars = audioWaveform.resamplePeaks(isPlayer ? decodedPeaks : waveform, fittedBarCount);
215
+ const progressValue = isPlayer ? playback.progress : progress;
216
+ const textColor = negative ? "text-content-primary-inverted" : "text-content-primary";
217
+ const playerTime = playback.hasStarted ? playback.currentTime : playback.displayDuration;
218
+ const displayTime = isPlayer ? audioWaveform.formatTime(playerTime) : time;
219
+ const controlLabel = playButtonLabel ?? (isPlaying ? "Pause" : "Play");
220
+ const trackAria = isPlayer ? seekProps : { "aria-hidden": true };
221
+ return (
222
+ // biome-ignore lint/a11y/useSemanticElements: <fieldset> would break the public HTMLDivElement ref/props API
223
+ /* @__PURE__ */ jsxRuntime.jsxs(
224
+ "div",
225
+ {
226
+ ref,
227
+ role: "group",
228
+ "aria-label": ariaLabel,
229
+ className: cn.cn("flex items-center gap-3", size === "small" ? "h-8" : "h-12", className),
230
+ ...props,
231
+ children: [
232
+ showControls && /* @__PURE__ */ jsxRuntime.jsx(
233
+ IconButton.IconButton,
234
+ {
235
+ variant: "secondary",
236
+ size: CONTROL_SIZE[size],
237
+ negative,
238
+ icon: isPlaying ? /* @__PURE__ */ jsxRuntime.jsx(PauseIcon.PauseIcon, {}) : /* @__PURE__ */ jsxRuntime.jsx(PlayIcon.PlayIcon, {}),
239
+ "aria-label": controlLabel,
240
+ onClick: handlePlayPause
241
+ }
242
+ ),
243
+ /* @__PURE__ */ jsxRuntime.jsx(
244
+ "div",
245
+ {
246
+ ref: trackRef,
247
+ "data-testid": "voice-note-waveform",
248
+ className: cn.cn(
249
+ "flex h-full min-w-0 flex-1 items-center gap-1 overflow-hidden",
250
+ isPlayer && "cursor-pointer touch-none select-none focus-visible:shadow-focus-ring focus-visible:outline-none"
251
+ ),
252
+ ...trackAria,
253
+ children: /* @__PURE__ */ jsxRuntime.jsx(
254
+ WaveformBars,
255
+ {
256
+ bars,
257
+ variant,
258
+ size,
259
+ negative,
260
+ progress: progressValue
261
+ }
262
+ )
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsxRuntime.jsx(
266
+ WaveformMeta,
267
+ {
268
+ time: showTimestamp ? displayTime : void 0,
269
+ fileName,
270
+ textColor
271
+ }
272
+ ),
273
+ showRemove && /* @__PURE__ */ jsxRuntime.jsx(
274
+ IconButton.IconButton,
275
+ {
276
+ variant: "tertiary",
277
+ size: CONTROL_SIZE[size],
278
+ negative,
279
+ icon: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon.CloseIcon, {}),
280
+ "aria-label": removeButtonLabel,
281
+ onClick: onRemove
282
+ }
283
+ ),
284
+ isPlayer && // biome-ignore lint/a11y/useMediaCaption: a UI voice note / short recording, not narrated content needing captions
285
+ /* @__PURE__ */ jsxRuntime.jsx(
286
+ "audio",
287
+ {
288
+ ref: playback.audioRef,
289
+ src,
290
+ preload: "metadata",
291
+ className: "hidden",
292
+ onLoadedMetadata: playback.audioHandlers.onLoadedMetadata,
293
+ onTimeUpdate: playback.audioHandlers.onTimeUpdate,
294
+ onEnded: playback.audioHandlers.onEnded
295
+ }
296
+ )
297
+ ]
298
+ }
299
+ )
300
+ );
301
+ }
302
+ );
303
+ VoiceNote.displayName = "VoiceNote";
304
+ exports.VoiceNote = VoiceNote;
305
+ //# sourceMappingURL=VoiceNote.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoiceNote.cjs","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":["jsx","Fragment","cn","jsxs","React","useAudioPlayback","useWaveformPeaks","useFittedBarCount","useWaveformSeek","resamplePeaks","formatTime","IconButton","PauseIcon","PlayIcon","CloseIcon"],"mappings":"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"}
@@ -250,6 +250,7 @@ const Tooltip = require("./components/Tooltip/Tooltip.cjs");
250
250
  const UserDisplayName = require("./components/UserDisplayName/UserDisplayName.cjs");
251
251
  const UserHandle = require("./components/UserHandle/UserHandle.cjs");
252
252
  const UserItem = require("./components/UserItem/UserItem.cjs");
253
+ const VoiceNote = require("./components/VoiceNote/VoiceNote.cjs");
253
254
  const cn = require("./utils/cn.cjs");
254
255
  const getInitials = require("./utils/getInitials.cjs");
255
256
  const useSuppressClickAfterDrag = require("./utils/useSuppressClickAfterDrag.cjs");
@@ -579,6 +580,7 @@ exports.TooltipTrigger = Tooltip.TooltipTrigger;
579
580
  exports.UserDisplayName = UserDisplayName.UserDisplayName;
580
581
  exports.UserHandle = UserHandle.UserHandle;
581
582
  exports.UserItem = UserItem.UserItem;
583
+ exports.VoiceNote = VoiceNote.VoiceNote;
582
584
  exports.cn = cn.cn;
583
585
  exports.getInitials = getInitials.getInitials;
584
586
  exports.useSuppressClickAfterDrag = useSuppressClickAfterDrag.useSuppressClickAfterDrag;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}