@fanvue/ui 3.15.0 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +16 -89
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs +54 -0
- package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs.map +1 -0
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs +5 -5
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs.map +1 -1
- package/dist/cjs/components/ChatInput/ChatInput.cjs +6 -11
- package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -1
- package/dist/cjs/components/ChatMessage/ChatMessage.cjs +228 -0
- package/dist/cjs/components/ChatMessage/ChatMessage.cjs.map +1 -0
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +160 -71
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/cjs/components/Icons/DenseGridViewIcon.cjs +52 -0
- package/dist/cjs/components/Icons/DenseGridViewIcon.cjs.map +1 -0
- package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs +66 -0
- package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs.map +1 -0
- package/dist/cjs/components/Select/Select.cjs +61 -17
- package/dist/cjs/components/Select/Select.cjs.map +1 -1
- package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs +53 -0
- package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs.map +1 -0
- package/dist/cjs/components/VoiceNote/VoiceNote.cjs +305 -0
- package/dist/cjs/components/VoiceNote/VoiceNote.cjs.map +1 -0
- package/dist/cjs/index.cjs +12 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/utils/audioWaveform.cjs +76 -0
- package/dist/cjs/utils/audioWaveform.cjs.map +1 -0
- package/dist/cjs/utils/useAudioPlayback.cjs +123 -0
- package/dist/cjs/utils/useAudioPlayback.cjs.map +1 -0
- package/dist/cjs/utils/useFittedBarCount.cjs +42 -0
- package/dist/cjs/utils/useFittedBarCount.cjs.map +1 -0
- package/dist/cjs/utils/useWaveformPeaks.cjs +48 -0
- package/dist/cjs/utils/useWaveformPeaks.cjs.map +1 -0
- package/dist/cjs/utils/useWaveformSeek.cjs +100 -0
- package/dist/cjs/utils/useWaveformSeek.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.mjs +7 -80
- package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -1
- package/dist/components/AudioRecordButton/AudioRecordButton.mjs +37 -0
- package/dist/components/AudioRecordButton/AudioRecordButton.mjs.map +1 -0
- package/dist/components/AudioUpload/AudioUpload.mjs +5 -5
- package/dist/components/AudioUpload/AudioUpload.mjs.map +1 -1
- package/dist/components/ChatInput/ChatInput.mjs +6 -11
- package/dist/components/ChatInput/ChatInput.mjs.map +1 -1
- package/dist/components/ChatMessage/ChatMessage.mjs +211 -0
- package/dist/components/ChatMessage/ChatMessage.mjs.map +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.mjs +161 -72
- package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
- package/dist/components/Icons/DenseGridViewIcon.mjs +35 -0
- package/dist/components/Icons/DenseGridViewIcon.mjs.map +1 -0
- package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs +49 -0
- package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs.map +1 -0
- package/dist/components/Select/Select.mjs +61 -17
- package/dist/components/Select/Select.mjs.map +1 -1
- package/dist/components/SubscribeButton/SubscribeButton.mjs +36 -0
- package/dist/components/SubscribeButton/SubscribeButton.mjs.map +1 -0
- package/dist/components/VoiceNote/VoiceNote.mjs +288 -0
- package/dist/components/VoiceNote/VoiceNote.mjs.map +1 -0
- package/dist/index.d.ts +321 -4
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -1
- package/dist/styles/base.css +11 -2
- package/dist/styles/theme.css +1 -1
- package/dist/utils/audioWaveform.mjs +76 -0
- package/dist/utils/audioWaveform.mjs.map +1 -0
- package/dist/utils/useAudioPlayback.mjs +106 -0
- package/dist/utils/useAudioPlayback.mjs.map +1 -0
- package/dist/utils/useFittedBarCount.mjs +25 -0
- package/dist/utils/useFittedBarCount.mjs.map +1 -0
- package/dist/utils/useWaveformPeaks.mjs +31 -0
- package/dist/utils/useWaveformPeaks.mjs.map +1 -0
- package/dist/utils/useWaveformSeek.mjs +83 -0
- package/dist/utils/useWaveformSeek.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,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":"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"}
|
|
@@ -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
|
-
|
|
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) =>
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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) =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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" :
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
|
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
|