@fanvue/ui 3.15.0 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +16 -89
  2. package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  3. package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs +54 -0
  4. package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs.map +1 -0
  5. package/dist/cjs/components/AudioUpload/AudioUpload.cjs +5 -5
  6. package/dist/cjs/components/AudioUpload/AudioUpload.cjs.map +1 -1
  7. package/dist/cjs/components/ChatInput/ChatInput.cjs +6 -11
  8. package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -1
  9. package/dist/cjs/components/ChatMessage/ChatMessage.cjs +228 -0
  10. package/dist/cjs/components/ChatMessage/ChatMessage.cjs.map +1 -0
  11. package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +160 -71
  12. package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  13. package/dist/cjs/components/Icons/DenseGridViewIcon.cjs +52 -0
  14. package/dist/cjs/components/Icons/DenseGridViewIcon.cjs.map +1 -0
  15. package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs +66 -0
  16. package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs.map +1 -0
  17. package/dist/cjs/components/Select/Select.cjs +61 -17
  18. package/dist/cjs/components/Select/Select.cjs.map +1 -1
  19. package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs +53 -0
  20. package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs.map +1 -0
  21. package/dist/cjs/components/VoiceNote/VoiceNote.cjs +305 -0
  22. package/dist/cjs/components/VoiceNote/VoiceNote.cjs.map +1 -0
  23. package/dist/cjs/index.cjs +12 -0
  24. package/dist/cjs/index.cjs.map +1 -1
  25. package/dist/cjs/utils/audioWaveform.cjs +76 -0
  26. package/dist/cjs/utils/audioWaveform.cjs.map +1 -0
  27. package/dist/cjs/utils/useAudioPlayback.cjs +123 -0
  28. package/dist/cjs/utils/useAudioPlayback.cjs.map +1 -0
  29. package/dist/cjs/utils/useFittedBarCount.cjs +42 -0
  30. package/dist/cjs/utils/useFittedBarCount.cjs.map +1 -0
  31. package/dist/cjs/utils/useWaveformPeaks.cjs +48 -0
  32. package/dist/cjs/utils/useWaveformPeaks.cjs.map +1 -0
  33. package/dist/cjs/utils/useWaveformSeek.cjs +100 -0
  34. package/dist/cjs/utils/useWaveformSeek.cjs.map +1 -0
  35. package/dist/components/AudioPlayer/AudioPlayer.mjs +7 -80
  36. package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -1
  37. package/dist/components/AudioRecordButton/AudioRecordButton.mjs +37 -0
  38. package/dist/components/AudioRecordButton/AudioRecordButton.mjs.map +1 -0
  39. package/dist/components/AudioUpload/AudioUpload.mjs +5 -5
  40. package/dist/components/AudioUpload/AudioUpload.mjs.map +1 -1
  41. package/dist/components/ChatInput/ChatInput.mjs +6 -11
  42. package/dist/components/ChatInput/ChatInput.mjs.map +1 -1
  43. package/dist/components/ChatMessage/ChatMessage.mjs +211 -0
  44. package/dist/components/ChatMessage/ChatMessage.mjs.map +1 -0
  45. package/dist/components/DropdownMenu/DropdownMenu.mjs +161 -72
  46. package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
  47. package/dist/components/Icons/DenseGridViewIcon.mjs +35 -0
  48. package/dist/components/Icons/DenseGridViewIcon.mjs.map +1 -0
  49. package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs +49 -0
  50. package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs.map +1 -0
  51. package/dist/components/Select/Select.mjs +61 -17
  52. package/dist/components/Select/Select.mjs.map +1 -1
  53. package/dist/components/SubscribeButton/SubscribeButton.mjs +36 -0
  54. package/dist/components/SubscribeButton/SubscribeButton.mjs.map +1 -0
  55. package/dist/components/VoiceNote/VoiceNote.mjs +288 -0
  56. package/dist/components/VoiceNote/VoiceNote.mjs.map +1 -0
  57. package/dist/index.d.ts +321 -4
  58. package/dist/index.mjs +12 -0
  59. package/dist/index.mjs.map +1 -1
  60. package/dist/styles/base.css +11 -2
  61. package/dist/styles/theme.css +1 -1
  62. package/dist/utils/audioWaveform.mjs +76 -0
  63. package/dist/utils/audioWaveform.mjs.map +1 -0
  64. package/dist/utils/useAudioPlayback.mjs +106 -0
  65. package/dist/utils/useAudioPlayback.mjs.map +1 -0
  66. package/dist/utils/useFittedBarCount.mjs +25 -0
  67. package/dist/utils/useFittedBarCount.mjs.map +1 -0
  68. package/dist/utils/useWaveformPeaks.mjs +31 -0
  69. package/dist/utils/useWaveformPeaks.mjs.map +1 -0
  70. package/dist/utils/useWaveformSeek.mjs +83 -0
  71. package/dist/utils/useWaveformSeek.mjs.map +1 -0
  72. package/package.json +1 -1
@@ -0,0 +1,211 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../../utils/cn.mjs";
5
+ import { Avatar } from "../Avatar/Avatar.mjs";
6
+ import { DoubleTickIcon } from "../Icons/DoubleTickIcon.mjs";
7
+ import { TrashIcon } from "../Icons/TrashIcon.mjs";
8
+ const DEFAULT_WAVEFORM = Array.from({ length: 64 }, () => 0);
9
+ const WAVEFORM_MIN_HEIGHT = 4;
10
+ const WAVEFORM_MAX_HEIGHT = 24;
11
+ const bubbleColors = {
12
+ sender: "bg-messages-background-sender border-messages-background-sender-stroke",
13
+ receiver: "bg-messages-background-receiver border-messages-background-receiver-2"
14
+ };
15
+ function ChatMessageMeta({
16
+ time,
17
+ showTick,
18
+ status,
19
+ className
20
+ }) {
21
+ return /* @__PURE__ */ jsxs(
22
+ "span",
23
+ {
24
+ className: cn(
25
+ "typography-description-12px-regular inline-flex items-center gap-1 whitespace-nowrap text-content-secondary leading-none",
26
+ className
27
+ ),
28
+ children: [
29
+ time ? /* @__PURE__ */ jsx("time", { children: time }) : null,
30
+ showTick ? (
31
+ // The 24px geometry (stroked) scaled to 16px — the icon's 16px variant
32
+ // is filled and renders as a blob at this size.
33
+ /* @__PURE__ */ jsx(
34
+ DoubleTickIcon,
35
+ {
36
+ size: 24,
37
+ "aria-hidden": false,
38
+ role: "img",
39
+ "aria-label": status === "read" ? "Read" : "Delivered",
40
+ className: cn(
41
+ "size-4 shrink-0",
42
+ status === "read" ? "text-messages-read" : "text-icons-disabled"
43
+ )
44
+ }
45
+ )
46
+ ) : null
47
+ ]
48
+ }
49
+ );
50
+ }
51
+ function TypingIndicator({ label }) {
52
+ const dot = "size-2 shrink-0 rounded-full motion-safe:animate-bounce";
53
+ return (
54
+ // biome-ignore lint/a11y/useSemanticElements: <output> is not appropriate for a typing indicator; role="status" is the correct live-region pattern
55
+ /* @__PURE__ */ jsxs("span", { role: "status", "aria-label": label, className: "flex items-center gap-2 px-0.5 py-1", children: [
56
+ /* @__PURE__ */ jsx("span", { className: cn(dot, "bg-content-primary") }),
57
+ /* @__PURE__ */ jsx("span", { className: cn(dot, "bg-neutral-alphas-700 [animation-delay:150ms]") }),
58
+ /* @__PURE__ */ jsx("span", { className: cn(dot, "bg-neutral-alphas-400 [animation-delay:300ms]") })
59
+ ] })
60
+ );
61
+ }
62
+ function Waveform({ bars }) {
63
+ return /* @__PURE__ */ jsx("span", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-hidden", "aria-hidden": true, children: bars.map((amplitude, index) => {
64
+ const clamped = Math.min(1, Math.max(0, amplitude));
65
+ const height = WAVEFORM_MIN_HEIGHT + clamped * (WAVEFORM_MAX_HEIGHT - WAVEFORM_MIN_HEIGHT);
66
+ return /* @__PURE__ */ jsx(
67
+ "span",
68
+ {
69
+ className: "w-1 shrink-0 rounded-3xs bg-messages-waveform-default",
70
+ style: { height }
71
+ },
72
+ index
73
+ );
74
+ }) });
75
+ }
76
+ function PlayGlyph() {
77
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", className: "size-6", children: /* @__PURE__ */ jsx("path", { d: "M8 5.14v13.72a1 1 0 0 0 1.54.84l10.28-6.86a1 1 0 0 0 0-1.68L9.54 4.3A1 1 0 0 0 8 5.14Z" }) });
78
+ }
79
+ function PauseGlyph() {
80
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", className: "size-6", children: [
81
+ /* @__PURE__ */ jsx("rect", { x: "7", y: "5", width: "3.5", height: "14", rx: "1" }),
82
+ /* @__PURE__ */ jsx("rect", { x: "13.5", y: "5", width: "3.5", height: "14", rx: "1" })
83
+ ] });
84
+ }
85
+ const ChatMessage = React.forwardRef(
86
+ ({
87
+ className,
88
+ user = "receiver",
89
+ variant = "text",
90
+ message,
91
+ time,
92
+ status = "delivered",
93
+ audioDuration = "0:00",
94
+ waveform,
95
+ playing,
96
+ defaultPlaying = false,
97
+ onPlayingChange,
98
+ showAvatar = true,
99
+ avatarSrc,
100
+ avatarAlt,
101
+ avatarFallback,
102
+ online = false,
103
+ typingLabel = "Typing",
104
+ deletedLabel = "Message deleted",
105
+ playLabel = "Play",
106
+ pauseLabel = "Pause",
107
+ ...props
108
+ }, ref) => {
109
+ const isSender = user === "sender";
110
+ const showTick = isSender;
111
+ const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);
112
+ const isPlaying = playing ?? internalPlaying;
113
+ const togglePlaying = () => {
114
+ const next = !isPlaying;
115
+ if (playing === void 0) setInternalPlaying(next);
116
+ onPlayingChange?.(next);
117
+ };
118
+ const bubbleBase = cn(
119
+ "w-fit min-w-0 max-w-full rounded-md border border-solid",
120
+ bubbleColors[user]
121
+ );
122
+ const hasMeta = Boolean(time) || showTick;
123
+ let bubble;
124
+ if (variant === "typing") {
125
+ bubble = /* @__PURE__ */ jsx("div", { className: cn(bubbleBase, "px-3 py-2"), children: /* @__PURE__ */ jsx(TypingIndicator, { label: typingLabel }) });
126
+ } else if (variant === "deleted") {
127
+ bubble = /* @__PURE__ */ jsxs("div", { className: cn(bubbleBase, "flex items-baseline gap-6 px-3 py-2 opacity-60"), children: [
128
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
129
+ /* @__PURE__ */ jsx(TrashIcon, { size: 16, className: "shrink-0 text-content-primary" }),
130
+ /* @__PURE__ */ jsx("span", { className: "typography-body-default-16px-regular whitespace-nowrap text-content-primary", children: deletedLabel })
131
+ ] }),
132
+ hasMeta ? /* @__PURE__ */ jsx(ChatMessageMeta, { time, showTick, status }) : null
133
+ ] });
134
+ } else if (variant === "audio") {
135
+ bubble = /* @__PURE__ */ jsx("div", { className: cn(bubbleBase, "flex w-full flex-col p-2"), children: /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col items-start gap-2 px-1", children: [
136
+ /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-3 pt-1", children: [
137
+ /* @__PURE__ */ jsx(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ onClick: togglePlaying,
142
+ "aria-pressed": isPlaying,
143
+ "aria-label": isPlaying ? pauseLabel : playLabel,
144
+ className: cn(
145
+ "flex size-12 shrink-0 items-center justify-center rounded-full bg-buttons-secondary-default text-icons-primary",
146
+ "focus-visible:shadow-focus-ring focus-visible:outline-none motion-safe:transition-colors motion-safe:duration-150"
147
+ ),
148
+ children: isPlaying ? /* @__PURE__ */ jsx(PauseGlyph, {}) : /* @__PURE__ */ jsx(PlayGlyph, {})
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsx(Waveform, { bars: waveform ?? DEFAULT_WAVEFORM }),
152
+ /* @__PURE__ */ jsx("span", { className: "typography-body-small-14px-regular whitespace-nowrap text-content-primary", children: audioDuration })
153
+ ] }),
154
+ hasMeta ? /* @__PURE__ */ jsx(
155
+ ChatMessageMeta,
156
+ {
157
+ time,
158
+ showTick,
159
+ status,
160
+ className: "self-end"
161
+ }
162
+ ) : null
163
+ ] }) });
164
+ } else {
165
+ bubble = /* @__PURE__ */ jsx("div", { className: cn(bubbleBase, "px-3 py-2"), children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline justify-end gap-x-6 gap-y-2", children: [
166
+ /* @__PURE__ */ jsx("p", { className: "typography-body-default-16px-regular m-0 min-w-0 break-words text-content-primary", children: message }),
167
+ hasMeta ? /* @__PURE__ */ jsx(
168
+ ChatMessageMeta,
169
+ {
170
+ time,
171
+ showTick,
172
+ status,
173
+ className: "shrink-0"
174
+ }
175
+ ) : null
176
+ ] }) });
177
+ }
178
+ const alignItems = variant === "typing" || variant === "deleted" ? "items-center" : "items-end";
179
+ return /* @__PURE__ */ jsxs(
180
+ "div",
181
+ {
182
+ ref,
183
+ className: cn(
184
+ "flex min-h-10 w-full gap-2",
185
+ alignItems,
186
+ isSender ? "justify-end" : "justify-start",
187
+ className
188
+ ),
189
+ ...props,
190
+ children: [
191
+ !isSender && (showAvatar ? /* @__PURE__ */ jsxs("div", { className: "relative flex shrink-0", children: [
192
+ /* @__PURE__ */ jsx(Avatar, { size: 40, src: avatarSrc, alt: avatarAlt, fallback: avatarFallback }),
193
+ online ? /* @__PURE__ */ jsx(
194
+ "span",
195
+ {
196
+ "aria-hidden": "true",
197
+ className: "absolute right-0 bottom-0 size-2.5 rounded-full border border-border-background bg-messages-status-active"
198
+ }
199
+ ) : null
200
+ ] }) : /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "w-10 shrink-0" })),
201
+ bubble
202
+ ]
203
+ }
204
+ );
205
+ }
206
+ );
207
+ ChatMessage.displayName = "ChatMessage";
208
+ export {
209
+ ChatMessage
210
+ };
211
+ //# sourceMappingURL=ChatMessage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatMessage.mjs","sources":["../../../src/components/ChatMessage/ChatMessage.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Avatar } from \"../Avatar/Avatar\";\nimport { DoubleTickIcon } from \"../Icons/DoubleTickIcon\";\nimport { TrashIcon } from \"../Icons/TrashIcon\";\n\nexport type ChatMessageUser = \"sender\" | \"receiver\";\n\nexport type ChatMessageVariant = \"text\" | \"typing\" | \"audio\" | \"deleted\";\n\nexport type ChatMessageStatus = \"delivered\" | \"read\";\n\nexport interface ChatMessageProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Who sent the message. `\"sender\"` is the current user (right-aligned, green\n * bubble, delivery tick). `\"receiver\"` is the other party (left-aligned, grey\n * bubble, with avatar). @default \"receiver\"\n */\n user?: ChatMessageUser;\n /** The kind of content the message carries. @default \"text\" */\n variant?: ChatMessageVariant;\n /** Message body for the `\"text\"` variant. Keep it to inline content. */\n message?: React.ReactNode;\n /** Timestamp shown with the message, e.g. `\"16:00\"`. */\n time?: string;\n /**\n * Delivery status shown on sender messages (a double tick). Ignored for\n * receiver messages. `\"read\"` renders the tick in the read colour. @default \"delivered\"\n */\n status?: ChatMessageStatus;\n /** Duration label for the `\"audio\"` variant, e.g. `\"0:05\"`. @default \"0:00\" */\n audioDuration?: string;\n /**\n * Relative bar heights (values `0`–`1`) for the `\"audio\"` waveform. Defaults\n * to a flat row of dots matching the unplayed design state.\n */\n waveform?: number[];\n /** Whether the audio is playing (controlled). Pairs with {@link onPlayingChange}. */\n playing?: boolean;\n /** Initial playing state when uncontrolled. @default false */\n defaultPlaying?: boolean;\n /** Fired when the audio play/pause button is pressed, with the next playing state. */\n onPlayingChange?: (playing: boolean) => void;\n /** Render the receiver avatar. Reserves the avatar space when `false` so grouped bubbles stay aligned. @default true */\n showAvatar?: boolean;\n /** Avatar image URL for receiver messages. */\n avatarSrc?: string;\n /** Avatar alt text. @default \"Avatar\" */\n avatarAlt?: string;\n /** Avatar fallback (initials or icon) shown before the image loads. */\n avatarFallback?: React.ReactNode;\n /** Show the online indicator on the receiver avatar. @default false */\n online?: boolean;\n /** Accessible label for the typing indicator. @default \"Typing\" */\n typingLabel?: string;\n /** Text shown for the `\"deleted\"` variant. @default \"Message deleted\" */\n deletedLabel?: string;\n /** Accessible label for the audio play button. @default \"Play\" */\n playLabel?: string;\n /** Accessible label for the audio pause button. @default \"Pause\" */\n pauseLabel?: string;\n}\n\nconst DEFAULT_WAVEFORM = Array.from({ length: 64 }, () => 0);\nconst WAVEFORM_MIN_HEIGHT = 4;\nconst WAVEFORM_MAX_HEIGHT = 24;\n\nconst bubbleColors: Record<ChatMessageUser, string> = {\n sender: \"bg-messages-background-sender border-messages-background-sender-stroke\",\n receiver: \"bg-messages-background-receiver border-messages-background-receiver-2\",\n};\n\nfunction ChatMessageMeta({\n time,\n showTick,\n status,\n className,\n}: {\n time?: string;\n showTick: boolean;\n status: ChatMessageStatus;\n className?: string;\n}) {\n return (\n <span\n className={cn(\n \"typography-description-12px-regular inline-flex items-center gap-1 whitespace-nowrap text-content-secondary leading-none\",\n className,\n )}\n >\n {time ? <time>{time}</time> : null}\n {showTick ? (\n // The 24px geometry (stroked) scaled to 16px — the icon's 16px variant\n // is filled and renders as a blob at this size.\n <DoubleTickIcon\n size={24}\n aria-hidden={false}\n role=\"img\"\n aria-label={status === \"read\" ? \"Read\" : \"Delivered\"}\n className={cn(\n \"size-4 shrink-0\",\n status === \"read\" ? \"text-messages-read\" : \"text-icons-disabled\",\n )}\n />\n ) : null}\n </span>\n );\n}\n\nfunction TypingIndicator({ label }: { label: string }) {\n const dot = \"size-2 shrink-0 rounded-full motion-safe:animate-bounce\";\n return (\n // biome-ignore lint/a11y/useSemanticElements: <output> is not appropriate for a typing indicator; role=\"status\" is the correct live-region pattern\n <span role=\"status\" aria-label={label} className=\"flex items-center gap-2 px-0.5 py-1\">\n <span className={cn(dot, \"bg-content-primary\")} />\n <span className={cn(dot, \"bg-neutral-alphas-700 [animation-delay:150ms]\")} />\n <span className={cn(dot, \"bg-neutral-alphas-400 [animation-delay:300ms]\")} />\n </span>\n );\n}\n\nfunction Waveform({ bars }: { bars: number[] }) {\n return (\n <span className=\"flex min-w-0 flex-1 items-center gap-1 overflow-hidden\" aria-hidden>\n {bars.map((amplitude, index) => {\n const clamped = Math.min(1, Math.max(0, amplitude));\n const height = WAVEFORM_MIN_HEIGHT + clamped * (WAVEFORM_MAX_HEIGHT - WAVEFORM_MIN_HEIGHT);\n return (\n <span\n // biome-ignore lint/suspicious/noArrayIndexKey: bars are positional and have no stable id\n key={index}\n className=\"w-1 shrink-0 rounded-3xs bg-messages-waveform-default\"\n style={{ height }}\n />\n );\n })}\n </span>\n );\n}\n\nfunction PlayGlyph() {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\" className=\"size-6\">\n <path d=\"M8 5.14v13.72a1 1 0 0 0 1.54.84l10.28-6.86a1 1 0 0 0 0-1.68L9.54 4.3A1 1 0 0 0 8 5.14Z\" />\n </svg>\n );\n}\n\nfunction PauseGlyph() {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\" className=\"size-6\">\n <rect x=\"7\" y=\"5\" width=\"3.5\" height=\"14\" rx=\"1\" />\n <rect x=\"13.5\" y=\"5\" width=\"3.5\" height=\"14\" rx=\"1\" />\n </svg>\n );\n}\n\n/**\n * A single chat message rendered as a bubble. Sender messages sit on the right\n * with a green bubble and a delivery tick; receiver messages sit on the left\n * with a grey bubble and an avatar. Supports plain text, a typing indicator, a\n * voice message with waveform, and a deleted-message placeholder.\n *\n * Text bubbles place the timestamp inline after short messages and drop it to\n * the bottom-right corner once the text wraps, so no variant switch is needed\n * for short versus long content.\n *\n * @example\n * ```tsx\n * <ChatMessage user=\"sender\" message=\"On my way!\" time=\"16:00\" status=\"read\" />\n * <ChatMessage user=\"receiver\" message=\"See you soon\" time=\"16:01\" avatarSrc=\"/jane.jpg\" online />\n * <ChatMessage user=\"receiver\" variant=\"typing\" avatarSrc=\"/jane.jpg\" />\n * ```\n */\nexport const ChatMessage = React.forwardRef<HTMLDivElement, ChatMessageProps>(\n (\n {\n className,\n user = \"receiver\",\n variant = \"text\",\n message,\n time,\n status = \"delivered\",\n audioDuration = \"0:00\",\n waveform,\n playing,\n defaultPlaying = false,\n onPlayingChange,\n showAvatar = true,\n avatarSrc,\n avatarAlt,\n avatarFallback,\n online = false,\n typingLabel = \"Typing\",\n deletedLabel = \"Message deleted\",\n playLabel = \"Play\",\n pauseLabel = \"Pause\",\n ...props\n },\n ref,\n ) => {\n const isSender = user === \"sender\";\n const showTick = isSender;\n\n const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);\n const isPlaying = playing ?? internalPlaying;\n const togglePlaying = () => {\n const next = !isPlaying;\n if (playing === undefined) setInternalPlaying(next);\n onPlayingChange?.(next);\n };\n\n const bubbleBase = cn(\n \"w-fit min-w-0 max-w-full rounded-md border border-solid\",\n bubbleColors[user],\n );\n const hasMeta = Boolean(time) || showTick;\n\n let bubble: React.ReactNode;\n\n if (variant === \"typing\") {\n bubble = (\n <div className={cn(bubbleBase, \"px-3 py-2\")}>\n <TypingIndicator label={typingLabel} />\n </div>\n );\n } else if (variant === \"deleted\") {\n bubble = (\n <div className={cn(bubbleBase, \"flex items-baseline gap-6 px-3 py-2 opacity-60\")}>\n <span className=\"flex items-center gap-2\">\n <TrashIcon size={16} className=\"shrink-0 text-content-primary\" />\n <span className=\"typography-body-default-16px-regular whitespace-nowrap text-content-primary\">\n {deletedLabel}\n </span>\n </span>\n {hasMeta ? <ChatMessageMeta time={time} showTick={showTick} status={status} /> : null}\n </div>\n );\n } else if (variant === \"audio\") {\n bubble = (\n <div className={cn(bubbleBase, \"flex w-full flex-col p-2\")}>\n <div className=\"flex w-full flex-col items-start gap-2 px-1\">\n <div className=\"flex w-full items-center gap-3 pt-1\">\n <button\n type=\"button\"\n onClick={togglePlaying}\n aria-pressed={isPlaying}\n aria-label={isPlaying ? pauseLabel : playLabel}\n className={cn(\n \"flex size-12 shrink-0 items-center justify-center rounded-full bg-buttons-secondary-default text-icons-primary\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none motion-safe:transition-colors motion-safe:duration-150\",\n )}\n >\n {isPlaying ? <PauseGlyph /> : <PlayGlyph />}\n </button>\n <Waveform bars={waveform ?? DEFAULT_WAVEFORM} />\n <span className=\"typography-body-small-14px-regular whitespace-nowrap text-content-primary\">\n {audioDuration}\n </span>\n </div>\n {hasMeta ? (\n <ChatMessageMeta\n time={time}\n showTick={showTick}\n status={status}\n className=\"self-end\"\n />\n ) : null}\n </div>\n </div>\n );\n } else {\n // \"text\": the message and timestamp share a wrapping row. A brief message\n // keeps the time inline (24px gap = the design's Short treatment); once the\n // text wraps, the time drops to its own line bottom-right (the Default\n // treatment). No length prop needed — the layout adapts to the content.\n bubble = (\n <div className={cn(bubbleBase, \"px-3 py-2\")}>\n <div className=\"flex flex-wrap items-baseline justify-end gap-x-6 gap-y-2\">\n <p className=\"typography-body-default-16px-regular m-0 min-w-0 break-words text-content-primary\">\n {message}\n </p>\n {hasMeta ? (\n <ChatMessageMeta\n time={time}\n showTick={showTick}\n status={status}\n className=\"shrink-0\"\n />\n ) : null}\n </div>\n </div>\n );\n }\n\n const alignItems = variant === \"typing\" || variant === \"deleted\" ? \"items-center\" : \"items-end\";\n\n return (\n <div\n ref={ref}\n className={cn(\n \"flex min-h-10 w-full gap-2\",\n alignItems,\n isSender ? \"justify-end\" : \"justify-start\",\n className,\n )}\n {...props}\n >\n {!isSender &&\n (showAvatar ? (\n <div className=\"relative flex shrink-0\">\n <Avatar size={40} src={avatarSrc} alt={avatarAlt} fallback={avatarFallback} />\n {online ? (\n <span\n aria-hidden=\"true\"\n className=\"absolute right-0 bottom-0 size-2.5 rounded-full border border-border-background bg-messages-status-active\"\n />\n ) : null}\n </div>\n ) : (\n <div aria-hidden=\"true\" className=\"w-10 shrink-0\" />\n ))}\n {bubble}\n </div>\n );\n },\n);\n\nChatMessage.displayName = \"ChatMessage\";\n"],"names":[],"mappings":";;;;;;;AA+DA,MAAM,mBAAmB,MAAM,KAAK,EAAE,QAAQ,GAAA,GAAM,MAAM,CAAC;AAC3D,MAAM,sBAAsB;AAC5B,MAAM,sBAAsB;AAE5B,MAAM,eAAgD;AAAA,EACpD,QAAQ;AAAA,EACR,UAAU;AACZ;AAEA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAGD,UAAA;AAAA,QAAA,OAAO,oBAAC,QAAA,EAAM,UAAA,KAAA,CAAK,IAAU;AAAA,QAC7B;AAAA;AAAA;AAAA,UAGC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAM;AAAA,cACN,eAAa;AAAA,cACb,MAAK;AAAA,cACL,cAAY,WAAW,SAAS,SAAS;AAAA,cACzC,WAAW;AAAA,gBACT;AAAA,gBACA,WAAW,SAAS,uBAAuB;AAAA,cAAA;AAAA,YAC7C;AAAA,UAAA;AAAA,YAEA;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGV;AAEA,SAAS,gBAAgB,EAAE,SAA4B;AACrD,QAAM,MAAM;AACZ;AAAA;AAAA,yBAEG,QAAA,EAAK,MAAK,UAAS,cAAY,OAAO,WAAU,uCAC/C,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAK,WAAW,GAAG,KAAK,oBAAoB,GAAG;AAAA,0BAC/C,QAAA,EAAK,WAAW,GAAG,KAAK,+CAA+C,GAAG;AAAA,0BAC1E,QAAA,EAAK,WAAW,GAAG,KAAK,+CAA+C,EAAA,CAAG;AAAA,IAAA,EAAA,CAC7E;AAAA;AAEJ;AAEA,SAAS,SAAS,EAAE,QAA4B;AAC9C,SACE,oBAAC,QAAA,EAAK,WAAU,0DAAyD,eAAW,MACjF,UAAA,KAAK,IAAI,CAAC,WAAW,UAAU;AAC9B,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;AAClD,UAAM,SAAS,sBAAsB,WAAW,sBAAsB;AACtE,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QAGC,WAAU;AAAA,QACV,OAAO,EAAE,OAAA;AAAA,MAAO;AAAA,MAFX;AAAA,IAAA;AAAA,EAKX,CAAC,EAAA,CACH;AAEJ;AAEA,SAAS,YAAY;AACnB,SACE,oBAAC,OAAA,EAAI,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,UACxE,UAAA,oBAAC,QAAA,EAAK,GAAE,0FAAyF,GACnG;AAEJ;AAEA,SAAS,aAAa;AACpB,SACE,qBAAC,SAAI,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,UACxE,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,GAAE,KAAI,GAAE,KAAI,OAAM,OAAM,QAAO,MAAK,IAAG,IAAA,CAAI;AAAA,IACjD,oBAAC,QAAA,EAAK,GAAE,QAAO,GAAE,KAAI,OAAM,OAAM,QAAO,MAAK,IAAG,IAAA,CAAI;AAAA,EAAA,GACtD;AAEJ;AAmBO,MAAM,cAAc,MAAM;AAAA,EAC/B,CACE;AAAA,IACE;AAAA,IACA,OAAO;AAAA,IACP,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,IACjB;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,cAAc;AAAA,IACd,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,WAAW,SAAS;AAC1B,UAAM,WAAW;AAEjB,UAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,cAAc;AAC3E,UAAM,YAAY,WAAW;AAC7B,UAAM,gBAAgB,MAAM;AAC1B,YAAM,OAAO,CAAC;AACd,UAAI,YAAY,OAAW,oBAAmB,IAAI;AAClD,wBAAkB,IAAI;AAAA,IACxB;AAEA,UAAM,aAAa;AAAA,MACjB;AAAA,MACA,aAAa,IAAI;AAAA,IAAA;AAEnB,UAAM,UAAU,QAAQ,IAAI,KAAK;AAEjC,QAAI;AAEJ,QAAI,YAAY,UAAU;AACxB,eACE,oBAAC,OAAA,EAAI,WAAW,GAAG,YAAY,WAAW,GACxC,UAAA,oBAAC,iBAAA,EAAgB,OAAO,YAAA,CAAa,GACvC;AAAA,IAEJ,WAAW,YAAY,WAAW;AAChC,oCACG,OAAA,EAAI,WAAW,GAAG,YAAY,gDAAgD,GAC7E,UAAA;AAAA,QAAA,qBAAC,QAAA,EAAK,WAAU,2BACd,UAAA;AAAA,UAAA,oBAAC,WAAA,EAAU,MAAM,IAAI,WAAU,iCAAgC;AAAA,UAC/D,oBAAC,QAAA,EAAK,WAAU,+EACb,UAAA,aAAA,CACH;AAAA,QAAA,GACF;AAAA,QACC,UAAU,oBAAC,iBAAA,EAAgB,MAAY,UAAoB,QAAgB,IAAK;AAAA,MAAA,GACnF;AAAA,IAEJ,WAAW,YAAY,SAAS;AAC9B,eACE,oBAAC,OAAA,EAAI,WAAW,GAAG,YAAY,0BAA0B,GACvD,UAAA,qBAAC,OAAA,EAAI,WAAU,+CACb,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,uCACb,UAAA;AAAA,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,gBAAc;AAAA,cACd,cAAY,YAAY,aAAa;AAAA,cACrC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cAAA;AAAA,cAGD,UAAA,YAAY,oBAAC,YAAA,CAAA,CAAW,wBAAM,WAAA,CAAA,CAAU;AAAA,YAAA;AAAA,UAAA;AAAA,UAE3C,oBAAC,UAAA,EAAS,MAAM,YAAY,iBAAA,CAAkB;AAAA,UAC9C,oBAAC,QAAA,EAAK,WAAU,6EACb,UAAA,cAAA,CACH;AAAA,QAAA,GACF;AAAA,QACC,UACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,WAAU;AAAA,UAAA;AAAA,QAAA,IAEV;AAAA,MAAA,EAAA,CACN,EAAA,CACF;AAAA,IAEJ,OAAO;AAKL,eACE,oBAAC,OAAA,EAAI,WAAW,GAAG,YAAY,WAAW,GACxC,UAAA,qBAAC,OAAA,EAAI,WAAU,6DACb,UAAA;AAAA,QAAA,oBAAC,KAAA,EAAE,WAAU,qFACV,UAAA,SACH;AAAA,QACC,UACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,WAAU;AAAA,UAAA;AAAA,QAAA,IAEV;AAAA,MAAA,EAAA,CACN,EAAA,CACF;AAAA,IAEJ;AAEA,UAAM,aAAa,YAAY,YAAY,YAAY,YAAY,iBAAiB;AAEpF,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,WAAW,gBAAgB;AAAA,UAC3B;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,CAAC,aACC,aACC,qBAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAO,MAAM,IAAI,KAAK,WAAW,KAAK,WAAW,UAAU,eAAA,CAAgB;AAAA,YAC3E,SACC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,eAAY;AAAA,gBACZ,WAAU;AAAA,cAAA;AAAA,YAAA,IAEV;AAAA,UAAA,GACN,IAEA,oBAAC,OAAA,EAAI,eAAY,QAAO,WAAU,gBAAA,CAAgB;AAAA,UAErD;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AAEA,YAAY,cAAc;"}
@@ -1,11 +1,14 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
4
+ import { Slot } from "@radix-ui/react-slot";
4
5
  import { useControllableState } from "@radix-ui/react-use-controllable-state";
5
6
  import * as React from "react";
6
7
  import { cn } from "../../utils/cn.mjs";
7
8
  import { FLOATING_CONTENT_COLLISION_PADDING } from "../../utils/floatingContentCollisionPadding.mjs";
9
+ import { Drawer, DrawerContent, DrawerTrigger } from "../Drawer/Drawer.mjs";
8
10
  import { IconButton } from "../IconButton/IconButton.mjs";
11
+ import { CheckIcon } from "../Icons/CheckIcon.mjs";
9
12
  import { CloseIcon } from "../Icons/CloseIcon.mjs";
10
13
  import { SearchIcon } from "../Icons/SearchIcon.mjs";
11
14
  const TAP_MOVEMENT_THRESHOLD_PX = 10;
@@ -19,10 +22,12 @@ const NAVIGATION_KEYS = /* @__PURE__ */ new Set([
19
22
  "Enter"
20
23
  ]);
21
24
  const ToggleOpenContext = React.createContext(null);
25
+ const DropdownMenuVariantContext = React.createContext("menu");
22
26
  function DropdownMenu({
23
27
  open: openProp,
24
28
  defaultOpen,
25
29
  onOpenChange,
30
+ variant = "menu",
26
31
  children,
27
32
  ...props
28
33
  }) {
@@ -31,11 +36,18 @@ function DropdownMenu({
31
36
  defaultProp: defaultOpen ?? false,
32
37
  onChange: onOpenChange
33
38
  });
34
- return /* @__PURE__ */ jsx(ToggleOpenContext.Provider, { value: setOpen, children: /* @__PURE__ */ jsx(DropdownMenuPrimitive.Root, { open, onOpenChange: setOpen, ...props, children }) });
39
+ if (variant === "sheet") {
40
+ return /* @__PURE__ */ jsx(DropdownMenuVariantContext.Provider, { value: "sheet", children: /* @__PURE__ */ jsx(ToggleOpenContext.Provider, { value: setOpen, children: /* @__PURE__ */ jsx(Drawer, { open, onOpenChange: setOpen, children }) }) });
41
+ }
42
+ return /* @__PURE__ */ jsx(DropdownMenuVariantContext.Provider, { value: "menu", children: /* @__PURE__ */ jsx(ToggleOpenContext.Provider, { value: setOpen, children: /* @__PURE__ */ jsx(DropdownMenuPrimitive.Root, { open, onOpenChange: setOpen, ...props, children }) }) });
35
43
  }
36
44
  const DropdownMenuTrigger = React.forwardRef((props, ref) => {
45
+ const variant = React.useContext(DropdownMenuVariantContext);
37
46
  const toggleOpen = React.useContext(ToggleOpenContext);
38
47
  const tapRef = React.useRef(null);
48
+ if (variant === "sheet") {
49
+ return /* @__PURE__ */ jsx(DrawerTrigger, { ...props, ref });
50
+ }
39
51
  if (toggleOpen === null) {
40
52
  return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Trigger, { ...props, ref });
41
53
  }
@@ -94,46 +106,68 @@ const DropdownMenuContent = React.forwardRef(
94
106
  className,
95
107
  style,
96
108
  sideOffset = 4,
109
+ // Radix defaults `avoidCollisions` to true, so passing `collisionPadding`
110
+ // is enough to keep the menu flipping/shifting to stay on screen — no
111
+ // hand-rolled reposition logic needed.
97
112
  collisionPadding = FLOATING_CONTENT_COLLISION_PADDING,
113
+ children,
98
114
  ...props
99
- }, ref) => /* @__PURE__ */ jsx(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
100
- DropdownMenuPrimitive.Content,
101
- {
102
- ref,
103
- sideOffset,
104
- collisionPadding,
105
- className: cn(
106
- "w-max min-w-(--radix-dropdown-menu-trigger-width) max-w-(--radix-dropdown-menu-content-available-width) overflow-y-auto rounded-sm border border-neutral-alphas-200 bg-surface-primary p-1 text-content-primary shadow-lg",
107
- "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
108
- "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
109
- "data-[side=top]:slide-in-from-bottom-2 data-[side=bottom]:slide-in-from-top-2",
110
- "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
111
- className
112
- ),
113
- style: {
114
- zIndex: "var(--fanvue-ui-portal-z-index, 50)",
115
- maxHeight: "var(--radix-dropdown-menu-content-available-height)",
116
- ...style
117
- },
118
- ...props
115
+ }, ref) => {
116
+ const variant = React.useContext(DropdownMenuVariantContext);
117
+ if (variant === "sheet") {
118
+ return /* @__PURE__ */ jsx(
119
+ DrawerContent,
120
+ {
121
+ ref,
122
+ position: "bottom",
123
+ variant: "sheet",
124
+ className: cn("flex flex-col gap-1 p-1", className),
125
+ style,
126
+ ...props,
127
+ children
128
+ }
129
+ );
119
130
  }
120
- ) })
131
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
132
+ DropdownMenuPrimitive.Content,
133
+ {
134
+ ref,
135
+ sideOffset,
136
+ collisionPadding,
137
+ className: cn(
138
+ "w-max min-w-(--radix-dropdown-menu-trigger-width) max-w-(--radix-dropdown-menu-content-available-width) overflow-y-auto rounded-sm border border-neutral-alphas-200 bg-surface-primary p-1 text-content-primary shadow-lg",
139
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
140
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
141
+ "data-[side=top]:slide-in-from-bottom-2 data-[side=bottom]:slide-in-from-top-2",
142
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
143
+ className
144
+ ),
145
+ style: {
146
+ zIndex: "var(--fanvue-ui-portal-z-index, 50)",
147
+ maxHeight: "var(--radix-dropdown-menu-content-available-height)",
148
+ ...style
149
+ },
150
+ ...props,
151
+ children
152
+ }
153
+ ) });
154
+ }
121
155
  );
122
156
  DropdownMenuContent.displayName = "DropdownMenuContent";
123
157
  const DropdownMenuGroup = DropdownMenuPrimitive.Group;
124
158
  DropdownMenuGroup.displayName = "DropdownMenuGroup";
125
- const DropdownMenuLabel = React.forwardRef(({ className, position = "default", ...props }, ref) => /* @__PURE__ */ jsx(
126
- DropdownMenuPrimitive.Label,
127
- {
128
- ref,
129
- className: cn(
130
- "typography-description-12px-regular flex items-center px-3 text-content-secondary",
131
- position === "top" ? "py-2" : "pb-2 pt-4",
132
- className
133
- ),
134
- ...props
159
+ const DropdownMenuLabel = React.forwardRef(({ className, position = "default", ...props }, ref) => {
160
+ const variant = React.useContext(DropdownMenuVariantContext);
161
+ const labelClassName = cn(
162
+ "typography-description-12px-regular flex items-center px-3 text-content-secondary",
163
+ position === "top" ? "py-2" : "pb-2 pt-4",
164
+ className
165
+ );
166
+ if (variant === "sheet") {
167
+ return /* @__PURE__ */ jsx("div", { ref, className: labelClassName, ...props });
135
168
  }
136
- ));
169
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Label, { ref, className: labelClassName, ...props });
170
+ });
137
171
  DropdownMenuLabel.displayName = "DropdownMenuLabel";
138
172
  const SIZE_NORMALIZED = {
139
173
  "40": "40",
@@ -145,14 +179,18 @@ const ITEM_SIZE_CLASSES = {
145
179
  "40": "min-h-10 py-2 typography-body-default-16px-regular",
146
180
  "32": "min-h-8 py-[7px] typography-body-small-14px-regular"
147
181
  };
148
- const ITEM_SELECTED_TYPOGRAPHY = {
149
- "40": "typography-body-default-16px-semibold",
150
- "32": "typography-body-small-14px-semibold"
151
- };
152
182
  const ITEM_COUNT_TYPOGRAPHY = {
153
183
  "40": "typography-body-default-16px-regular",
154
184
  "32": "typography-body-small-14px-regular"
155
185
  };
186
+ function SelectedCheckIndicator({ hasDescription }) {
187
+ return /* @__PURE__ */ jsx(
188
+ CheckIcon,
189
+ {
190
+ className: cn("size-4 shrink-0 text-content-primary", hasDescription && "self-start")
191
+ }
192
+ );
193
+ }
156
194
  const DropdownMenuItem = React.forwardRef(
157
195
  ({
158
196
  size = "40",
@@ -166,8 +204,12 @@ const DropdownMenuItem = React.forwardRef(
166
204
  className,
167
205
  children,
168
206
  asChild,
207
+ onSelect,
208
+ disabled,
169
209
  ...props
170
210
  }, ref) => {
211
+ const variant = React.useContext(DropdownMenuVariantContext);
212
+ const toggleOpen = React.useContext(ToggleOpenContext);
171
213
  const normalizedSize = SIZE_NORMALIZED[size];
172
214
  const hasDescription = description != null;
173
215
  const hasAvatar = avatar != null;
@@ -180,17 +222,19 @@ const DropdownMenuItem = React.forwardRef(
180
222
  hasAvatar && !hasDescription && normalizedSize === "32" && "py-1",
181
223
  "data-[highlighted]:bg-neutral-alphas-50",
182
224
  "data-[disabled]:cursor-not-allowed data-[disabled]:text-content-disabled",
225
+ "disabled:cursor-not-allowed disabled:text-content-disabled",
226
+ // Sheet-variant asChild items are marked disabled via aria-disabled
227
+ // (see below), not the native disabled attribute or Radix's
228
+ // data-disabled — neither selector above matches them.
229
+ "aria-disabled:cursor-not-allowed aria-disabled:text-content-disabled",
183
230
  destructive && "text-error-content",
184
- selected && [
185
- "bg-buttons-primary-default text-content-primary-inverted",
186
- "data-[highlighted]:bg-buttons-primary-default",
187
- ITEM_SELECTED_TYPOGRAPHY[normalizedSize]
188
- ],
231
+ // bg-interaction-hover aliases to the same token as the plain hover
232
+ // background above, so a selected row would be indistinguishable from a
233
+ // hovered-but-unselected one. Use the next step up the neutral-alphas
234
+ // ramp instead (still a subtle overlay, not the heavy filled style).
235
+ selected && ["bg-neutral-alphas-100", "data-[highlighted]:bg-neutral-alphas-200"],
189
236
  className
190
237
  );
191
- if (asChild) {
192
- return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Item, { ref, asChild: true, className: itemClassName, ...props, children });
193
- }
194
238
  const iconAlignClassName = hasDescription ? "flex shrink-0 items-center pt-1" : null;
195
239
  const countNode = count != null && /* @__PURE__ */ jsx(
196
240
  "span",
@@ -198,41 +242,85 @@ const DropdownMenuItem = React.forwardRef(
198
242
  className: cn(
199
243
  "shrink-0 tabular-nums",
200
244
  ITEM_COUNT_TYPOGRAPHY[normalizedSize],
201
- destructive ? "text-error-content" : selected ? "text-content-primary-inverted" : "text-content-tertiary",
245
+ destructive ? "text-error-content" : "text-content-tertiary",
202
246
  "group-data-[disabled]:text-content-disabled"
203
247
  ),
204
248
  children: count
205
249
  }
206
250
  );
207
- return /* @__PURE__ */ jsxs(DropdownMenuPrimitive.Item, { ref, className: itemClassName, ...props, children: [
251
+ const trailingNode = trailingIcon != null ? hasDescription ? /* @__PURE__ */ jsx("span", { className: iconAlignClassName, children: trailingIcon }) : trailingIcon : selected && /* @__PURE__ */ jsx(SelectedCheckIndicator, { hasDescription });
252
+ const itemChildren = /* @__PURE__ */ jsxs(Fragment, { children: [
208
253
  avatar != null ? /* @__PURE__ */ jsx("span", { className: "shrink-0", children: avatar }) : leadingIcon != null && (hasDescription ? /* @__PURE__ */ jsx("span", { className: iconAlignClassName, children: leadingIcon }) : leadingIcon),
209
254
  hasDescription ? /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
210
255
  /* @__PURE__ */ jsx("span", { className: "truncate", children }),
211
- /* @__PURE__ */ jsx(
212
- "span",
213
- {
214
- className: cn(
215
- "typography-body-small-14px-regular truncate",
216
- selected ? "text-content-primary-inverted" : "text-content-secondary"
217
- ),
218
- children: description
219
- }
220
- )
256
+ /* @__PURE__ */ jsx("span", { className: "typography-body-small-14px-regular truncate text-content-secondary", children: description })
221
257
  ] }) : /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children }),
222
258
  countNode,
223
- trailingIcon != null && (hasDescription ? /* @__PURE__ */ jsx("span", { className: iconAlignClassName, children: trailingIcon }) : trailingIcon)
259
+ trailingNode
224
260
  ] });
261
+ if (variant === "sheet") {
262
+ const Comp = asChild ? Slot : "button";
263
+ const { onClick: consumerOnClick, ...restProps } = props;
264
+ const sheetSpecificProps = !asChild ? { type: "button", disabled } : disabled ? { "aria-disabled": true } : {};
265
+ return /* @__PURE__ */ jsx(
266
+ Comp,
267
+ {
268
+ ref,
269
+ ...restProps,
270
+ ...sheetSpecificProps,
271
+ role: "option",
272
+ "aria-selected": selected,
273
+ className: itemClassName,
274
+ onClick: (event) => {
275
+ if (disabled) {
276
+ event.preventDefault();
277
+ return;
278
+ }
279
+ consumerOnClick?.(event);
280
+ if (event.defaultPrevented) return;
281
+ onSelect?.(event.nativeEvent);
282
+ if (!event.nativeEvent.defaultPrevented) toggleOpen?.(() => false);
283
+ },
284
+ children: asChild ? children : itemChildren
285
+ }
286
+ );
287
+ }
288
+ if (asChild) {
289
+ return /* @__PURE__ */ jsx(
290
+ DropdownMenuPrimitive.Item,
291
+ {
292
+ ref,
293
+ asChild: true,
294
+ className: itemClassName,
295
+ disabled,
296
+ onSelect,
297
+ ...props,
298
+ children
299
+ }
300
+ );
301
+ }
302
+ return /* @__PURE__ */ jsx(
303
+ DropdownMenuPrimitive.Item,
304
+ {
305
+ ref,
306
+ className: itemClassName,
307
+ disabled,
308
+ onSelect,
309
+ ...props,
310
+ children: itemChildren
311
+ }
312
+ );
225
313
  }
226
314
  );
227
315
  DropdownMenuItem.displayName = "DropdownMenuItem";
228
- const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
229
- DropdownMenuPrimitive.Separator,
230
- {
231
- ref,
232
- className: cn("my-1 h-px bg-neutral-alphas-200", className),
233
- ...props
316
+ const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => {
317
+ const variant = React.useContext(DropdownMenuVariantContext);
318
+ const separatorClassName = cn("my-1 h-px bg-neutral-alphas-200", className);
319
+ if (variant === "sheet") {
320
+ return /* @__PURE__ */ jsx("hr", { ref, className: separatorClassName, ...props });
234
321
  }
235
- ));
322
+ return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Separator, { ref, className: separatorClassName, ...props });
323
+ });
236
324
  DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
237
325
  const DropdownMenuHeader = React.forwardRef(
238
326
  ({
@@ -338,8 +426,11 @@ const DropdownMenuRadioItem = React.forwardRef(({ className, children, helper, s
338
426
  "group flex w-full cursor-pointer items-start gap-3 rounded-xs px-4 py-2 outline-none",
339
427
  "data-[highlighted]:bg-neutral-alphas-50",
340
428
  "data-[disabled]:cursor-not-allowed data-[disabled]:text-content-disabled",
341
- "data-[state=checked]:bg-buttons-primary-default data-[state=checked]:text-content-primary-inverted",
342
- "data-[state=checked]:data-[highlighted]:bg-buttons-primary-default",
429
+ // See DropdownMenuItem above: bg-interaction-hover aliases to the same
430
+ // token as the plain hover background, so it can't distinguish the
431
+ // checked state from an unchecked-but-hovered row.
432
+ "data-[state=checked]:bg-neutral-alphas-100",
433
+ "data-[state=checked]:data-[highlighted]:bg-neutral-alphas-200",
343
434
  className
344
435
  ),
345
436
  ...props,
@@ -349,11 +440,10 @@ const DropdownMenuRadioItem = React.forwardRef(({ className, children, helper, s
349
440
  {
350
441
  className: cn(
351
442
  "mt-1 flex size-4 shrink-0 items-center justify-center rounded-full border border-icons-primary",
352
- "group-data-[disabled]:border-content-disabled",
353
- "group-data-[state=checked]:border-icons-primary-inverted"
443
+ "group-data-[disabled]:border-content-disabled"
354
444
  ),
355
445
  "aria-hidden": "true",
356
- children: /* @__PURE__ */ jsx(DropdownMenuPrimitive.ItemIndicator, { asChild: true, children: /* @__PURE__ */ jsx("span", { className: "size-2 rounded-full bg-content-primary-inverted" }) })
446
+ children: /* @__PURE__ */ jsx(DropdownMenuPrimitive.ItemIndicator, { asChild: true, children: /* @__PURE__ */ jsx("span", { className: "size-2 rounded-full bg-content-primary" }) })
357
447
  }
358
448
  ),
359
449
  /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
@@ -363,7 +453,6 @@ const DropdownMenuRadioItem = React.forwardRef(({ className, children, helper, s
363
453
  {
364
454
  className: cn(
365
455
  "typography-description-12px-regular text-content-secondary",
366
- "group-data-[state=checked]:text-content-primary-inverted",
367
456
  "group-data-[disabled]:text-content-disabled"
368
457
  ),
369
458
  children: helper