@fanvue/ui 3.12.1 → 3.14.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 (66) hide show
  1. package/dist/cjs/components/Accordion/AccordionTrigger.cjs +11 -2
  2. package/dist/cjs/components/Accordion/AccordionTrigger.cjs.map +1 -1
  3. package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +357 -0
  4. package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
  5. package/dist/cjs/components/Avatar/Avatar.cjs +2 -2
  6. package/dist/cjs/components/Avatar/Avatar.cjs.map +1 -1
  7. package/dist/cjs/components/Dialog/Dialog.cjs +2 -1
  8. package/dist/cjs/components/Dialog/Dialog.cjs.map +1 -1
  9. package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +25 -2
  10. package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  11. package/dist/cjs/components/FloatingActionButton/FloatingActionButton.cjs +59 -0
  12. package/dist/cjs/components/FloatingActionButton/FloatingActionButton.cjs.map +1 -0
  13. package/dist/cjs/components/Icons/GifIcon.cjs +12 -36
  14. package/dist/cjs/components/Icons/GifIcon.cjs.map +1 -1
  15. package/dist/cjs/components/Icons/GridViewIcon.cjs +50 -0
  16. package/dist/cjs/components/Icons/GridViewIcon.cjs.map +1 -0
  17. package/dist/cjs/components/Icons/ListViewIcon.cjs +50 -0
  18. package/dist/cjs/components/Icons/ListViewIcon.cjs.map +1 -0
  19. package/dist/cjs/components/ProgressBar/ProgressBar.cjs +30 -5
  20. package/dist/cjs/components/ProgressBar/ProgressBar.cjs.map +1 -1
  21. package/dist/cjs/components/ProgressBar/ProgressBarItem.cjs +49 -0
  22. package/dist/cjs/components/ProgressBar/ProgressBarItem.cjs.map +1 -0
  23. package/dist/cjs/components/ProgressBar/ProgressBarSteps.cjs +66 -0
  24. package/dist/cjs/components/ProgressBar/ProgressBarSteps.cjs.map +1 -0
  25. package/dist/cjs/components/SegmentedControl/SegmentedControl.cjs +42 -11
  26. package/dist/cjs/components/SegmentedControl/SegmentedControl.cjs.map +1 -1
  27. package/dist/cjs/components/TextArea/TextArea.cjs +4 -4
  28. package/dist/cjs/components/TextArea/TextArea.cjs.map +1 -1
  29. package/dist/cjs/components/TextField/TextField.cjs +105 -62
  30. package/dist/cjs/components/TextField/TextField.cjs.map +1 -1
  31. package/dist/cjs/index.cjs +12 -0
  32. package/dist/cjs/index.cjs.map +1 -1
  33. package/dist/components/Accordion/AccordionTrigger.mjs +11 -2
  34. package/dist/components/Accordion/AccordionTrigger.mjs.map +1 -1
  35. package/dist/components/AudioPlayer/AudioPlayer.mjs +340 -0
  36. package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -0
  37. package/dist/components/Avatar/Avatar.mjs +2 -2
  38. package/dist/components/Avatar/Avatar.mjs.map +1 -1
  39. package/dist/components/Dialog/Dialog.mjs +2 -1
  40. package/dist/components/Dialog/Dialog.mjs.map +1 -1
  41. package/dist/components/DropdownMenu/DropdownMenu.mjs +25 -2
  42. package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
  43. package/dist/components/FloatingActionButton/FloatingActionButton.mjs +42 -0
  44. package/dist/components/FloatingActionButton/FloatingActionButton.mjs.map +1 -0
  45. package/dist/components/Icons/GifIcon.mjs +12 -36
  46. package/dist/components/Icons/GifIcon.mjs.map +1 -1
  47. package/dist/components/Icons/GridViewIcon.mjs +33 -0
  48. package/dist/components/Icons/GridViewIcon.mjs.map +1 -0
  49. package/dist/components/Icons/ListViewIcon.mjs +33 -0
  50. package/dist/components/Icons/ListViewIcon.mjs.map +1 -0
  51. package/dist/components/ProgressBar/ProgressBar.mjs +30 -5
  52. package/dist/components/ProgressBar/ProgressBar.mjs.map +1 -1
  53. package/dist/components/ProgressBar/ProgressBarItem.mjs +32 -0
  54. package/dist/components/ProgressBar/ProgressBarItem.mjs.map +1 -0
  55. package/dist/components/ProgressBar/ProgressBarSteps.mjs +49 -0
  56. package/dist/components/ProgressBar/ProgressBarSteps.mjs.map +1 -0
  57. package/dist/components/SegmentedControl/SegmentedControl.mjs +42 -11
  58. package/dist/components/SegmentedControl/SegmentedControl.mjs.map +1 -1
  59. package/dist/components/TextArea/TextArea.mjs +4 -4
  60. package/dist/components/TextArea/TextArea.mjs.map +1 -1
  61. package/dist/components/TextField/TextField.mjs +105 -62
  62. package/dist/components/TextField/TextField.mjs.map +1 -1
  63. package/dist/index.d.ts +237 -10
  64. package/dist/index.mjs +12 -0
  65. package/dist/index.mjs.map +1 -1
  66. package/package.json +1 -1
@@ -0,0 +1,340 @@
1
+ "use client";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../../utils/cn.mjs";
5
+ import { PauseIcon } from "../Icons/PauseIcon.mjs";
6
+ import { PlayIcon } from "../Icons/PlayIcon.mjs";
7
+ const BAR_WIDTH_PX = 4;
8
+ const BAR_GAP_PX = 4;
9
+ const MIN_BAR_HEIGHT_PX = 4;
10
+ const MAX_BAR_HEIGHT_PX = 26;
11
+ const DEFAULT_BAR_COUNT = 24;
12
+ const RAW_PEAK_COUNT = 128;
13
+ const SEEK_STEP_SECONDS = 1;
14
+ const SEEK_STEP_LARGE_SECONDS = 5;
15
+ function formatTime(seconds) {
16
+ if (seconds === void 0 || !Number.isFinite(seconds)) return "--:--";
17
+ const totalSeconds = Math.max(0, Math.floor(seconds));
18
+ const minutes = Math.floor(totalSeconds / 60);
19
+ const remainingSeconds = totalSeconds % 60;
20
+ return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
21
+ }
22
+ function hashString(value) {
23
+ let hash = 2166136261;
24
+ for (let i = 0; i < value.length; i++) {
25
+ hash ^= value.charCodeAt(i);
26
+ hash = Math.imul(hash, 16777619);
27
+ }
28
+ return hash >>> 0;
29
+ }
30
+ function createSeededRandom(seed) {
31
+ let state = seed;
32
+ return () => {
33
+ state = state + 1831565813 | 0;
34
+ let t = Math.imul(state ^ state >>> 15, 1 | state);
35
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
36
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
37
+ };
38
+ }
39
+ function generateFallbackPeaks(src, count) {
40
+ const random = createSeededRandom(hashString(src));
41
+ return Array.from({ length: count }, () => 0.2 + random() * 0.8);
42
+ }
43
+ function computePeaksFromChannelData(channelData, count) {
44
+ const blockSize = Math.max(1, Math.floor(channelData.length / count));
45
+ const peaks = [];
46
+ for (let i = 0; i < count; i++) {
47
+ const start = i * blockSize;
48
+ let max = 0;
49
+ for (let j = 0; j < blockSize && start + j < channelData.length; j++) {
50
+ max = Math.max(max, Math.abs(channelData[start + j] ?? 0));
51
+ }
52
+ peaks.push(max);
53
+ }
54
+ return peaks;
55
+ }
56
+ function resamplePeaks(peaks, barCount) {
57
+ if (peaks.length === 0 || barCount <= 0) return [];
58
+ const step = peaks.length / barCount;
59
+ return Array.from(
60
+ { length: barCount },
61
+ (_, i) => peaks[Math.min(peaks.length - 1, Math.floor(i * step))] ?? 0
62
+ );
63
+ }
64
+ async function decodeAudioPeaks(src, signal) {
65
+ const AudioContextCtor = window.AudioContext ?? window.webkitAudioContext;
66
+ if (!AudioContextCtor) throw new Error("WebAudio unsupported");
67
+ const response = await fetch(src, { signal });
68
+ const arrayBuffer = await response.arrayBuffer();
69
+ const audioContext = new AudioContextCtor();
70
+ try {
71
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
72
+ return computePeaksFromChannelData(audioBuffer.getChannelData(0), RAW_PEAK_COUNT);
73
+ } finally {
74
+ if (audioContext.state !== "closed") audioContext.close().catch(() => {
75
+ });
76
+ }
77
+ }
78
+ const AudioPlayer = React.forwardRef(
79
+ ({
80
+ className,
81
+ src,
82
+ duration,
83
+ playing: controlledPlaying,
84
+ defaultPlaying = false,
85
+ onPlay,
86
+ onPause,
87
+ onEnded,
88
+ size = "40",
89
+ ...props
90
+ }, ref) => {
91
+ const audioRef = React.useRef(null);
92
+ const trackRef = React.useRef(null);
93
+ const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);
94
+ const isControlled = controlledPlaying !== void 0;
95
+ const playing = isControlled ? controlledPlaying : internalPlaying;
96
+ const [currentTime, setCurrentTime] = React.useState(0);
97
+ const [mediaDuration, setMediaDuration] = React.useState(void 0);
98
+ const [barCount, setBarCount] = React.useState(DEFAULT_BAR_COUNT);
99
+ const [peaks, setPeaks] = React.useState(
100
+ () => generateFallbackPeaks(src, RAW_PEAK_COUNT)
101
+ );
102
+ const [isDragging, setIsDragging] = React.useState(false);
103
+ const displayDuration = mediaDuration ?? duration;
104
+ const hasStarted = playing || currentTime > 0;
105
+ const playingRef = React.useRef(playing);
106
+ playingRef.current = playing;
107
+ const isControlledRef = React.useRef(isControlled);
108
+ isControlledRef.current = isControlled;
109
+ const onPauseRef = React.useRef(onPause);
110
+ onPauseRef.current = onPause;
111
+ const isFirstSrcRenderRef = React.useRef(true);
112
+ React.useEffect(() => {
113
+ if (isFirstSrcRenderRef.current) {
114
+ isFirstSrcRenderRef.current = false;
115
+ return;
116
+ }
117
+ setCurrentTime(0);
118
+ setMediaDuration(void 0);
119
+ setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));
120
+ if (playingRef.current) {
121
+ if (!isControlledRef.current) setInternalPlaying(false);
122
+ onPauseRef.current?.();
123
+ }
124
+ }, [src]);
125
+ React.useEffect(() => {
126
+ let cancelled = false;
127
+ const abortController = new AbortController();
128
+ decodeAudioPeaks(src, abortController.signal).then((decodedPeaks) => {
129
+ if (!cancelled) setPeaks(decodedPeaks);
130
+ }).catch(() => {
131
+ if (!cancelled) setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));
132
+ });
133
+ return () => {
134
+ cancelled = true;
135
+ abortController.abort();
136
+ };
137
+ }, [src]);
138
+ React.useEffect(() => {
139
+ const track = trackRef.current;
140
+ if (!track) return;
141
+ const updateFromWidth = (width) => {
142
+ if (width <= 0) return;
143
+ setBarCount(Math.max(1, Math.floor((width + BAR_GAP_PX) / (BAR_WIDTH_PX + BAR_GAP_PX))));
144
+ };
145
+ updateFromWidth(track.getBoundingClientRect().width);
146
+ const observer = new ResizeObserver((entries) => {
147
+ const entry = entries[0];
148
+ if (entry) updateFromWidth(entry.contentRect.width);
149
+ });
150
+ observer.observe(track);
151
+ return () => observer.disconnect();
152
+ }, []);
153
+ React.useEffect(() => {
154
+ const audio = audioRef.current;
155
+ if (!audio) return;
156
+ if (playing) {
157
+ audio.play().catch(() => {
158
+ if (!isControlled) setInternalPlaying(false);
159
+ onPauseRef.current?.();
160
+ });
161
+ } else {
162
+ audio.pause();
163
+ }
164
+ }, [playing, isControlled]);
165
+ const handleToggle = () => {
166
+ const next = !playing;
167
+ if (!isControlled) setInternalPlaying(next);
168
+ if (next) onPlay?.();
169
+ else onPause?.();
170
+ };
171
+ const seekTo = React.useCallback(
172
+ (time) => {
173
+ if (displayDuration === void 0) return;
174
+ const clamped = Math.min(Math.max(time, 0), displayDuration);
175
+ setCurrentTime(clamped);
176
+ const audio = audioRef.current;
177
+ if (audio) audio.currentTime = clamped;
178
+ },
179
+ [displayDuration]
180
+ );
181
+ const seekFromClientX = React.useCallback(
182
+ (clientX) => {
183
+ const track = trackRef.current;
184
+ if (!track || displayDuration === void 0) return;
185
+ const rect = track.getBoundingClientRect();
186
+ if (rect.width <= 0) return;
187
+ const fraction = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1);
188
+ seekTo(fraction * displayDuration);
189
+ },
190
+ [displayDuration, seekTo]
191
+ );
192
+ React.useEffect(() => {
193
+ if (!isDragging) return;
194
+ const handleWindowPointerMove = (event) => seekFromClientX(event.clientX);
195
+ const handleWindowPointerUp = () => setIsDragging(false);
196
+ window.addEventListener("pointermove", handleWindowPointerMove);
197
+ window.addEventListener("pointerup", handleWindowPointerUp);
198
+ return () => {
199
+ window.removeEventListener("pointermove", handleWindowPointerMove);
200
+ window.removeEventListener("pointerup", handleWindowPointerUp);
201
+ };
202
+ }, [isDragging, seekFromClientX]);
203
+ const handlePointerDown = (event) => {
204
+ seekFromClientX(event.clientX);
205
+ setIsDragging(true);
206
+ };
207
+ const handleKeyDown = (event) => {
208
+ if (displayDuration === void 0) return;
209
+ switch (event.key) {
210
+ case "ArrowRight":
211
+ case "ArrowUp":
212
+ event.preventDefault();
213
+ seekTo(currentTime + SEEK_STEP_SECONDS);
214
+ break;
215
+ case "ArrowLeft":
216
+ case "ArrowDown":
217
+ event.preventDefault();
218
+ seekTo(currentTime - SEEK_STEP_SECONDS);
219
+ break;
220
+ case "PageUp":
221
+ event.preventDefault();
222
+ seekTo(currentTime + SEEK_STEP_LARGE_SECONDS);
223
+ break;
224
+ case "PageDown":
225
+ event.preventDefault();
226
+ seekTo(currentTime - SEEK_STEP_LARGE_SECONDS);
227
+ break;
228
+ case "Home":
229
+ event.preventDefault();
230
+ seekTo(0);
231
+ break;
232
+ case "End":
233
+ event.preventDefault();
234
+ seekTo(displayDuration);
235
+ break;
236
+ }
237
+ };
238
+ const bars = resamplePeaks(peaks, barCount);
239
+ const progressFraction = displayDuration && displayDuration > 0 ? Math.min(Math.max(currentTime / displayDuration, 0), 1) : 0;
240
+ const playedBarCount = hasStarted ? Math.round(progressFraction * bars.length) : bars.length;
241
+ const barData = bars.map((amplitude, index) => {
242
+ const isPlayed = index < playedBarCount;
243
+ return {
244
+ heightPx: MIN_BAR_HEIGHT_PX + amplitude * (MAX_BAR_HEIGHT_PX - MIN_BAR_HEIGHT_PX),
245
+ className: cn(
246
+ "w-1 shrink-0 rounded-3xs bg-messages-waveform-default",
247
+ "motion-safe:transition-opacity motion-safe:duration-150 motion-safe:ease-in-out",
248
+ isPlayed ? "opacity-100" : "opacity-50"
249
+ )
250
+ };
251
+ });
252
+ return /* @__PURE__ */ jsxs(
253
+ "div",
254
+ {
255
+ ref,
256
+ "data-size": size,
257
+ className: cn("flex w-full items-center gap-3", className),
258
+ ...props,
259
+ children: [
260
+ /* @__PURE__ */ jsx(
261
+ "button",
262
+ {
263
+ type: "button",
264
+ onClick: handleToggle,
265
+ "aria-label": playing ? "Pause" : "Play",
266
+ className: cn(
267
+ "flex size-8 shrink-0 items-center justify-center rounded-full",
268
+ "bg-buttons-secondary-default text-icons-primary backdrop-blur-[40px]",
269
+ "motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
270
+ "hover:bg-buttons-secondary-hover",
271
+ "focus-visible:shadow-focus-ring focus-visible:outline-none"
272
+ ),
273
+ children: playing ? /* @__PURE__ */ jsx(PauseIcon, { size: 16, filled: true }) : /* @__PURE__ */ jsx(PlayIcon, { size: 16, filled: true })
274
+ }
275
+ ),
276
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [
277
+ /* @__PURE__ */ jsx(
278
+ "div",
279
+ {
280
+ ref: trackRef,
281
+ role: "slider",
282
+ tabIndex: 0,
283
+ "aria-label": "Seek",
284
+ "aria-orientation": "horizontal",
285
+ "aria-valuemin": 0,
286
+ "aria-valuemax": displayDuration,
287
+ "aria-valuenow": Math.round(currentTime),
288
+ "aria-valuetext": displayDuration !== void 0 ? `${formatTime(currentTime)} of ${formatTime(displayDuration)}` : formatTime(currentTime),
289
+ onPointerDown: handlePointerDown,
290
+ onKeyDown: handleKeyDown,
291
+ className: cn(
292
+ "flex h-10 min-w-0 flex-1 cursor-pointer touch-none select-none items-center gap-1 overflow-hidden",
293
+ "focus-visible:shadow-focus-ring focus-visible:outline-none"
294
+ ),
295
+ children: barData.map((bar, index) => (
296
+ // biome-ignore lint/suspicious/noArrayIndexKey: bars are a fixed-count, non-reorderable list
297
+ /* @__PURE__ */ jsx("span", { className: bar.className, style: { height: bar.heightPx } }, index)
298
+ ))
299
+ }
300
+ ),
301
+ /* @__PURE__ */ jsx("span", { className: "typography-body-small-14px-semibold flex shrink-0 items-center gap-0.5 whitespace-nowrap", children: hasStarted ? /* @__PURE__ */ jsxs(Fragment, { children: [
302
+ /* @__PURE__ */ jsx("span", { className: "text-content-primary", children: formatTime(currentTime) }),
303
+ /* @__PURE__ */ jsxs("span", { className: "text-content-secondary", children: [
304
+ "/",
305
+ formatTime(displayDuration)
306
+ ] })
307
+ ] }) : /* @__PURE__ */ jsx("span", { className: "text-content-primary", children: formatTime(displayDuration) }) })
308
+ ] }),
309
+ /* @__PURE__ */ jsx(
310
+ "audio",
311
+ {
312
+ ref: audioRef,
313
+ src,
314
+ preload: "metadata",
315
+ className: "hidden",
316
+ onLoadedMetadata: (event) => {
317
+ const { duration: nativeDuration } = event.currentTarget;
318
+ setMediaDuration(Number.isFinite(nativeDuration) ? nativeDuration : void 0);
319
+ },
320
+ onTimeUpdate: (event) => {
321
+ if (isDragging) return;
322
+ setCurrentTime(event.currentTarget.currentTime);
323
+ },
324
+ onEnded: () => {
325
+ setCurrentTime(0);
326
+ if (!isControlled) setInternalPlaying(false);
327
+ onEnded?.();
328
+ }
329
+ }
330
+ )
331
+ ]
332
+ }
333
+ );
334
+ }
335
+ );
336
+ AudioPlayer.displayName = "AudioPlayer";
337
+ export {
338
+ AudioPlayer
339
+ };
340
+ //# sourceMappingURL=AudioPlayer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioPlayer.mjs","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":[],"mappings":"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"}
@@ -28,7 +28,7 @@ const AvatarRoot = React.forwardRef(
28
28
  ...props
29
29
  }, ref) => {
30
30
  const statusPosition = STATUS_POSITIONS[size];
31
- return /* @__PURE__ */ jsx(AvatarContext.Provider, { value: { size, NSFWShow }, children: /* @__PURE__ */ jsxs("div", { className: "relative inline-flex", children: [
31
+ return /* @__PURE__ */ jsx(AvatarContext.Provider, { value: { size, NSFWShow }, children: /* @__PURE__ */ jsxs("span", { className: "relative inline-flex", children: [
32
32
  /* @__PURE__ */ jsx(
33
33
  AvatarPrimitive.Root,
34
34
  {
@@ -51,7 +51,7 @@ const AvatarRoot = React.forwardRef(
51
51
  }
52
52
  ),
53
53
  platinumShow && /* @__PURE__ */ jsx(
54
- "div",
54
+ "span",
55
55
  {
56
56
  className: "pointer-events-none absolute inset-0 rounded-full",
57
57
  style: {
@@ -1 +1 @@
1
- {"version":3,"file":"Avatar.mjs","sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Allowed pixel sizes for the avatar. */\nexport type AvatarSize = 16 | 24 | 32 | 40 | 48 | 64 | 88 | 148;\n\nconst AvatarContext = React.createContext<{ size: AvatarSize; NSFWShow: boolean }>({\n size: 40,\n NSFWShow: false,\n});\n\nconst STATUS_POSITIONS: Record<\n AvatarSize,\n { top: number; right: number; indicatorSize: string; borderSize: string }\n> = {\n 16: { top: -2, right: -2, indicatorSize: \"size-2\", borderSize: \"border\" },\n 24: { top: 0, right: 0, indicatorSize: \"size-2\", borderSize: \"border\" },\n 32: { top: 0, right: 0, indicatorSize: \"size-2\", borderSize: \"border\" },\n 40: { top: 2, right: 2, indicatorSize: \"size-2\", borderSize: \"border\" },\n 48: { top: 5, right: 2, indicatorSize: \"size-2\", borderSize: \"border\" },\n 64: { top: 5, right: 1, indicatorSize: \"size-3\", borderSize: \"border\" },\n 88: { top: 8, right: 6, indicatorSize: \"size-3\", borderSize: \"border\" },\n 148: { top: 15, right: 15, indicatorSize: \"size-3\", borderSize: \"border\" },\n};\n\n/** Shared avatar styling props. */\ninterface AvatarStyleProps {\n /** Pixel size of the avatar. @default 40 */\n size?: AvatarSize;\n /** Whether to show the online-status indicator dot. @default false */\n onlineIndicator?: boolean;\n /** Whether to show the platinum gradient border ring. @default false */\n platinumShow?: boolean;\n /** Whether to apply the NSFW blur filter over the image. @default false */\n NSFWShow?: boolean;\n}\n\n/** Props for the low-level {@link AvatarRoot} compound component. */\nexport interface AvatarRootProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {}\n\n/**\n * Low-level avatar root for custom compositions. Provides size context to\n * child `AvatarImage` and `AvatarFallback` components.\n *\n * Prefer the higher-level {@link Avatar} component for most use cases.\n */\nconst AvatarRoot = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarRootProps\n>(\n (\n {\n className,\n size = 40,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const statusPosition = STATUS_POSITIONS[size];\n\n return (\n <AvatarContext.Provider value={{ size, NSFWShow }}>\n <div className=\"relative inline-flex\">\n <AvatarPrimitive.Root\n ref={ref}\n data-testid=\"avatar\"\n className={cn(\n \"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-background-avatar\",\n size === 16 && \"size-4 text-2xs\",\n size === 24 && \"size-6 text-xs\",\n size === 32 && \"size-8 text-xs\",\n size === 40 && \"size-10 text-sm\",\n size === 48 && \"size-12 text-base\",\n size === 64 && \"size-16 text-xl\",\n size === 88 && \"size-22 text-2xl\",\n size === 148 && \"size-37 text-4xl\",\n className,\n )}\n {...props}\n >\n {children}\n </AvatarPrimitive.Root>\n {platinumShow && (\n <div\n className=\"pointer-events-none absolute inset-0 rounded-full\"\n style={{\n background: `linear-gradient(143deg, #504F54 0%, #B1B1B1 20.3154%, #13181C 37.3727%, #C6C6C8 58.8154%, #FFFFFF 69.3154%, #0C0F14 81.3154%, #696A6E 100%)`,\n WebkitMask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n mask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n }}\n aria-hidden=\"true\"\n />\n )}\n {onlineIndicator && (\n <span\n className={cn(\n \"absolute rounded-full border-surface-primary bg-brand-primary-default\",\n statusPosition.borderSize,\n statusPosition.indicatorSize,\n )}\n style={{\n top: `${statusPosition.top}px`,\n right: `${statusPosition.right}px`,\n }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n </AvatarContext.Provider>\n );\n },\n);\n\nAvatarRoot.displayName = \"AvatarRoot\";\n\n/** Props for the {@link AvatarImage} compound component. */\nexport interface AvatarImageProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> {}\n\n/** Renders the avatar image. Automatically applies the NSFW blur when enabled on the parent `AvatarRoot`. */\nconst AvatarImage = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Image>,\n AvatarImageProps\n>(({ className, ...props }, ref) => {\n const { NSFWShow } = React.useContext(AvatarContext);\n return (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"size-full object-cover\", NSFWShow && \"blur-md\", className)}\n {...props}\n />\n );\n});\n\nAvatarImage.displayName = \"AvatarImage\";\n\n/** Props for the {@link AvatarFallback} compound component. */\nexport interface AvatarFallbackProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> {}\n\n/** Renders fallback content (e.g. initials or an icon) when the avatar image has not loaded. */\nconst AvatarFallback = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Fallback>,\n AvatarFallbackProps\n>(({ className, children, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"flex size-full items-center justify-center font-semibold text-content-primary uppercase leading-none\",\n className,\n )}\n delayMs={0}\n {...props}\n >\n {children}\n </AvatarPrimitive.Fallback>\n));\n\nAvatarFallback.displayName = \"AvatarFallback\";\n\nexport interface AvatarProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {\n /** URL of the avatar image. */\n src?: string;\n /** Alt text for the avatar image. @default \"Avatar\" */\n alt?: string;\n /** Fallback content rendered when the image has not loaded (e.g. initials or an icon). */\n fallback?: React.ReactNode;\n}\n\n/**\n * Displays a user avatar with optional online indicator, platinum border, and\n * NSFW blur. Pass `src` and `fallback` for the simple API, or compose your own\n * layout with `AvatarRoot`, `AvatarImage`, and `AvatarFallback` as children.\n *\n * @example\n * ```tsx\n * <Avatar src=\"/photo.jpg\" alt=\"Jane Doe\" fallback=\"JD\" size={48} />\n * ```\n */\nexport const Avatar = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(\n (\n {\n className,\n size = 40,\n src,\n alt,\n fallback,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const rootProps = {\n ref,\n size,\n onlineIndicator,\n platinumShow,\n NSFWShow,\n className,\n ...props,\n };\n\n if (children) {\n return <AvatarRoot {...rootProps}>{children}</AvatarRoot>;\n }\n\n return (\n <AvatarRoot {...rootProps}>\n {src && <AvatarImage src={src} alt={alt ?? \"Avatar\"} />}\n <AvatarFallback>{fallback}</AvatarFallback>\n </AvatarRoot>\n );\n },\n);\n\nAvatar.displayName = \"Avatar\";\n\nexport { AvatarRoot, AvatarImage, AvatarFallback };\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Avatar.mjs","sources":["../../../src/components/Avatar/Avatar.tsx"],"sourcesContent":["import * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Allowed pixel sizes for the avatar. */\nexport type AvatarSize = 16 | 24 | 32 | 40 | 48 | 64 | 88 | 148;\n\nconst AvatarContext = React.createContext<{ size: AvatarSize; NSFWShow: boolean }>({\n size: 40,\n NSFWShow: false,\n});\n\nconst STATUS_POSITIONS: Record<\n AvatarSize,\n { top: number; right: number; indicatorSize: string; borderSize: string }\n> = {\n 16: { top: -2, right: -2, indicatorSize: \"size-2\", borderSize: \"border\" },\n 24: { top: 0, right: 0, indicatorSize: \"size-2\", borderSize: \"border\" },\n 32: { top: 0, right: 0, indicatorSize: \"size-2\", borderSize: \"border\" },\n 40: { top: 2, right: 2, indicatorSize: \"size-2\", borderSize: \"border\" },\n 48: { top: 5, right: 2, indicatorSize: \"size-2\", borderSize: \"border\" },\n 64: { top: 5, right: 1, indicatorSize: \"size-3\", borderSize: \"border\" },\n 88: { top: 8, right: 6, indicatorSize: \"size-3\", borderSize: \"border\" },\n 148: { top: 15, right: 15, indicatorSize: \"size-3\", borderSize: \"border\" },\n};\n\n/** Shared avatar styling props. */\ninterface AvatarStyleProps {\n /** Pixel size of the avatar. @default 40 */\n size?: AvatarSize;\n /** Whether to show the online-status indicator dot. @default false */\n onlineIndicator?: boolean;\n /** Whether to show the platinum gradient border ring. @default false */\n platinumShow?: boolean;\n /** Whether to apply the NSFW blur filter over the image. @default false */\n NSFWShow?: boolean;\n}\n\n/** Props for the low-level {@link AvatarRoot} compound component. */\nexport interface AvatarRootProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {}\n\n/**\n * Low-level avatar root for custom compositions. Provides size context to\n * child `AvatarImage` and `AvatarFallback` components.\n *\n * Prefer the higher-level {@link Avatar} component for most use cases.\n */\nconst AvatarRoot = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarRootProps\n>(\n (\n {\n className,\n size = 40,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const statusPosition = STATUS_POSITIONS[size];\n\n return (\n <AvatarContext.Provider value={{ size, NSFWShow }}>\n <span className=\"relative inline-flex\">\n <AvatarPrimitive.Root\n ref={ref}\n data-testid=\"avatar\"\n className={cn(\n \"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-background-avatar\",\n size === 16 && \"size-4 text-2xs\",\n size === 24 && \"size-6 text-xs\",\n size === 32 && \"size-8 text-xs\",\n size === 40 && \"size-10 text-sm\",\n size === 48 && \"size-12 text-base\",\n size === 64 && \"size-16 text-xl\",\n size === 88 && \"size-22 text-2xl\",\n size === 148 && \"size-37 text-4xl\",\n className,\n )}\n {...props}\n >\n {children}\n </AvatarPrimitive.Root>\n {platinumShow && (\n <span\n className=\"pointer-events-none absolute inset-0 rounded-full\"\n style={{\n background: `linear-gradient(143deg, #504F54 0%, #B1B1B1 20.3154%, #13181C 37.3727%, #C6C6C8 58.8154%, #FFFFFF 69.3154%, #0C0F14 81.3154%, #696A6E 100%)`,\n WebkitMask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n mask: \"radial-gradient(circle closest-side, transparent 96%, black 96%)\",\n }}\n aria-hidden=\"true\"\n />\n )}\n {onlineIndicator && (\n <span\n className={cn(\n \"absolute rounded-full border-surface-primary bg-brand-primary-default\",\n statusPosition.borderSize,\n statusPosition.indicatorSize,\n )}\n style={{\n top: `${statusPosition.top}px`,\n right: `${statusPosition.right}px`,\n }}\n aria-hidden=\"true\"\n />\n )}\n </span>\n </AvatarContext.Provider>\n );\n },\n);\n\nAvatarRoot.displayName = \"AvatarRoot\";\n\n/** Props for the {@link AvatarImage} compound component. */\nexport interface AvatarImageProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> {}\n\n/** Renders the avatar image. Automatically applies the NSFW blur when enabled on the parent `AvatarRoot`. */\nconst AvatarImage = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Image>,\n AvatarImageProps\n>(({ className, ...props }, ref) => {\n const { NSFWShow } = React.useContext(AvatarContext);\n return (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"size-full object-cover\", NSFWShow && \"blur-md\", className)}\n {...props}\n />\n );\n});\n\nAvatarImage.displayName = \"AvatarImage\";\n\n/** Props for the {@link AvatarFallback} compound component. */\nexport interface AvatarFallbackProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> {}\n\n/** Renders fallback content (e.g. initials or an icon) when the avatar image has not loaded. */\nconst AvatarFallback = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Fallback>,\n AvatarFallbackProps\n>(({ className, children, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"flex size-full items-center justify-center font-semibold text-content-primary uppercase leading-none\",\n className,\n )}\n delayMs={0}\n {...props}\n >\n {children}\n </AvatarPrimitive.Fallback>\n));\n\nAvatarFallback.displayName = \"AvatarFallback\";\n\nexport interface AvatarProps\n extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,\n AvatarStyleProps {\n /** URL of the avatar image. */\n src?: string;\n /** Alt text for the avatar image. @default \"Avatar\" */\n alt?: string;\n /** Fallback content rendered when the image has not loaded (e.g. initials or an icon). */\n fallback?: React.ReactNode;\n}\n\n/**\n * Displays a user avatar with optional online indicator, platinum border, and\n * NSFW blur. Pass `src` and `fallback` for the simple API, or compose your own\n * layout with `AvatarRoot`, `AvatarImage`, and `AvatarFallback` as children.\n *\n * @example\n * ```tsx\n * <Avatar src=\"/photo.jpg\" alt=\"Jane Doe\" fallback=\"JD\" size={48} />\n * ```\n */\nexport const Avatar = React.forwardRef<\n React.ComponentRef<typeof AvatarPrimitive.Root>,\n AvatarProps\n>(\n (\n {\n className,\n size = 40,\n src,\n alt,\n fallback,\n onlineIndicator = false,\n platinumShow = false,\n NSFWShow = false,\n children,\n ...props\n },\n ref,\n ) => {\n const rootProps = {\n ref,\n size,\n onlineIndicator,\n platinumShow,\n NSFWShow,\n className,\n ...props,\n };\n\n if (children) {\n return <AvatarRoot {...rootProps}>{children}</AvatarRoot>;\n }\n\n return (\n <AvatarRoot {...rootProps}>\n {src && <AvatarImage src={src} alt={alt ?? \"Avatar\"} />}\n <AvatarFallback>{fallback}</AvatarFallback>\n </AvatarRoot>\n );\n },\n);\n\nAvatar.displayName = \"Avatar\";\n\nexport { AvatarRoot, AvatarImage, AvatarFallback };\n"],"names":[],"mappings":"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"}
@@ -38,12 +38,13 @@ const DialogContent = React.forwardRef(
38
38
  portal = true,
39
39
  showMobileHandle = true,
40
40
  mobilePresentation = "sheet",
41
+ overlayProps,
41
42
  style,
42
43
  onOpenAutoFocus,
43
44
  ...props
44
45
  }, ref) => {
45
46
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [
46
- overlay && /* @__PURE__ */ jsx(DialogOverlay, {}),
47
+ overlay && /* @__PURE__ */ jsx(DialogOverlay, { ...overlayProps }),
47
48
  /* @__PURE__ */ jsxs(
48
49
  DialogPrimitive.Content,
49
50
  {
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.mjs","sources":["../../../src/components/Dialog/Dialog.tsx"],"sourcesContent":["import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useSuppressClickAfterDrag } from \"../../utils/useSuppressClickAfterDrag\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { ArrowLeftIcon } from \"../Icons/ArrowLeftIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Props for the {@link Dialog} root component. */\nexport interface DialogProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {\n /** Controlled open state. When provided, you must also supply `onOpenChange`. */\n open?: boolean;\n /** Called when the open state changes. Required when `open` is controlled. */\n onOpenChange?: (open: boolean) => void;\n /** The open state of the dialog when it is initially rendered (uncontrolled). */\n defaultOpen?: boolean;\n}\n\n/** Root component that manages open/close state for a dialog. */\nexport const Dialog = DialogPrimitive.Root;\n\n/** Props for the {@link DialogTrigger} component. */\nexport type DialogTriggerProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>;\n\n/**\n * The element that opens the dialog when clicked.\n *\n * On touch / pen, a press-and-release that crosses a small movement threshold\n * is treated as a drag and the resulting synthetic click is suppressed —\n * defends against Android Chrome opening the dialog on a scroll-drag-end.\n */\nexport const DialogTrigger = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Trigger>,\n DialogTriggerProps\n>((props, ref) => <DialogPrimitive.Trigger ref={ref} {...useSuppressClickAfterDrag(props)} />);\nDialogTrigger.displayName = \"DialogTrigger\";\n\n/** Convenience alias for Radix `Dialog.Close`. Closes the dialog when clicked. */\nexport const DialogClose = DialogPrimitive.Close;\n\n/** Props for the {@link DialogClose} component. */\nexport type DialogCloseProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>;\n\nexport interface DialogOverlayProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> {}\n\n/**\n * Semi-transparent backdrop rendered behind the dialog content.\n * Rendered by {@link DialogContent}; portaled to `document.body` when {@link DialogContent} `portal` is true.\n */\nexport const DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n DialogOverlayProps\n>(({ className, style, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 fixed inset-0 bg-background-overlay-default data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className,\n )}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\nexport interface DialogContentProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n /**\n * Width preset for the dialog.\n * - `\"sm\"` — 400px max-width (confirmations, simple forms)\n * - `\"md\"` — 440px max-width (default, standard dialogs)\n * - `\"lg\"` — 600px max-width (complex content, tables)\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** When true, renders overlay automatically. @default true */\n overlay?: boolean;\n /**\n * When true, teleports overlay and panel to `document.body`.\n * When false, renders inline in the React tree (useful inside theme providers or scoped containers).\n * @default true\n */\n portal?: boolean;\n /** Show the v2 mobile sheet pull handle. Only rendered when `mobilePresentation` is `\"sheet\"`. @default true */\n showMobileHandle?: boolean;\n /**\n * How the dialog presents below the `sm` breakpoint.\n * - `\"sheet\"` — bottom sheet pinned to the viewport bottom edge (default)\n * - `\"card\"` — centered floating card per the v2-modal confirmation spec:\n * 16px side margins, 24px padding, 32px radius on all corners, no pull handle\n *\n * @default \"sheet\"\n */\n mobilePresentation?: \"sheet\" | \"card\";\n}\n\nconst SIZE_CLASSES: Record<NonNullable<DialogContentProps[\"size\"]>, string> = {\n sm: \"sm:max-w-[400px]\",\n md: \"sm:max-w-[440px]\",\n lg: \"sm:max-w-[600px]\",\n};\n\n/**\n * The dialog panel. Includes the overlay by default and portals to `document.body` by default.\n *\n * Set `portal={false}` to keep overlay and content in the DOM subtree of the parent `Dialog`.\n * `fixed` positioning still applies; ancestors with `transform` or `overflow` may affect layout.\n *\n * On mobile viewports (<640px), the dialog slides up from the bottom as a sheet\n * with top-only border radius by default; pass `mobilePresentation=\"card\"` to\n * render a centered floating card instead (used for small confirmation dialogs).\n * On larger viewports it renders centered with full border radius.\n *\n * @example\n * ```tsx\n * <Dialog>\n * <DialogTrigger asChild>\n * <Button>Open</Button>\n * </DialogTrigger>\n * <DialogContent>\n * <DialogHeader>\n * <DialogTitle>Title</DialogTitle>\n * </DialogHeader>\n * <DialogBody>Content here</DialogBody>\n * <DialogFooter>\n * <DialogClose asChild>\n * <Button variant=\"secondary\">Cancel</Button>\n * </DialogClose>\n * <Button>Accept</Button>\n * </DialogFooter>\n * </DialogContent>\n * </Dialog>\n * ```\n */\nexport const DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(\n (\n {\n className,\n children,\n size = \"md\",\n overlay = true,\n portal = true,\n showMobileHandle = true,\n mobilePresentation = \"sheet\",\n style,\n onOpenAutoFocus,\n ...props\n },\n ref,\n ) => {\n const content = (\n <>\n {overlay && <DialogOverlay />}\n <DialogPrimitive.Content\n ref={ref}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n onOpenAutoFocus={(e) => {\n if (onOpenAutoFocus) {\n onOpenAutoFocus(e);\n return;\n }\n e.preventDefault();\n (e.currentTarget as HTMLElement).focus();\n }}\n className={cn(\n \"fixed flex flex-col overflow-hidden border border-modal-stroke bg-modal-background shadow-blur-menu backdrop-blur-[4px] focus:outline-none\",\n \"data-[state=open]:fade-in-0 data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:animate-out\",\n mobilePresentation === \"card\"\n ? // Floating confirmation card (v2-modal): 16px side margins, vertically centered, 32px radius\n cn(\n \"inset-x-4 top-1/2 max-h-[85vh] -translate-y-1/2 rounded-xl p-6\",\n \"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95\",\n \"sm:inset-x-auto\",\n )\n : // Bottom sheet pinned to the viewport bottom edge\n cn(\n \"inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-xl p-4 pt-3\",\n \"data-[state=open]:slide-in-from-bottom-full\",\n \"data-[state=closed]:slide-out-to-bottom-full\",\n \"sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95\",\n \"sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95\",\n ),\n \"sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg sm:p-6\",\n \"duration-200\",\n SIZE_CLASSES[size],\n className,\n )}\n {...props}\n >\n {showMobileHandle && mobilePresentation === \"sheet\" && (\n <div\n aria-hidden=\"true\"\n className=\"mb-3 h-1 w-8 shrink-0 self-center rounded-full bg-icons-tertiary sm:hidden\"\n />\n )}\n {children}\n </DialogPrimitive.Content>\n </>\n );\n\n return portal ? <DialogPrimitive.Portal>{content}</DialogPrimitive.Portal> : content;\n },\n);\nDialogContent.displayName = \"DialogContent\";\n\nexport interface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Show the close (X) button in the header. @default true */\n showClose?: boolean;\n /** Show a back arrow button on the left side. Defaults to `true` when `onBack` is provided. */\n showBack?: boolean;\n /** Called when the back button is clicked. */\n onBack?: () => void;\n /** Accessible label for the back button. @default \"Go back\" */\n backLabel?: string;\n /** Accessible label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * Header bar for the dialog. Renders the title with an optional back arrow\n * and close button.\n *\n * @example\n * ```tsx\n * <DialogHeader>\n * <DialogTitle>Settings</DialogTitle>\n * </DialogHeader>\n *\n * <DialogHeader showBack onBack={() => setStep(0)}>\n * <DialogTitle>Step 2</DialogTitle>\n * </DialogHeader>\n * ```\n */\nexport const DialogHeader = React.forwardRef<HTMLDivElement, DialogHeaderProps>(\n (\n {\n className,\n children,\n showClose = true,\n showBack,\n onBack,\n backLabel = \"Go back\",\n closeLabel = \"Close\",\n ...props\n },\n ref,\n ) => {\n const shouldShowBack = showBack ?? !!onBack;\n\n return (\n <div\n ref={ref}\n className={cn(\"flex shrink-0 items-center justify-end gap-4\", className)}\n {...props}\n >\n {shouldShowBack && (\n <IconButton\n variant=\"secondary\"\n size=\"32\"\n icon={<ArrowLeftIcon size={16} />}\n onClick={onBack}\n disabled={!onBack}\n aria-label={backLabel}\n />\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">{children}</div>\n {showClose && (\n <DialogPrimitive.Close asChild>\n <IconButton\n variant=\"secondary\"\n size=\"32\"\n icon={<CloseIcon size={16} />}\n aria-label={closeLabel}\n />\n </DialogPrimitive.Close>\n )}\n </div>\n );\n },\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** Props for the {@link DialogTitle} component. */\nexport type DialogTitleProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;\n\n/**\n * Accessible title for the dialog. Must be rendered inside {@link DialogHeader}\n * or directly within {@link DialogContent}.\n */\nexport const DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n DialogTitleProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\"typography-header-heading-xs text-content-primary\", className)}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** Props for the {@link DialogDescription} component. */\nexport type DialogDescriptionProps = React.ComponentPropsWithoutRef<\n typeof DialogPrimitive.Description\n>;\n\n/** Accessible description for the dialog. Rendered as secondary text. */\nexport const DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n DialogDescriptionProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"typography-body-default-16px-regular text-content-secondary\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Scrollable content area (slot) between the header and footer.\n * Grows to fill available space and scrolls when content overflows.\n */\nexport const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex-1 overflow-y-auto py-4\", className)} {...props} />\n ),\n);\nDialogBody.displayName = \"DialogBody\";\n\nexport interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Footer bar for the dialog. Typically contains action buttons.\n * Children are laid out in a horizontal row with equal flex-basis.\n */\nexport const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex shrink-0 items-center gap-2\", \"[&>*]:min-w-0 [&>*]:flex-1\", className)}\n {...props}\n />\n ),\n);\nDialogFooter.displayName = \"DialogFooter\";\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Dialog.mjs","sources":["../../../src/components/Dialog/Dialog.tsx"],"sourcesContent":["import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useSuppressClickAfterDrag } from \"../../utils/useSuppressClickAfterDrag\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { ArrowLeftIcon } from \"../Icons/ArrowLeftIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Props for the {@link Dialog} root component. */\nexport interface DialogProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {\n /** Controlled open state. When provided, you must also supply `onOpenChange`. */\n open?: boolean;\n /** Called when the open state changes. Required when `open` is controlled. */\n onOpenChange?: (open: boolean) => void;\n /** The open state of the dialog when it is initially rendered (uncontrolled). */\n defaultOpen?: boolean;\n}\n\n/** Root component that manages open/close state for a dialog. */\nexport const Dialog = DialogPrimitive.Root;\n\n/** Props for the {@link DialogTrigger} component. */\nexport type DialogTriggerProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>;\n\n/**\n * The element that opens the dialog when clicked.\n *\n * On touch / pen, a press-and-release that crosses a small movement threshold\n * is treated as a drag and the resulting synthetic click is suppressed —\n * defends against Android Chrome opening the dialog on a scroll-drag-end.\n */\nexport const DialogTrigger = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Trigger>,\n DialogTriggerProps\n>((props, ref) => <DialogPrimitive.Trigger ref={ref} {...useSuppressClickAfterDrag(props)} />);\nDialogTrigger.displayName = \"DialogTrigger\";\n\n/** Convenience alias for Radix `Dialog.Close`. Closes the dialog when clicked. */\nexport const DialogClose = DialogPrimitive.Close;\n\n/** Props for the {@link DialogClose} component. */\nexport type DialogCloseProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>;\n\nexport interface DialogOverlayProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> {}\n\n/**\n * Semi-transparent backdrop rendered behind the dialog content.\n * Rendered by {@link DialogContent}; portaled to `document.body` when {@link DialogContent} `portal` is true.\n */\nexport const DialogOverlay = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Overlay>,\n DialogOverlayProps\n>(({ className, style, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 fixed inset-0 bg-background-overlay-default data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className,\n )}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\nexport interface DialogContentProps\n extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n /**\n * Width preset for the dialog.\n * - `\"sm\"` — 400px max-width (confirmations, simple forms)\n * - `\"md\"` — 440px max-width (default, standard dialogs)\n * - `\"lg\"` — 600px max-width (complex content, tables)\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** When true, renders overlay automatically. @default true */\n overlay?: boolean;\n /**\n * When true, teleports overlay and panel to `document.body`.\n * When false, renders inline in the React tree (useful inside theme providers or scoped containers).\n * @default true\n */\n portal?: boolean;\n /** Show the v2 mobile sheet pull handle. Only rendered when `mobilePresentation` is `\"sheet\"`. @default true */\n showMobileHandle?: boolean;\n /**\n * How the dialog presents below the `sm` breakpoint.\n * - `\"sheet\"` — bottom sheet pinned to the viewport bottom edge (default)\n * - `\"card\"` — centered floating card per the v2-modal confirmation spec:\n * 16px side margins, 24px padding, 32px radius on all corners, no pull handle\n *\n * @default \"sheet\"\n */\n mobilePresentation?: \"sheet\" | \"card\";\n /** Props forwarded to the default {@link DialogOverlay} when `overlay` is `true`. */\n overlayProps?: DialogOverlayProps;\n}\n\nconst SIZE_CLASSES: Record<NonNullable<DialogContentProps[\"size\"]>, string> = {\n sm: \"sm:max-w-[400px]\",\n md: \"sm:max-w-[440px]\",\n lg: \"sm:max-w-[600px]\",\n};\n\n/**\n * The dialog panel. Includes the overlay by default and portals to `document.body` by default.\n *\n * Set `portal={false}` to keep overlay and content in the DOM subtree of the parent `Dialog`.\n * `fixed` positioning still applies; ancestors with `transform` or `overflow` may affect layout.\n *\n * On mobile viewports (<640px), the dialog slides up from the bottom as a sheet\n * with top-only border radius by default; pass `mobilePresentation=\"card\"` to\n * render a centered floating card instead (used for small confirmation dialogs).\n * On larger viewports it renders centered with full border radius.\n *\n * @example\n * ```tsx\n * <Dialog>\n * <DialogTrigger asChild>\n * <Button>Open</Button>\n * </DialogTrigger>\n * <DialogContent>\n * <DialogHeader>\n * <DialogTitle>Title</DialogTitle>\n * </DialogHeader>\n * <DialogBody>Content here</DialogBody>\n * <DialogFooter>\n * <DialogClose asChild>\n * <Button variant=\"secondary\">Cancel</Button>\n * </DialogClose>\n * <Button>Accept</Button>\n * </DialogFooter>\n * </DialogContent>\n * </Dialog>\n * ```\n */\nexport const DialogContent = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(\n (\n {\n className,\n children,\n size = \"md\",\n overlay = true,\n portal = true,\n showMobileHandle = true,\n mobilePresentation = \"sheet\",\n overlayProps,\n style,\n onOpenAutoFocus,\n ...props\n },\n ref,\n ) => {\n const content = (\n <>\n {overlay && <DialogOverlay {...overlayProps} />}\n <DialogPrimitive.Content\n ref={ref}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n onOpenAutoFocus={(e) => {\n if (onOpenAutoFocus) {\n onOpenAutoFocus(e);\n return;\n }\n e.preventDefault();\n (e.currentTarget as HTMLElement).focus();\n }}\n className={cn(\n \"fixed flex flex-col overflow-hidden border border-modal-stroke bg-modal-background shadow-blur-menu backdrop-blur-[4px] focus:outline-none\",\n \"data-[state=open]:fade-in-0 data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:animate-out\",\n mobilePresentation === \"card\"\n ? // Floating confirmation card (v2-modal): 16px side margins, vertically centered, 32px radius\n cn(\n \"inset-x-4 top-1/2 max-h-[85vh] -translate-y-1/2 rounded-xl p-6\",\n \"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95\",\n \"sm:inset-x-auto\",\n )\n : // Bottom sheet pinned to the viewport bottom edge\n cn(\n \"inset-x-0 bottom-0 max-h-[85vh] w-full rounded-t-xl p-4 pt-3\",\n \"data-[state=open]:slide-in-from-bottom-full\",\n \"data-[state=closed]:slide-out-to-bottom-full\",\n \"sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95\",\n \"sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95\",\n ),\n \"sm:inset-auto sm:top-1/2 sm:left-1/2 sm:max-h-[85vh] sm:w-full sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-lg sm:p-6\",\n \"duration-200\",\n SIZE_CLASSES[size],\n className,\n )}\n {...props}\n >\n {showMobileHandle && mobilePresentation === \"sheet\" && (\n <div\n aria-hidden=\"true\"\n className=\"mb-3 h-1 w-8 shrink-0 self-center rounded-full bg-icons-tertiary sm:hidden\"\n />\n )}\n {children}\n </DialogPrimitive.Content>\n </>\n );\n\n return portal ? <DialogPrimitive.Portal>{content}</DialogPrimitive.Portal> : content;\n },\n);\nDialogContent.displayName = \"DialogContent\";\n\nexport interface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Show the close (X) button in the header. @default true */\n showClose?: boolean;\n /** Show a back arrow button on the left side. Defaults to `true` when `onBack` is provided. */\n showBack?: boolean;\n /** Called when the back button is clicked. */\n onBack?: () => void;\n /** Accessible label for the back button. @default \"Go back\" */\n backLabel?: string;\n /** Accessible label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * Header bar for the dialog. Renders the title with an optional back arrow\n * and close button.\n *\n * @example\n * ```tsx\n * <DialogHeader>\n * <DialogTitle>Settings</DialogTitle>\n * </DialogHeader>\n *\n * <DialogHeader showBack onBack={() => setStep(0)}>\n * <DialogTitle>Step 2</DialogTitle>\n * </DialogHeader>\n * ```\n */\nexport const DialogHeader = React.forwardRef<HTMLDivElement, DialogHeaderProps>(\n (\n {\n className,\n children,\n showClose = true,\n showBack,\n onBack,\n backLabel = \"Go back\",\n closeLabel = \"Close\",\n ...props\n },\n ref,\n ) => {\n const shouldShowBack = showBack ?? !!onBack;\n\n return (\n <div\n ref={ref}\n className={cn(\"flex shrink-0 items-center justify-end gap-4\", className)}\n {...props}\n >\n {shouldShowBack && (\n <IconButton\n variant=\"secondary\"\n size=\"32\"\n icon={<ArrowLeftIcon size={16} />}\n onClick={onBack}\n disabled={!onBack}\n aria-label={backLabel}\n />\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">{children}</div>\n {showClose && (\n <DialogPrimitive.Close asChild>\n <IconButton\n variant=\"secondary\"\n size=\"32\"\n icon={<CloseIcon size={16} />}\n aria-label={closeLabel}\n />\n </DialogPrimitive.Close>\n )}\n </div>\n );\n },\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** Props for the {@link DialogTitle} component. */\nexport type DialogTitleProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;\n\n/**\n * Accessible title for the dialog. Must be rendered inside {@link DialogHeader}\n * or directly within {@link DialogContent}.\n */\nexport const DialogTitle = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Title>,\n DialogTitleProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\"typography-header-heading-xs text-content-primary\", className)}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** Props for the {@link DialogDescription} component. */\nexport type DialogDescriptionProps = React.ComponentPropsWithoutRef<\n typeof DialogPrimitive.Description\n>;\n\n/** Accessible description for the dialog. Rendered as secondary text. */\nexport const DialogDescription = React.forwardRef<\n React.ComponentRef<typeof DialogPrimitive.Description>,\n DialogDescriptionProps\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"typography-body-default-16px-regular text-content-secondary\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport interface DialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Scrollable content area (slot) between the header and footer.\n * Grows to fill available space and scrolls when content overflows.\n */\nexport const DialogBody = React.forwardRef<HTMLDivElement, DialogBodyProps>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex-1 overflow-y-auto py-4\", className)} {...props} />\n ),\n);\nDialogBody.displayName = \"DialogBody\";\n\nexport interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}\n\n/**\n * Footer bar for the dialog. Typically contains action buttons.\n * Children are laid out in a horizontal row with equal flex-basis.\n */\nexport const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex shrink-0 items-center gap-2\", \"[&>*]:min-w-0 [&>*]:flex-1\", className)}\n {...props}\n />\n ),\n);\nDialogFooter.displayName = \"DialogFooter\";\n"],"names":[],"mappings":"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"}
@@ -149,12 +149,18 @@ const ITEM_SELECTED_TYPOGRAPHY = {
149
149
  "40": "typography-body-default-16px-semibold",
150
150
  "32": "typography-body-small-14px-semibold"
151
151
  };
152
+ const ITEM_COUNT_TYPOGRAPHY = {
153
+ "40": "typography-body-default-16px-regular",
154
+ "32": "typography-body-small-14px-regular"
155
+ };
152
156
  const DropdownMenuItem = React.forwardRef(
153
157
  ({
154
158
  size = "40",
155
159
  destructive,
156
160
  leadingIcon,
161
+ avatar,
157
162
  trailingIcon,
163
+ count,
158
164
  description,
159
165
  selected,
160
166
  className,
@@ -164,10 +170,14 @@ const DropdownMenuItem = React.forwardRef(
164
170
  }, ref) => {
165
171
  const normalizedSize = SIZE_NORMALIZED[size];
166
172
  const hasDescription = description != null;
173
+ const hasAvatar = avatar != null;
167
174
  const itemClassName = cn(
168
- "flex w-full cursor-pointer gap-2 rounded-xs px-3 outline-none",
175
+ "group flex w-full cursor-pointer gap-2 rounded-xs px-3 outline-none",
169
176
  hasDescription ? "items-start" : "items-center",
170
177
  ITEM_SIZE_CLASSES[normalizedSize],
178
+ // A 24px avatar would push the compact 32px row past its height with the
179
+ // default padding; tighten it so the avatar variant keeps the 32px contract.
180
+ hasAvatar && !hasDescription && normalizedSize === "32" && "py-1",
171
181
  "data-[highlighted]:bg-neutral-alphas-50",
172
182
  "data-[disabled]:cursor-not-allowed data-[disabled]:text-content-disabled",
173
183
  destructive && "text-error-content",
@@ -182,8 +192,20 @@ const DropdownMenuItem = React.forwardRef(
182
192
  return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Item, { ref, asChild: true, className: itemClassName, ...props, children });
183
193
  }
184
194
  const iconAlignClassName = hasDescription ? "flex shrink-0 items-center pt-1" : null;
195
+ const countNode = count != null && /* @__PURE__ */ jsx(
196
+ "span",
197
+ {
198
+ className: cn(
199
+ "shrink-0 tabular-nums",
200
+ ITEM_COUNT_TYPOGRAPHY[normalizedSize],
201
+ destructive ? "text-error-content" : selected ? "text-content-primary-inverted" : "text-content-tertiary",
202
+ "group-data-[disabled]:text-content-disabled"
203
+ ),
204
+ children: count
205
+ }
206
+ );
185
207
  return /* @__PURE__ */ jsxs(DropdownMenuPrimitive.Item, { ref, className: itemClassName, ...props, children: [
186
- leadingIcon != null && (hasDescription ? /* @__PURE__ */ jsx("span", { className: iconAlignClassName, children: leadingIcon }) : leadingIcon),
208
+ avatar != null ? /* @__PURE__ */ jsx("span", { className: "shrink-0", children: avatar }) : leadingIcon != null && (hasDescription ? /* @__PURE__ */ jsx("span", { className: iconAlignClassName, children: leadingIcon }) : leadingIcon),
187
209
  hasDescription ? /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
188
210
  /* @__PURE__ */ jsx("span", { className: "truncate", children }),
189
211
  /* @__PURE__ */ jsx(
@@ -197,6 +219,7 @@ const DropdownMenuItem = React.forwardRef(
197
219
  }
198
220
  )
199
221
  ] }) : /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children }),
222
+ countNode,
200
223
  trailingIcon != null && (hasDescription ? /* @__PURE__ */ jsx("span", { className: iconAlignClassName, children: trailingIcon }) : trailingIcon)
201
224
  ] });
202
225
  }