@fanvue/ui 3.15.0 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +16 -89
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs +54 -0
- package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs.map +1 -0
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs +5 -5
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs.map +1 -1
- package/dist/cjs/components/ChatInput/ChatInput.cjs +6 -11
- package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -1
- package/dist/cjs/components/ChatMessage/ChatMessage.cjs +228 -0
- package/dist/cjs/components/ChatMessage/ChatMessage.cjs.map +1 -0
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +160 -71
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/cjs/components/Icons/DenseGridViewIcon.cjs +52 -0
- package/dist/cjs/components/Icons/DenseGridViewIcon.cjs.map +1 -0
- package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs +66 -0
- package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs.map +1 -0
- package/dist/cjs/components/Select/Select.cjs +61 -17
- package/dist/cjs/components/Select/Select.cjs.map +1 -1
- package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs +53 -0
- package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs.map +1 -0
- package/dist/cjs/components/VoiceNote/VoiceNote.cjs +305 -0
- package/dist/cjs/components/VoiceNote/VoiceNote.cjs.map +1 -0
- package/dist/cjs/index.cjs +12 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/utils/audioWaveform.cjs +76 -0
- package/dist/cjs/utils/audioWaveform.cjs.map +1 -0
- package/dist/cjs/utils/useAudioPlayback.cjs +123 -0
- package/dist/cjs/utils/useAudioPlayback.cjs.map +1 -0
- package/dist/cjs/utils/useFittedBarCount.cjs +42 -0
- package/dist/cjs/utils/useFittedBarCount.cjs.map +1 -0
- package/dist/cjs/utils/useWaveformPeaks.cjs +48 -0
- package/dist/cjs/utils/useWaveformPeaks.cjs.map +1 -0
- package/dist/cjs/utils/useWaveformSeek.cjs +100 -0
- package/dist/cjs/utils/useWaveformSeek.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.mjs +7 -80
- package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -1
- package/dist/components/AudioRecordButton/AudioRecordButton.mjs +37 -0
- package/dist/components/AudioRecordButton/AudioRecordButton.mjs.map +1 -0
- package/dist/components/AudioUpload/AudioUpload.mjs +5 -5
- package/dist/components/AudioUpload/AudioUpload.mjs.map +1 -1
- package/dist/components/ChatInput/ChatInput.mjs +6 -11
- package/dist/components/ChatInput/ChatInput.mjs.map +1 -1
- package/dist/components/ChatMessage/ChatMessage.mjs +211 -0
- package/dist/components/ChatMessage/ChatMessage.mjs.map +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.mjs +161 -72
- package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
- package/dist/components/Icons/DenseGridViewIcon.mjs +35 -0
- package/dist/components/Icons/DenseGridViewIcon.mjs.map +1 -0
- package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs +49 -0
- package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs.map +1 -0
- package/dist/components/Select/Select.mjs +61 -17
- package/dist/components/Select/Select.mjs.map +1 -1
- package/dist/components/SubscribeButton/SubscribeButton.mjs +36 -0
- package/dist/components/SubscribeButton/SubscribeButton.mjs.map +1 -0
- package/dist/components/VoiceNote/VoiceNote.mjs +288 -0
- package/dist/components/VoiceNote/VoiceNote.mjs.map +1 -0
- package/dist/index.d.ts +321 -4
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -1
- package/dist/styles/base.css +11 -2
- package/dist/styles/theme.css +1 -1
- package/dist/utils/audioWaveform.mjs +76 -0
- package/dist/utils/audioWaveform.mjs.map +1 -0
- package/dist/utils/useAudioPlayback.mjs +106 -0
- package/dist/utils/useAudioPlayback.mjs.map +1 -0
- package/dist/utils/useFittedBarCount.mjs +25 -0
- package/dist/utils/useFittedBarCount.mjs.map +1 -0
- package/dist/utils/useWaveformPeaks.mjs +31 -0
- package/dist/utils/useWaveformPeaks.mjs.map +1 -0
- package/dist/utils/useWaveformSeek.mjs +83 -0
- package/dist/utils/useWaveformSeek.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ const AccordionItem = require("./components/Accordion/AccordionItem.cjs");
|
|
|
8
8
|
const AccordionTrigger = require("./components/Accordion/AccordionTrigger.cjs");
|
|
9
9
|
const Alert = require("./components/Alert/Alert.cjs");
|
|
10
10
|
const AudioPlayer = require("./components/AudioPlayer/AudioPlayer.cjs");
|
|
11
|
+
const AudioRecordButton = require("./components/AudioRecordButton/AudioRecordButton.cjs");
|
|
11
12
|
const AudioUpload = require("./components/AudioUpload/AudioUpload.cjs");
|
|
12
13
|
const useAudioRecorder = require("./components/AudioUpload/useAudioRecorder.cjs");
|
|
13
14
|
const Autocomplete = require("./components/Autocomplete/Autocomplete.cjs");
|
|
@@ -20,6 +21,7 @@ const Breadcrumb = require("./components/Breadcrumb/Breadcrumb.cjs");
|
|
|
20
21
|
const Button = require("./components/Button/Button.cjs");
|
|
21
22
|
const Card = require("./components/Card/Card.cjs");
|
|
22
23
|
const ChatInput = require("./components/ChatInput/ChatInput.cjs");
|
|
24
|
+
const ChatMessage = require("./components/ChatMessage/ChatMessage.cjs");
|
|
23
25
|
const Checkbox = require("./components/Checkbox/Checkbox.cjs");
|
|
24
26
|
const Chip = require("./components/Chip/Chip.cjs");
|
|
25
27
|
const Count = require("./components/Count/Count.cjs");
|
|
@@ -79,6 +81,7 @@ const CopyIcon = require("./components/Icons/CopyIcon.cjs");
|
|
|
79
81
|
const CrossCircleIcon = require("./components/Icons/CrossCircleIcon.cjs");
|
|
80
82
|
const CrossIcon = require("./components/Icons/CrossIcon.cjs");
|
|
81
83
|
const CrownIcon = require("./components/Icons/CrownIcon.cjs");
|
|
84
|
+
const DenseGridViewIcon = require("./components/Icons/DenseGridViewIcon.cjs");
|
|
82
85
|
const DiamondIcon = require("./components/Icons/DiamondIcon.cjs");
|
|
83
86
|
const DiscordIcon = require("./components/Icons/DiscordIcon.cjs");
|
|
84
87
|
const DiscountIcon = require("./components/Icons/DiscountIcon.cjs");
|
|
@@ -206,6 +209,7 @@ const InlineEdit = require("./components/InlineEdit/InlineEdit.cjs");
|
|
|
206
209
|
const Link = require("./components/Link/Link.cjs");
|
|
207
210
|
const Loader = require("./components/Loader/Loader.cjs");
|
|
208
211
|
const Logo = require("./components/Logo/Logo.cjs");
|
|
212
|
+
const MediaStatusIndicator = require("./components/MediaStatusIndicator/MediaStatusIndicator.cjs");
|
|
209
213
|
const MobileStepper = require("./components/MobileStepper/MobileStepper.cjs");
|
|
210
214
|
const OnlineBlinkingIcon = require("./components/OnlineBlinkingIcon/OnlineBlinkingIcon.cjs");
|
|
211
215
|
const PageSelector = require("./components/PageSelector/PageSelector.cjs");
|
|
@@ -229,6 +233,7 @@ const Slider = require("./components/Slider/Slider.cjs");
|
|
|
229
233
|
const Snackbar = require("./components/Snackbar/Snackbar.cjs");
|
|
230
234
|
const Stepper = require("./components/Stepper/Stepper.cjs");
|
|
231
235
|
const StepperStep = require("./components/Stepper/StepperStep.cjs");
|
|
236
|
+
const SubscribeButton = require("./components/SubscribeButton/SubscribeButton.cjs");
|
|
232
237
|
const Switch = require("./components/Switch/Switch.cjs");
|
|
233
238
|
const SwitchField = require("./components/SwitchField/SwitchField.cjs");
|
|
234
239
|
const SwitchToggle = require("./components/SwitchToggle/SwitchToggle.cjs");
|
|
@@ -245,6 +250,7 @@ const Tooltip = require("./components/Tooltip/Tooltip.cjs");
|
|
|
245
250
|
const UserDisplayName = require("./components/UserDisplayName/UserDisplayName.cjs");
|
|
246
251
|
const UserHandle = require("./components/UserHandle/UserHandle.cjs");
|
|
247
252
|
const UserItem = require("./components/UserItem/UserItem.cjs");
|
|
253
|
+
const VoiceNote = require("./components/VoiceNote/VoiceNote.cjs");
|
|
248
254
|
const cn = require("./utils/cn.cjs");
|
|
249
255
|
const getInitials = require("./utils/getInitials.cjs");
|
|
250
256
|
const useSuppressClickAfterDrag = require("./utils/useSuppressClickAfterDrag.cjs");
|
|
@@ -262,6 +268,7 @@ exports.AccordionItem = AccordionItem.AccordionItem;
|
|
|
262
268
|
exports.AccordionTrigger = AccordionTrigger.AccordionTrigger;
|
|
263
269
|
exports.Alert = Alert.Alert;
|
|
264
270
|
exports.AudioPlayer = AudioPlayer.AudioPlayer;
|
|
271
|
+
exports.AudioRecordButton = AudioRecordButton.AudioRecordButton;
|
|
265
272
|
exports.AudioUpload = AudioUpload.AudioUpload;
|
|
266
273
|
exports.useAudioRecorder = useAudioRecorder.useAudioRecorder;
|
|
267
274
|
exports.Autocomplete = Autocomplete.Autocomplete;
|
|
@@ -287,6 +294,7 @@ exports.CardFooter = Card.CardFooter;
|
|
|
287
294
|
exports.CardHeader = Card.CardHeader;
|
|
288
295
|
exports.CardTitle = Card.CardTitle;
|
|
289
296
|
exports.ChatInput = ChatInput.ChatInput;
|
|
297
|
+
exports.ChatMessage = ChatMessage.ChatMessage;
|
|
290
298
|
exports.Checkbox = Checkbox.Checkbox;
|
|
291
299
|
exports.Chip = Chip.Chip;
|
|
292
300
|
exports.Count = Count.Count;
|
|
@@ -372,6 +380,7 @@ exports.CopyIcon = CopyIcon.CopyIcon;
|
|
|
372
380
|
exports.CrossCircleIcon = CrossCircleIcon.CrossCircleIcon;
|
|
373
381
|
exports.CrossIcon = CrossIcon.CrossIcon;
|
|
374
382
|
exports.CrownIcon = CrownIcon.CrownIcon;
|
|
383
|
+
exports.DenseGridViewIcon = DenseGridViewIcon.DenseGridViewIcon;
|
|
375
384
|
exports.DiamondIcon = DiamondIcon.DiamondIcon;
|
|
376
385
|
exports.DiscordIcon = DiscordIcon.DiscordIcon;
|
|
377
386
|
exports.DiscountIcon = DiscountIcon.DiscountIcon;
|
|
@@ -501,6 +510,7 @@ exports.InlineEdit = InlineEdit.InlineEdit;
|
|
|
501
510
|
exports.Link = Link.Link;
|
|
502
511
|
exports.Loader = Loader.Loader;
|
|
503
512
|
exports.Logo = Logo.Logo;
|
|
513
|
+
exports.MediaStatusIndicator = MediaStatusIndicator.MediaStatusIndicator;
|
|
504
514
|
exports.MobileStepper = MobileStepper.MobileStepper;
|
|
505
515
|
exports.OnlineBlinkingIcon = OnlineBlinkingIcon.OnlineBlinkingIcon;
|
|
506
516
|
exports.PageSelector = PageSelector.PageSelector;
|
|
@@ -529,6 +539,7 @@ exports.Slider = Slider.Slider;
|
|
|
529
539
|
exports.Snackbar = Snackbar.Snackbar;
|
|
530
540
|
exports.Stepper = Stepper.Stepper;
|
|
531
541
|
exports.StepperStep = StepperStep.StepperStep;
|
|
542
|
+
exports.SubscribeButton = SubscribeButton.SubscribeButton;
|
|
532
543
|
exports.Switch = Switch.Switch;
|
|
533
544
|
exports.SwitchField = SwitchField.SwitchField;
|
|
534
545
|
exports.SwitchToggle = SwitchToggle.SwitchToggle;
|
|
@@ -569,6 +580,7 @@ exports.TooltipTrigger = Tooltip.TooltipTrigger;
|
|
|
569
580
|
exports.UserDisplayName = UserDisplayName.UserDisplayName;
|
|
570
581
|
exports.UserHandle = UserHandle.UserHandle;
|
|
571
582
|
exports.UserItem = UserItem.UserItem;
|
|
583
|
+
exports.VoiceNote = VoiceNote.VoiceNote;
|
|
572
584
|
exports.cn = cn.cn;
|
|
573
585
|
exports.getInitials = getInitials.getInitials;
|
|
574
586
|
exports.useSuppressClickAfterDrag = useSuppressClickAfterDrag.useSuppressClickAfterDrag;
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const RAW_PEAK_COUNT = 128;
|
|
5
|
+
function formatTime(seconds) {
|
|
6
|
+
if (seconds === void 0 || !Number.isFinite(seconds)) return "--:--";
|
|
7
|
+
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
8
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
9
|
+
const remainingSeconds = totalSeconds % 60;
|
|
10
|
+
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
|
|
11
|
+
}
|
|
12
|
+
function hashString(value) {
|
|
13
|
+
let hash = 2166136261;
|
|
14
|
+
for (let i = 0; i < value.length; i++) {
|
|
15
|
+
hash ^= value.charCodeAt(i);
|
|
16
|
+
hash = Math.imul(hash, 16777619);
|
|
17
|
+
}
|
|
18
|
+
return hash >>> 0;
|
|
19
|
+
}
|
|
20
|
+
function createSeededRandom(seed) {
|
|
21
|
+
let state = seed;
|
|
22
|
+
return () => {
|
|
23
|
+
state = state + 1831565813 | 0;
|
|
24
|
+
let t = Math.imul(state ^ state >>> 15, 1 | state);
|
|
25
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
26
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function generateFallbackPeaks(src, count) {
|
|
30
|
+
const random = createSeededRandom(hashString(src));
|
|
31
|
+
return Array.from({ length: count }, () => 0.2 + random() * 0.8);
|
|
32
|
+
}
|
|
33
|
+
function computePeaksFromChannelData(channelData, count) {
|
|
34
|
+
const blockSize = Math.max(1, Math.floor(channelData.length / count));
|
|
35
|
+
const peaks = [];
|
|
36
|
+
for (let i = 0; i < count; i++) {
|
|
37
|
+
const start = i * blockSize;
|
|
38
|
+
let max = 0;
|
|
39
|
+
for (let j = 0; j < blockSize && start + j < channelData.length; j++) {
|
|
40
|
+
max = Math.max(max, Math.abs(channelData[start + j] ?? 0));
|
|
41
|
+
}
|
|
42
|
+
peaks.push(max);
|
|
43
|
+
}
|
|
44
|
+
return peaks;
|
|
45
|
+
}
|
|
46
|
+
function resamplePeaks(peaks, barCount) {
|
|
47
|
+
if (peaks.length === 0 || barCount <= 0) return [];
|
|
48
|
+
const step = peaks.length / barCount;
|
|
49
|
+
return Array.from(
|
|
50
|
+
{ length: barCount },
|
|
51
|
+
(_, i) => peaks[Math.min(peaks.length - 1, Math.floor(i * step))] ?? 0
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
async function decodeAudioPeaks(src, signal) {
|
|
55
|
+
const AudioContextCtor = window.AudioContext ?? window.webkitAudioContext;
|
|
56
|
+
if (!AudioContextCtor) throw new Error("WebAudio unsupported");
|
|
57
|
+
const response = await fetch(src, { signal });
|
|
58
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
59
|
+
const audioContext = new AudioContextCtor();
|
|
60
|
+
try {
|
|
61
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
62
|
+
return computePeaksFromChannelData(audioBuffer.getChannelData(0), RAW_PEAK_COUNT);
|
|
63
|
+
} finally {
|
|
64
|
+
if (audioContext.state !== "closed") audioContext.close().catch(() => {
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.RAW_PEAK_COUNT = RAW_PEAK_COUNT;
|
|
69
|
+
exports.computePeaksFromChannelData = computePeaksFromChannelData;
|
|
70
|
+
exports.createSeededRandom = createSeededRandom;
|
|
71
|
+
exports.decodeAudioPeaks = decodeAudioPeaks;
|
|
72
|
+
exports.formatTime = formatTime;
|
|
73
|
+
exports.generateFallbackPeaks = generateFallbackPeaks;
|
|
74
|
+
exports.hashString = hashString;
|
|
75
|
+
exports.resamplePeaks = resamplePeaks;
|
|
76
|
+
//# sourceMappingURL=audioWaveform.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audioWaveform.cjs","sources":["../../../src/utils/audioWaveform.ts"],"sourcesContent":["/** Number of amplitude samples decoded/kept internally, resampled to the rendered bar count. */\nexport const RAW_PEAK_COUNT = 128;\n\n/** Formats a duration in seconds as `m:ss`, or `--:--` when unknown. */\nexport function 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. */\nexport function 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`. */\nexport function 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). */\nexport function 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. */\nexport function 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 `barCount` values (nearest-neighbour). */\nexport function 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. */\nexport async 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"],"names":[],"mappings":";;;AACO,MAAM,iBAAiB;AAGvB,SAAS,WAAW,SAAqC;AAC9D,MAAI,YAAY,UAAa,CAAC,OAAO,SAAS,OAAO,EAAG,QAAO;AAC/D,QAAM,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC;AACpD,QAAM,UAAU,KAAK,MAAM,eAAe,EAAE;AAC5C,QAAM,mBAAmB,eAAe;AACxC,SAAO,GAAG,OAAO,IAAI,OAAO,gBAAgB,EAAE,SAAS,GAAG,GAAG,CAAC;AAChE;AAGO,SAAS,WAAW,OAAuB;AAChD,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAQ,MAAM,WAAW,CAAC;AAC1B,WAAO,KAAK,KAAK,MAAM,QAAQ;AAAA,EACjC;AACA,SAAO,SAAS;AAClB;AAGO,SAAS,mBAAmB,MAA4B;AAC7D,MAAI,QAAQ;AACZ,SAAO,MAAM;AACX,YAAS,QAAQ,aAAc;AAC/B,QAAI,IAAI,KAAK,KAAK,QAAS,UAAU,IAAK,IAAI,KAAK;AACnD,QAAK,IAAI,KAAK,KAAK,IAAK,MAAM,GAAI,KAAK,CAAC,IAAK;AAC7C,aAAS,IAAK,MAAM,QAAS,KAAK;AAAA,EACpC;AACF;AAGO,SAAS,sBAAsB,KAAa,OAAyB;AAC1E,QAAM,SAAS,mBAAmB,WAAW,GAAG,CAAC;AACjD,SAAO,MAAM,KAAK,EAAE,QAAQ,MAAA,GAAS,MAAM,MAAM,OAAA,IAAW,GAAG;AACjE;AAGO,SAAS,4BAA4B,aAA2B,OAAyB;AAC9F,QAAM,YAAY,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,SAAS,KAAK,CAAC;AACpE,QAAM,QAAkB,CAAA;AACxB,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,UAAM,QAAQ,IAAI;AAClB,QAAI,MAAM;AACV,aAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,IAAI,YAAY,QAAQ,KAAK;AACpE,YAAM,KAAK,IAAI,KAAK,KAAK,IAAI,YAAY,QAAQ,CAAC,KAAK,CAAC,CAAC;AAAA,IAC3D;AACA,UAAM,KAAK,GAAG;AAAA,EAChB;AACA,SAAO;AACT;AAGO,SAAS,cAAc,OAAiB,UAA4B;AACzE,MAAI,MAAM,WAAW,KAAK,YAAY,UAAU,CAAA;AAChD,QAAM,OAAO,MAAM,SAAS;AAC5B,SAAO,MAAM;AAAA,IACX,EAAE,QAAQ,SAAA;AAAA,IACV,CAAC,GAAG,MAAM,MAAM,KAAK,IAAI,MAAM,SAAS,GAAG,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,KAAK;AAAA,EAAA;AAEzE;AAGA,eAAsB,iBAAiB,KAAa,QAAwC;AAC1F,QAAM,mBACJ,OAAO,gBACN,OAAmE;AACtE,MAAI,CAAC,iBAAkB,OAAM,IAAI,MAAM,sBAAsB;AAE7D,QAAM,WAAW,MAAM,MAAM,KAAK,EAAE,QAAQ;AAC5C,QAAM,cAAc,MAAM,SAAS,YAAA;AACnC,QAAM,eAAe,IAAI,iBAAA;AACzB,MAAI;AACF,UAAM,cAAc,MAAM,aAAa,gBAAgB,WAAW;AAClE,WAAO,4BAA4B,YAAY,eAAe,CAAC,GAAG,cAAc;AAAA,EAClF,UAAA;AACE,QAAI,aAAa,UAAU,uBAAuB,MAAA,EAAQ,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAC1E;AACF;;;;;;;;;"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const React = require("react");
|
|
5
|
+
function _interopNamespaceDefault(e) {
|
|
6
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
7
|
+
if (e) {
|
|
8
|
+
for (const k in e) {
|
|
9
|
+
if (k !== "default") {
|
|
10
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
11
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: () => e[k]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
n.default = e;
|
|
19
|
+
return Object.freeze(n);
|
|
20
|
+
}
|
|
21
|
+
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
22
|
+
function useAudioPlayback({
|
|
23
|
+
src,
|
|
24
|
+
duration,
|
|
25
|
+
playing: controlledPlaying,
|
|
26
|
+
defaultPlaying = false,
|
|
27
|
+
onPlay,
|
|
28
|
+
onPause,
|
|
29
|
+
onEnded
|
|
30
|
+
}) {
|
|
31
|
+
const audioRef = React__namespace.useRef(null);
|
|
32
|
+
const [internalPlaying, setInternalPlaying] = React__namespace.useState(defaultPlaying);
|
|
33
|
+
const isControlled = controlledPlaying !== void 0;
|
|
34
|
+
const isPlaying = isControlled ? controlledPlaying : internalPlaying;
|
|
35
|
+
const [currentTime, setCurrentTime] = React__namespace.useState(0);
|
|
36
|
+
const [mediaDuration, setMediaDuration] = React__namespace.useState(void 0);
|
|
37
|
+
const seekingRef = React__namespace.useRef(false);
|
|
38
|
+
const displayDuration = mediaDuration ?? duration;
|
|
39
|
+
const hasStarted = isPlaying || currentTime > 0;
|
|
40
|
+
const playingRef = React__namespace.useRef(isPlaying);
|
|
41
|
+
playingRef.current = isPlaying;
|
|
42
|
+
const isControlledRef = React__namespace.useRef(isControlled);
|
|
43
|
+
isControlledRef.current = isControlled;
|
|
44
|
+
const onPauseRef = React__namespace.useRef(onPause);
|
|
45
|
+
onPauseRef.current = onPause;
|
|
46
|
+
const isFirstSrcRenderRef = React__namespace.useRef(true);
|
|
47
|
+
React__namespace.useEffect(() => {
|
|
48
|
+
if (isFirstSrcRenderRef.current) {
|
|
49
|
+
isFirstSrcRenderRef.current = false;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
setCurrentTime(0);
|
|
53
|
+
setMediaDuration(void 0);
|
|
54
|
+
if (playingRef.current) {
|
|
55
|
+
if (!isControlledRef.current) setInternalPlaying(false);
|
|
56
|
+
onPauseRef.current?.();
|
|
57
|
+
}
|
|
58
|
+
}, [src]);
|
|
59
|
+
React__namespace.useEffect(() => {
|
|
60
|
+
const audio = audioRef.current;
|
|
61
|
+
if (!audio) return;
|
|
62
|
+
if (isPlaying) {
|
|
63
|
+
audio.play().catch(() => {
|
|
64
|
+
if (!isControlled) setInternalPlaying(false);
|
|
65
|
+
onPauseRef.current?.();
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
audio.pause();
|
|
69
|
+
}
|
|
70
|
+
}, [isPlaying, isControlled]);
|
|
71
|
+
const toggle = React__namespace.useCallback(() => {
|
|
72
|
+
const next = !playingRef.current;
|
|
73
|
+
if (!isControlledRef.current) setInternalPlaying(next);
|
|
74
|
+
if (next) onPlay?.();
|
|
75
|
+
else onPause?.();
|
|
76
|
+
}, [onPlay, onPause]);
|
|
77
|
+
const seekTo = React__namespace.useCallback(
|
|
78
|
+
(time) => {
|
|
79
|
+
if (displayDuration === void 0) return;
|
|
80
|
+
const clamped = Math.min(Math.max(time, 0), displayDuration);
|
|
81
|
+
setCurrentTime(clamped);
|
|
82
|
+
const audio = audioRef.current;
|
|
83
|
+
if (audio) audio.currentTime = clamped;
|
|
84
|
+
},
|
|
85
|
+
[displayDuration]
|
|
86
|
+
);
|
|
87
|
+
const setSeeking = React__namespace.useCallback((seeking) => {
|
|
88
|
+
seekingRef.current = seeking;
|
|
89
|
+
}, []);
|
|
90
|
+
const progress = displayDuration && displayDuration > 0 ? Math.min(Math.max(currentTime / displayDuration, 0), 1) : 0;
|
|
91
|
+
const audioHandlers = React__namespace.useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
onLoadedMetadata: (event) => {
|
|
94
|
+
const nativeDuration = event.currentTarget.duration;
|
|
95
|
+
setMediaDuration(Number.isFinite(nativeDuration) ? nativeDuration : void 0);
|
|
96
|
+
},
|
|
97
|
+
onTimeUpdate: (event) => {
|
|
98
|
+
if (seekingRef.current) return;
|
|
99
|
+
setCurrentTime(event.currentTarget.currentTime);
|
|
100
|
+
},
|
|
101
|
+
onEnded: () => {
|
|
102
|
+
setCurrentTime(0);
|
|
103
|
+
if (!isControlledRef.current) setInternalPlaying(false);
|
|
104
|
+
onEnded?.();
|
|
105
|
+
}
|
|
106
|
+
}),
|
|
107
|
+
[onEnded]
|
|
108
|
+
);
|
|
109
|
+
return {
|
|
110
|
+
audioRef,
|
|
111
|
+
isPlaying,
|
|
112
|
+
toggle,
|
|
113
|
+
currentTime,
|
|
114
|
+
displayDuration,
|
|
115
|
+
progress,
|
|
116
|
+
hasStarted,
|
|
117
|
+
seekTo,
|
|
118
|
+
setSeeking,
|
|
119
|
+
audioHandlers
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
exports.useAudioPlayback = useAudioPlayback;
|
|
123
|
+
//# sourceMappingURL=useAudioPlayback.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAudioPlayback.cjs","sources":["../../../src/utils/useAudioPlayback.ts"],"sourcesContent":["import * as React from \"react\";\n\n/** Options for {@link useAudioPlayback}. */\nexport interface UseAudioPlaybackOptions {\n /** URL of the audio to play. When absent, the hook stays idle (no media element attached). */\n src?: string;\n /** Fallback total duration (seconds) used until the media's own metadata loads. */\n duration?: number;\n /** Whether playback is active (controlled). */\n playing?: boolean;\n /** Initial playing state (uncontrolled). @default false */\n defaultPlaying?: boolean;\n /** Called when playback starts (via the toggle). */\n onPlay?: () => void;\n /** Called when playback pauses (via the toggle, a failed play(), or a src swap). */\n onPause?: () => void;\n /** Called when playback reaches the end of the media. */\n onEnded?: () => void;\n}\n\n/** Handlers to spread onto the backing `<audio>` element. */\nexport interface AudioElementHandlers {\n onLoadedMetadata: (event: React.SyntheticEvent<HTMLAudioElement>) => void;\n onTimeUpdate: (event: React.SyntheticEvent<HTMLAudioElement>) => void;\n onEnded: () => void;\n}\n\n/** Result of {@link useAudioPlayback}. */\nexport interface UseAudioPlaybackResult {\n /** Attach to the backing `<audio>` element. */\n audioRef: React.RefObject<HTMLAudioElement | null>;\n /** Current play/pause state (respects controlled `playing`). */\n isPlaying: boolean;\n /** Toggle play/pause, firing `onPlay`/`onPause`. */\n toggle: () => void;\n /** Current playback position in seconds. */\n currentTime: number;\n /** Media duration once known, else the `duration` fallback. */\n displayDuration: number | undefined;\n /** Playback progress, 0–1. */\n progress: number;\n /** Whether playback has begun (playing or scrubbed past 0). */\n hasStarted: boolean;\n /** Seek to a time in seconds (clamped to the duration). */\n seekTo: (time: number) => void;\n /** Suppress `timeupdate`-driven position updates while a manual scrub is in progress. */\n setSeeking: (seeking: boolean) => void;\n /** Handlers to spread onto the `<audio>` element. */\n audioHandlers: AudioElementHandlers;\n}\n\n/**\n * Drives a single `<audio>` element: controlled/uncontrolled play/pause, live\n * position + duration, seeking, and src-swap resets. Extracted from\n * {@link AudioPlayer} so waveform players (e.g. {@link VoiceNote}) can share the\n * same playback engine without duplicating the effect wiring.\n */\nexport function useAudioPlayback({\n src,\n duration,\n playing: controlledPlaying,\n defaultPlaying = false,\n onPlay,\n onPause,\n onEnded,\n}: UseAudioPlaybackOptions): UseAudioPlaybackResult {\n const audioRef = React.useRef<HTMLAudioElement>(null);\n\n const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);\n const isControlled = controlledPlaying !== undefined;\n const isPlaying = isControlled ? controlledPlaying : internalPlaying;\n\n const [currentTime, setCurrentTime] = React.useState(0);\n const [mediaDuration, setMediaDuration] = React.useState<number | undefined>(undefined);\n const seekingRef = React.useRef(false);\n\n const displayDuration = mediaDuration ?? duration;\n const hasStarted = isPlaying || currentTime > 0;\n\n // Latest-value refs so the src-change effect can react to a src swap without\n // re-running whenever playing/isControlled/onPause change.\n const playingRef = React.useRef(isPlaying);\n playingRef.current = isPlaying;\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 on source change; skip the initial mount and,\n // if playback was active, stop it (the browser drops playback on a src swap).\n const isFirstSrcRenderRef = React.useRef(true);\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally keyed on src to reset transient state on a source swap\n React.useEffect(() => {\n if (isFirstSrcRenderRef.current) {\n isFirstSrcRenderRef.current = false;\n return;\n }\n setCurrentTime(0);\n setMediaDuration(undefined);\n if (playingRef.current) {\n if (!isControlledRef.current) setInternalPlaying(false);\n onPauseRef.current?.();\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 (isPlaying) {\n audio.play().catch(() => {\n if (!isControlled) setInternalPlaying(false);\n onPauseRef.current?.();\n });\n } else {\n audio.pause();\n }\n }, [isPlaying, isControlled]);\n\n const toggle = React.useCallback(() => {\n const next = !playingRef.current;\n if (!isControlledRef.current) setInternalPlaying(next);\n if (next) onPlay?.();\n else onPause?.();\n }, [onPlay, onPause]);\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 setSeeking = React.useCallback((seeking: boolean) => {\n seekingRef.current = seeking;\n }, []);\n\n const progress =\n displayDuration && displayDuration > 0\n ? Math.min(Math.max(currentTime / displayDuration, 0), 1)\n : 0;\n\n const audioHandlers = React.useMemo<AudioElementHandlers>(\n () => ({\n onLoadedMetadata: (event) => {\n const nativeDuration = event.currentTarget.duration;\n setMediaDuration(Number.isFinite(nativeDuration) ? nativeDuration : undefined);\n },\n onTimeUpdate: (event) => {\n if (seekingRef.current) return;\n setCurrentTime(event.currentTarget.currentTime);\n },\n onEnded: () => {\n setCurrentTime(0);\n if (!isControlledRef.current) setInternalPlaying(false);\n onEnded?.();\n },\n }),\n [onEnded],\n );\n\n return {\n audioRef,\n isPlaying,\n toggle,\n currentTime,\n displayDuration,\n progress,\n hasStarted,\n seekTo,\n setSeeking,\n audioHandlers,\n };\n}\n"],"names":["React"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const React = require("react");
|
|
5
|
+
function _interopNamespaceDefault(e) {
|
|
6
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
7
|
+
if (e) {
|
|
8
|
+
for (const k in e) {
|
|
9
|
+
if (k !== "default") {
|
|
10
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
11
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: () => e[k]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
n.default = e;
|
|
19
|
+
return Object.freeze(n);
|
|
20
|
+
}
|
|
21
|
+
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
22
|
+
function useFittedBarCount(trackRef, { barWidthPx, gapPx, fallback }) {
|
|
23
|
+
const [barCount, setBarCount] = React__namespace.useState(fallback);
|
|
24
|
+
React__namespace.useEffect(() => {
|
|
25
|
+
const track = trackRef.current;
|
|
26
|
+
if (!track) return;
|
|
27
|
+
const updateFromWidth = (width) => {
|
|
28
|
+
if (width <= 0) return;
|
|
29
|
+
setBarCount(Math.max(1, Math.floor((width + gapPx) / (barWidthPx + gapPx))));
|
|
30
|
+
};
|
|
31
|
+
updateFromWidth(track.getBoundingClientRect().width);
|
|
32
|
+
const observer = new ResizeObserver((entries) => {
|
|
33
|
+
const entry = entries[0];
|
|
34
|
+
if (entry) updateFromWidth(entry.contentRect.width);
|
|
35
|
+
});
|
|
36
|
+
observer.observe(track);
|
|
37
|
+
return () => observer.disconnect();
|
|
38
|
+
}, [trackRef, barWidthPx, gapPx]);
|
|
39
|
+
return barCount;
|
|
40
|
+
}
|
|
41
|
+
exports.useFittedBarCount = useFittedBarCount;
|
|
42
|
+
//# sourceMappingURL=useFittedBarCount.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFittedBarCount.cjs","sources":["../../../src/utils/useFittedBarCount.ts"],"sourcesContent":["import * as React from \"react\";\n\n/** Options for {@link useFittedBarCount}. */\nexport interface UseFittedBarCountOptions {\n /** Rendered width of a single bar, in pixels. */\n barWidthPx: number;\n /** Gap between bars, in pixels. */\n gapPx: number;\n /** Bar count used before the track is measured (SSR, jsdom, or zero-width layout). */\n fallback: number;\n}\n\n/**\n * Measures a waveform track and returns how many fixed-width bars fit across it,\n * updating on resize. Lets callers resample their peaks to the *visible* bar\n * count so overflow bars aren't rendered (and, crucially, aren't counted when\n * computing playback progress).\n */\nexport function useFittedBarCount(\n trackRef: React.RefObject<HTMLElement | null>,\n { barWidthPx, gapPx, fallback }: UseFittedBarCountOptions,\n): number {\n const [barCount, setBarCount] = React.useState(fallback);\n\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 + gapPx) / (barWidthPx + gapPx))));\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 }, [trackRef, barWidthPx, gapPx]);\n\n return barCount;\n}\n"],"names":["React"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAkBO,SAAS,kBACd,UACA,EAAE,YAAY,OAAO,YACb;AACR,QAAM,CAAC,UAAU,WAAW,IAAIA,iBAAM,SAAS,QAAQ;AAEvDA,mBAAM,UAAU,MAAM;AACpB,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AAEZ,UAAM,kBAAkB,CAAC,UAAkB;AACzC,UAAI,SAAS,EAAG;AAChB,kBAAY,KAAK,IAAI,GAAG,KAAK,OAAO,QAAQ,UAAU,aAAa,MAAM,CAAC,CAAC;AAAA,IAC7E;AAEA,oBAAgB,MAAM,sBAAA,EAAwB,KAAK;AAEnD,UAAM,WAAW,IAAI,eAAe,CAAC,YAAY;AAC/C,YAAM,QAAQ,QAAQ,CAAC;AACvB,UAAI,MAAO,iBAAgB,MAAM,YAAY,KAAK;AAAA,IACpD,CAAC;AACD,aAAS,QAAQ,KAAK;AACtB,WAAO,MAAM,SAAS,WAAA;AAAA,EACxB,GAAG,CAAC,UAAU,YAAY,KAAK,CAAC;AAEhC,SAAO;AACT;;"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const audioWaveform = require("./audioWaveform.cjs");
|
|
6
|
+
function _interopNamespaceDefault(e) {
|
|
7
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
8
|
+
if (e) {
|
|
9
|
+
for (const k in e) {
|
|
10
|
+
if (k !== "default") {
|
|
11
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: () => e[k]
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
23
|
+
function useWaveformPeaks(src, count = audioWaveform.RAW_PEAK_COUNT) {
|
|
24
|
+
const [peaks, setPeaks] = React__namespace.useState(
|
|
25
|
+
() => src ? audioWaveform.generateFallbackPeaks(src, count) : []
|
|
26
|
+
);
|
|
27
|
+
React__namespace.useEffect(() => {
|
|
28
|
+
if (!src) {
|
|
29
|
+
setPeaks([]);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setPeaks(audioWaveform.generateFallbackPeaks(src, count));
|
|
33
|
+
let cancelled = false;
|
|
34
|
+
const abortController = new AbortController();
|
|
35
|
+
audioWaveform.decodeAudioPeaks(src, abortController.signal).then((decoded) => {
|
|
36
|
+
if (!cancelled) setPeaks(decoded);
|
|
37
|
+
}).catch(() => {
|
|
38
|
+
if (!cancelled) setPeaks(audioWaveform.generateFallbackPeaks(src, count));
|
|
39
|
+
});
|
|
40
|
+
return () => {
|
|
41
|
+
cancelled = true;
|
|
42
|
+
abortController.abort();
|
|
43
|
+
};
|
|
44
|
+
}, [src, count]);
|
|
45
|
+
return peaks;
|
|
46
|
+
}
|
|
47
|
+
exports.useWaveformPeaks = useWaveformPeaks;
|
|
48
|
+
//# sourceMappingURL=useWaveformPeaks.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWaveformPeaks.cjs","sources":["../../../src/utils/useWaveformPeaks.ts"],"sourcesContent":["import * as React from \"react\";\nimport { decodeAudioPeaks, generateFallbackPeaks, RAW_PEAK_COUNT } from \"./audioWaveform\";\n\n/**\n * Decodes `src` into waveform peak amplitudes (0–1). Seeds a deterministic\n * fallback immediately (stable across renders/SSR) and swaps in the real\n * decoded peaks once WebAudio resolves, falling back again on any failure.\n * Returns an empty array when `src` is absent.\n */\nexport function useWaveformPeaks(\n src: string | undefined,\n count: number = RAW_PEAK_COUNT,\n): number[] {\n const [peaks, setPeaks] = React.useState<number[]>(() =>\n src ? generateFallbackPeaks(src, count) : [],\n );\n\n React.useEffect(() => {\n if (!src) {\n setPeaks([]);\n return;\n }\n\n setPeaks(generateFallbackPeaks(src, count));\n\n let cancelled = false;\n const abortController = new AbortController();\n decodeAudioPeaks(src, abortController.signal)\n .then((decoded) => {\n if (!cancelled) setPeaks(decoded);\n })\n .catch(() => {\n if (!cancelled) setPeaks(generateFallbackPeaks(src, count));\n });\n\n return () => {\n cancelled = true;\n abortController.abort();\n };\n }, [src, count]);\n\n return peaks;\n}\n"],"names":["RAW_PEAK_COUNT","React","generateFallbackPeaks","decodeAudioPeaks"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AASO,SAAS,iBACd,KACA,QAAgBA,8BACN;AACV,QAAM,CAAC,OAAO,QAAQ,IAAIC,iBAAM;AAAA,IAAmB,MACjD,MAAMC,cAAAA,sBAAsB,KAAK,KAAK,IAAI,CAAA;AAAA,EAAC;AAG7CD,mBAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAK;AACR,eAAS,CAAA,CAAE;AACX;AAAA,IACF;AAEA,aAASC,cAAAA,sBAAsB,KAAK,KAAK,CAAC;AAE1C,QAAI,YAAY;AAChB,UAAM,kBAAkB,IAAI,gBAAA;AAC5BC,kBAAAA,iBAAiB,KAAK,gBAAgB,MAAM,EACzC,KAAK,CAAC,YAAY;AACjB,UAAI,CAAC,UAAW,UAAS,OAAO;AAAA,IAClC,CAAC,EACA,MAAM,MAAM;AACX,UAAI,CAAC,UAAW,UAASD,cAAAA,sBAAsB,KAAK,KAAK,CAAC;AAAA,IAC5D,CAAC;AAEH,WAAO,MAAM;AACX,kBAAY;AACZ,sBAAgB,MAAA;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,KAAK,KAAK,CAAC;AAEf,SAAO;AACT;;"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const audioWaveform = require("./audioWaveform.cjs");
|
|
6
|
+
function _interopNamespaceDefault(e) {
|
|
7
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
8
|
+
if (e) {
|
|
9
|
+
for (const k in e) {
|
|
10
|
+
if (k !== "default") {
|
|
11
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: () => e[k]
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
23
|
+
const SEEK_STEP_SECONDS = 1;
|
|
24
|
+
const SEEK_STEP_LARGE_SECONDS = 5;
|
|
25
|
+
const STEP_BY_KEY = {
|
|
26
|
+
ArrowRight: SEEK_STEP_SECONDS,
|
|
27
|
+
ArrowUp: SEEK_STEP_SECONDS,
|
|
28
|
+
ArrowLeft: -SEEK_STEP_SECONDS,
|
|
29
|
+
ArrowDown: -SEEK_STEP_SECONDS,
|
|
30
|
+
PageUp: SEEK_STEP_LARGE_SECONDS,
|
|
31
|
+
PageDown: -SEEK_STEP_LARGE_SECONDS
|
|
32
|
+
};
|
|
33
|
+
function useWaveformSeek({
|
|
34
|
+
trackRef,
|
|
35
|
+
displayDuration,
|
|
36
|
+
currentTime,
|
|
37
|
+
seekTo,
|
|
38
|
+
setSeeking,
|
|
39
|
+
label = "Seek"
|
|
40
|
+
}) {
|
|
41
|
+
const [isDragging, setIsDragging] = React__namespace.useState(false);
|
|
42
|
+
const seekFromClientX = React__namespace.useCallback(
|
|
43
|
+
(clientX) => {
|
|
44
|
+
const track = trackRef.current;
|
|
45
|
+
if (!track || displayDuration === void 0) return;
|
|
46
|
+
const rect = track.getBoundingClientRect();
|
|
47
|
+
if (rect.width <= 0) return;
|
|
48
|
+
const fraction = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1);
|
|
49
|
+
seekTo(fraction * displayDuration);
|
|
50
|
+
},
|
|
51
|
+
[trackRef, displayDuration, seekTo]
|
|
52
|
+
);
|
|
53
|
+
React__namespace.useEffect(() => {
|
|
54
|
+
if (!isDragging) return;
|
|
55
|
+
const handleMove = (event) => seekFromClientX(event.clientX);
|
|
56
|
+
const handleUp = () => {
|
|
57
|
+
setIsDragging(false);
|
|
58
|
+
setSeeking(false);
|
|
59
|
+
};
|
|
60
|
+
window.addEventListener("pointermove", handleMove);
|
|
61
|
+
window.addEventListener("pointerup", handleUp);
|
|
62
|
+
return () => {
|
|
63
|
+
window.removeEventListener("pointermove", handleMove);
|
|
64
|
+
window.removeEventListener("pointerup", handleUp);
|
|
65
|
+
};
|
|
66
|
+
}, [isDragging, seekFromClientX, setSeeking]);
|
|
67
|
+
const onPointerDown = (event) => {
|
|
68
|
+
seekFromClientX(event.clientX);
|
|
69
|
+
setSeeking(true);
|
|
70
|
+
setIsDragging(true);
|
|
71
|
+
};
|
|
72
|
+
const onKeyDown = (event) => {
|
|
73
|
+
if (displayDuration === void 0) return;
|
|
74
|
+
const delta = STEP_BY_KEY[event.key];
|
|
75
|
+
if (delta !== void 0) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
seekTo(currentTime + delta);
|
|
78
|
+
} else if (event.key === "Home") {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
seekTo(0);
|
|
81
|
+
} else if (event.key === "End") {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
seekTo(displayDuration);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
role: "slider",
|
|
88
|
+
tabIndex: 0,
|
|
89
|
+
"aria-label": label,
|
|
90
|
+
"aria-orientation": "horizontal",
|
|
91
|
+
"aria-valuemin": 0,
|
|
92
|
+
"aria-valuemax": displayDuration,
|
|
93
|
+
"aria-valuenow": Math.round(currentTime),
|
|
94
|
+
"aria-valuetext": displayDuration !== void 0 ? `${audioWaveform.formatTime(currentTime)} of ${audioWaveform.formatTime(displayDuration)}` : void 0,
|
|
95
|
+
onPointerDown,
|
|
96
|
+
onKeyDown
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
exports.useWaveformSeek = useWaveformSeek;
|
|
100
|
+
//# sourceMappingURL=useWaveformSeek.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWaveformSeek.cjs","sources":["../../../src/utils/useWaveformSeek.ts"],"sourcesContent":["import * as React from \"react\";\nimport { formatTime } from \"./audioWaveform\";\n\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/** Signed seek delta (seconds) for each stepping key. */\nconst STEP_BY_KEY: Record<string, number | undefined> = {\n ArrowRight: SEEK_STEP_SECONDS,\n ArrowUp: SEEK_STEP_SECONDS,\n ArrowLeft: -SEEK_STEP_SECONDS,\n ArrowDown: -SEEK_STEP_SECONDS,\n PageUp: SEEK_STEP_LARGE_SECONDS,\n PageDown: -SEEK_STEP_LARGE_SECONDS,\n};\n\n/** Options for {@link useWaveformSeek}. */\nexport interface UseWaveformSeekOptions {\n /** Ref to the seekable track element (its width maps pointer position → time). */\n trackRef: React.RefObject<HTMLElement | null>;\n /** Total duration in seconds, or `undefined` until known. */\n displayDuration: number | undefined;\n /** Current playback position in seconds. */\n currentTime: number;\n /** Seek to a time in seconds. */\n seekTo: (time: number) => void;\n /** Suppress position updates while a manual scrub is in progress. */\n setSeeking: (seeking: boolean) => void;\n /** Accessible name for the slider. @default \"Seek\" */\n label?: string;\n}\n\n/** ARIA + event props to spread onto the seekable track element. */\nexport interface WaveformSeekProps {\n role: \"slider\";\n tabIndex: 0;\n \"aria-label\": string;\n \"aria-orientation\": \"horizontal\";\n \"aria-valuemin\": 0;\n \"aria-valuemax\": number | undefined;\n \"aria-valuenow\": number;\n \"aria-valuetext\": string | undefined;\n onPointerDown: (event: React.PointerEvent<HTMLElement>) => void;\n onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;\n}\n\n/**\n * Turns a waveform track into a keyboard- and pointer-seekable slider. Handles\n * click/drag position math (via window listeners so drags can leave the element)\n * and Arrow/Page/Home/End stepping, and returns ARIA slider props to spread onto\n * the track. Shared by waveform players so seek behaviour isn't reimplemented.\n */\nexport function useWaveformSeek({\n trackRef,\n displayDuration,\n currentTime,\n seekTo,\n setSeeking,\n label = \"Seek\",\n}: UseWaveformSeekOptions): WaveformSeekProps {\n const [isDragging, setIsDragging] = React.useState(false);\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 [trackRef, displayDuration, seekTo],\n );\n\n React.useEffect(() => {\n if (!isDragging) return;\n const handleMove = (event: PointerEvent) => seekFromClientX(event.clientX);\n const handleUp = () => {\n setIsDragging(false);\n setSeeking(false);\n };\n window.addEventListener(\"pointermove\", handleMove);\n window.addEventListener(\"pointerup\", handleUp);\n return () => {\n window.removeEventListener(\"pointermove\", handleMove);\n window.removeEventListener(\"pointerup\", handleUp);\n };\n }, [isDragging, seekFromClientX, setSeeking]);\n\n const onPointerDown = (event: React.PointerEvent<HTMLElement>) => {\n seekFromClientX(event.clientX);\n setSeeking(true);\n setIsDragging(true);\n };\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n if (displayDuration === undefined) return;\n const delta = STEP_BY_KEY[event.key];\n if (delta !== undefined) {\n event.preventDefault();\n seekTo(currentTime + delta);\n } else if (event.key === \"Home\") {\n event.preventDefault();\n seekTo(0);\n } else if (event.key === \"End\") {\n event.preventDefault();\n seekTo(displayDuration);\n }\n };\n\n return {\n role: \"slider\",\n tabIndex: 0,\n \"aria-label\": label,\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 : undefined,\n onPointerDown,\n onKeyDown,\n };\n}\n"],"names":["React","formatTime"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAIA,MAAM,oBAAoB;AAE1B,MAAM,0BAA0B;AAGhC,MAAM,cAAkD;AAAA,EACtD,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,WAAW,CAAC;AAAA,EACZ,WAAW,CAAC;AAAA,EACZ,QAAQ;AAAA,EACR,UAAU,CAAC;AACb;AAsCO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAA8C;AAC5C,QAAM,CAAC,YAAY,aAAa,IAAIA,iBAAM,SAAS,KAAK;AAExD,QAAM,kBAAkBA,iBAAM;AAAA,IAC5B,CAAC,YAAoB;AACnB,YAAM,QAAQ,SAAS;AACvB,UAAI,CAAC,SAAS,oBAAoB,OAAW;AAC7C,YAAM,OAAO,MAAM,sBAAA;AACnB,UAAI,KAAK,SAAS,EAAG;AACrB,YAAM,WAAW,KAAK,IAAI,KAAK,KAAK,UAAU,KAAK,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC;AAC5E,aAAO,WAAW,eAAe;AAAA,IACnC;AAAA,IACA,CAAC,UAAU,iBAAiB,MAAM;AAAA,EAAA;AAGpCA,mBAAM,UAAU,MAAM;AACpB,QAAI,CAAC,WAAY;AACjB,UAAM,aAAa,CAAC,UAAwB,gBAAgB,MAAM,OAAO;AACzE,UAAM,WAAW,MAAM;AACrB,oBAAc,KAAK;AACnB,iBAAW,KAAK;AAAA,IAClB;AACA,WAAO,iBAAiB,eAAe,UAAU;AACjD,WAAO,iBAAiB,aAAa,QAAQ;AAC7C,WAAO,MAAM;AACX,aAAO,oBAAoB,eAAe,UAAU;AACpD,aAAO,oBAAoB,aAAa,QAAQ;AAAA,IAClD;AAAA,EACF,GAAG,CAAC,YAAY,iBAAiB,UAAU,CAAC;AAE5C,QAAM,gBAAgB,CAAC,UAA2C;AAChE,oBAAgB,MAAM,OAAO;AAC7B,eAAW,IAAI;AACf,kBAAc,IAAI;AAAA,EACpB;AAEA,QAAM,YAAY,CAAC,UAA4C;AAC7D,QAAI,oBAAoB,OAAW;AACnC,UAAM,QAAQ,YAAY,MAAM,GAAG;AACnC,QAAI,UAAU,QAAW;AACvB,YAAM,eAAA;AACN,aAAO,cAAc,KAAK;AAAA,IAC5B,WAAW,MAAM,QAAQ,QAAQ;AAC/B,YAAM,eAAA;AACN,aAAO,CAAC;AAAA,IACV,WAAW,MAAM,QAAQ,OAAO;AAC9B,YAAM,eAAA;AACN,aAAO,eAAe;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,cAAc;AAAA,IACd,oBAAoB;AAAA,IACpB,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,iBAAiB,KAAK,MAAM,WAAW;AAAA,IACvC,kBACE,oBAAoB,SAChB,GAAGC,cAAAA,WAAW,WAAW,CAAC,OAAOA,cAAAA,WAAW,eAAe,CAAC,KAC5D;AAAA,IACN;AAAA,IACA;AAAA,EAAA;AAEJ;;"}
|