@xhub-short/ui 0.1.0-beta.0 → 0.1.0-beta.1

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.
@@ -197,5 +197,11 @@ function VerifiedIcon(props) {
197
197
  )
198
198
  ] });
199
199
  }
200
+ function PlusIcon(props) {
201
+ return /* @__PURE__ */ jsxs("svg", { ...getIconProps({ ...props, fill: "none", stroke: "currentColor" }), "aria-hidden": "true", children: [
202
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19", strokeWidth: "2.5", strokeLinecap: "round" }),
203
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12", strokeWidth: "2.5", strokeLinecap: "round" })
204
+ ] });
205
+ }
200
206
 
201
- export { BookmarkFilledIcon, BookmarkIcon, CloseIcon, CommentIcon, HeartFilledIcon, HeartIcon, MoreIcon, MusicIcon, PauseIcon, PlayIcon, ShareIcon, UserCheckIcon, UserPlusIcon, VerifiedIcon, VolumeIcon, VolumeMutedIcon };
207
+ export { BookmarkFilledIcon, BookmarkIcon, CloseIcon, CommentIcon, HeartFilledIcon, HeartIcon, MoreIcon, MusicIcon, PauseIcon, PlayIcon, PlusIcon, ShareIcon, UserCheckIcon, UserPlusIcon, VerifiedIcon, VolumeIcon, VolumeMutedIcon };
@@ -15,7 +15,7 @@ var ACTION_BAR_CSS = (
15
15
  display: flex;
16
16
  flex-direction: column;
17
17
  align-items: center;
18
- gap: var(--sv-action-bar-gap, 16px);
18
+ gap: var(--sv-action-bar-gap, 12px);
19
19
  padding: var(--sv-action-bar-padding, 8px 0);
20
20
  }
21
21
 
@@ -77,10 +77,11 @@ var ACTION_BAR_CSS = (
77
77
  transform: none;
78
78
  }
79
79
 
80
- /* Pending state (API call in progress) */
80
+ /* Pending state - No visual change, debounce handles rapid clicks */
81
+ /* Button remains fully interactive for smooth UX like TikTok/Instagram */
81
82
  .sv-action-button--pending {
82
- opacity: var(--sv-action-button-pending-opacity, 0.6);
83
- pointer-events: none;
83
+ /* No opacity change - keep button fully visible */
84
+ /* No pointer-events:none - allow continuous clicks */
84
85
  }
85
86
 
86
87
  /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -323,11 +324,11 @@ function ActionButton({
323
324
  const handleClick = useCallback(
324
325
  (e) => {
325
326
  e.stopPropagation();
326
- if (!disabled && !isPending && onClick) {
327
+ if (!disabled && onClick) {
327
328
  onClick();
328
329
  }
329
330
  },
330
- [disabled, isPending, onClick]
331
+ [disabled, onClick]
331
332
  );
332
333
  const handlePointerDown = useCallback((e) => {
333
334
  e.stopPropagation();
@@ -335,16 +336,25 @@ function ActionButton({
335
336
  const handlePointerUp = useCallback((e) => {
336
337
  e.stopPropagation();
337
338
  }, []);
339
+ const handleTouchEnd = useCallback(
340
+ (e) => {
341
+ e.stopPropagation();
342
+ if (!disabled && onClick) {
343
+ onClick();
344
+ }
345
+ },
346
+ [disabled, onClick]
347
+ );
338
348
  const handleKeyDown = useCallback(
339
349
  (e) => {
340
350
  if (e.key === "Enter" || e.key === " ") {
341
351
  e.preventDefault();
342
- if (!disabled && !isPending && onClick) {
352
+ if (!disabled && onClick) {
343
353
  onClick();
344
354
  }
345
355
  }
346
356
  },
347
- [disabled, isPending, onClick]
357
+ [disabled, onClick]
348
358
  );
349
359
  const formatter = customFormatCount ?? formatCount;
350
360
  const formattedCount = count !== void 0 ? formatter(count) : null;
@@ -368,8 +378,9 @@ function ActionButton({
368
378
  onClick: handleClick,
369
379
  onPointerDown: handlePointerDown,
370
380
  onPointerUp: handlePointerUp,
381
+ onTouchEnd: handleTouchEnd,
371
382
  onKeyDown: handleKeyDown,
372
- disabled: disabled || isPending,
383
+ disabled,
373
384
  "aria-label": ariaLabel,
374
385
  "aria-pressed": isActive,
375
386
  "data-testid": testId,
@@ -0,0 +1,562 @@
1
+ import { injectComponentCSS } from './chunk-UXMA4KJZ.js';
2
+ import { clsx } from 'clsx';
3
+ import { createContext, useInsertionEffect, useMemo, useState, useContext } from 'react';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+
6
+ var VideoInfoContext = createContext(void 0);
7
+ function useVideoInfoContext() {
8
+ const context = useContext(VideoInfoContext);
9
+ if (context === void 0) {
10
+ throw new Error("useVideoInfoContext must be used within a VideoInfoHeadless component");
11
+ }
12
+ return context;
13
+ }
14
+ function useOptionalVideoInfoContext() {
15
+ return useContext(VideoInfoContext);
16
+ }
17
+ function DefaultVerifiedIcon() {
18
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "10", height: "10", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" }) });
19
+ }
20
+ function VideoAuthorName({
21
+ name,
22
+ showAtPrefix = true,
23
+ showVerified = true,
24
+ isVerified,
25
+ verifiedIcon,
26
+ onClick,
27
+ className,
28
+ children
29
+ }) {
30
+ const context = useOptionalVideoInfoContext();
31
+ const authorName = name ?? context?.state.authorName;
32
+ const verified = isVerified ?? context?.state.isVerified ?? false;
33
+ const handleClick = onClick ?? context?.actions.openProfile;
34
+ if (!authorName) {
35
+ return null;
36
+ }
37
+ const handleKeyDown = (e) => {
38
+ if ((e.key === "Enter" || e.key === " ") && handleClick) {
39
+ e.preventDefault();
40
+ e.stopPropagation();
41
+ handleClick();
42
+ }
43
+ };
44
+ return /* @__PURE__ */ jsx(
45
+ "div",
46
+ {
47
+ className: clsx("sv-video-info__author", className),
48
+ onClick: (e) => {
49
+ e.stopPropagation();
50
+ handleClick?.();
51
+ },
52
+ onKeyDown: handleKeyDown,
53
+ role: handleClick ? "button" : void 0,
54
+ tabIndex: handleClick ? 0 : void 0,
55
+ "aria-label": handleClick ? `View ${authorName}'s profile` : void 0,
56
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ /* @__PURE__ */ jsxs("span", { className: "sv-video-info__author-name", children: [
58
+ showAtPrefix && /* @__PURE__ */ jsx("span", { className: "sv-video-info__author-prefix", children: "@" }),
59
+ authorName
60
+ ] }),
61
+ showVerified && verified && /* @__PURE__ */ jsx("span", { className: "sv-video-info__verified-badge", "aria-label": "Verified", children: verifiedIcon ?? /* @__PURE__ */ jsx(DefaultVerifiedIcon, {}) })
62
+ ] })
63
+ }
64
+ );
65
+ }
66
+ function VideoCaption({
67
+ caption,
68
+ maxLines = 2,
69
+ expandable = false,
70
+ moreText = "more",
71
+ lessText = "less",
72
+ className,
73
+ children
74
+ }) {
75
+ const context = useOptionalVideoInfoContext();
76
+ const [isExpanded, setIsExpanded] = useState(false);
77
+ const captionText = caption ?? context?.state.caption;
78
+ if (!captionText) {
79
+ return null;
80
+ }
81
+ const lineClampClass = isExpanded ? "sv-video-info__caption--expanded" : `sv-video-info__caption--lines-${maxLines}`;
82
+ const handleToggleExpand = (e) => {
83
+ e.stopPropagation();
84
+ setIsExpanded(!isExpanded);
85
+ };
86
+ return /* @__PURE__ */ jsx("div", { className: clsx("sv-video-info__caption", lineClampClass, className), children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
87
+ captionText,
88
+ expandable && /* @__PURE__ */ jsx(
89
+ "button",
90
+ {
91
+ type: "button",
92
+ className: "sv-video-info__caption-more",
93
+ onClick: handleToggleExpand,
94
+ children: isExpanded ? lessText : moreText
95
+ }
96
+ )
97
+ ] }) });
98
+ }
99
+ function VideoHashtags({
100
+ hashtags,
101
+ clickable = true,
102
+ onHashtagClick,
103
+ display = "block",
104
+ className,
105
+ children
106
+ }) {
107
+ const context = useOptionalVideoInfoContext();
108
+ const hashtagsArray = hashtags ?? context?.state.hashtags ?? [];
109
+ const handleClick = onHashtagClick ?? context?.actions.onHashtagClick;
110
+ if (hashtagsArray.length === 0) {
111
+ return null;
112
+ }
113
+ const handleHashtagClick = (tag) => (e) => {
114
+ e.stopPropagation();
115
+ if (clickable && handleClick) {
116
+ handleClick(tag);
117
+ }
118
+ };
119
+ const handleKeyDown = (tag) => (e) => {
120
+ if ((e.key === "Enter" || e.key === " ") && clickable && handleClick) {
121
+ e.preventDefault();
122
+ handleClick(tag);
123
+ }
124
+ };
125
+ const formatTag = (tag) => tag.startsWith("#") ? tag : `#${tag}`;
126
+ if (children) {
127
+ return /* @__PURE__ */ jsx(Fragment, { children });
128
+ }
129
+ return /* @__PURE__ */ jsx(
130
+ "div",
131
+ {
132
+ className: clsx(
133
+ "sv-video-info__hashtags",
134
+ display === "inline" && "sv-video-info__hashtags--inline",
135
+ className
136
+ ),
137
+ children: hashtagsArray.map((tag) => /* @__PURE__ */ jsx(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ className: clsx(
142
+ "sv-video-info__hashtag",
143
+ !clickable && "sv-video-info__hashtag--not-clickable"
144
+ ),
145
+ onClick: handleHashtagClick(tag),
146
+ onKeyDown: handleKeyDown(tag),
147
+ tabIndex: clickable ? 0 : -1,
148
+ "aria-label": clickable ? `View hashtag ${tag}` : void 0,
149
+ children: formatTag(tag)
150
+ },
151
+ tag
152
+ ))
153
+ }
154
+ );
155
+ }
156
+
157
+ // src/components/VideoInfo/VideoInfo.css.ts
158
+ var VIDEO_INFO_CSS = `
159
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
160
+ * VideoInfo Container
161
+ * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
162
+
163
+ .sv-video-info {
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: var(--sv-video-info-gap, 6px);
167
+ color: var(--sv-video-info-color, #fff);
168
+ font-family: var(--sv-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
169
+ text-shadow: var(--sv-video-info-text-shadow, 0 1px 2px rgba(0, 0, 0, 0.5));
170
+ max-width: 100%;
171
+ overflow: hidden;
172
+ }
173
+
174
+ /* Overlay variant (on top of video) */
175
+ .sv-video-info--overlay {
176
+ pointer-events: none;
177
+ }
178
+
179
+ .sv-video-info--overlay > * {
180
+ pointer-events: auto;
181
+ }
182
+
183
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
184
+ * Author Name
185
+ * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
186
+
187
+ .sv-video-info__author {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: var(--sv-video-info-author-gap, 6px);
191
+ cursor: pointer;
192
+ transition: opacity 0.15s ease;
193
+ }
194
+
195
+ .sv-video-info__author:hover {
196
+ opacity: 0.85;
197
+ }
198
+
199
+ .sv-video-info__author:active {
200
+ opacity: 0.7;
201
+ }
202
+
203
+ .sv-video-info__author-name {
204
+ font-size: var(--sv-video-info-author-font-size, 16px);
205
+ font-weight: var(--sv-video-info-author-font-weight, 700);
206
+ line-height: 1.3;
207
+ white-space: nowrap;
208
+ overflow: hidden;
209
+ text-overflow: ellipsis;
210
+ max-width: 200px;
211
+ }
212
+
213
+ .sv-video-info__author-prefix {
214
+ opacity: 0.95;
215
+ }
216
+
217
+ .sv-video-info__verified-badge {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ width: var(--sv-video-info-verified-size, 14px);
222
+ height: var(--sv-video-info-verified-size, 14px);
223
+ background: var(--sv-video-info-verified-bg, #20d5ec);
224
+ border-radius: 50%;
225
+ flex-shrink: 0;
226
+ }
227
+
228
+ .sv-video-info__verified-badge svg,
229
+ .sv-video-info__verified-badge span {
230
+ font-size: 10px;
231
+ color: #fff;
232
+ font-weight: 700;
233
+ }
234
+
235
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
236
+ * Caption
237
+ * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
238
+
239
+ .sv-video-info__caption {
240
+ font-size: var(--sv-video-info-caption-font-size, 14px);
241
+ line-height: var(--sv-video-info-caption-line-height, 1.4);
242
+ color: var(--sv-video-info-caption-color, #fff);
243
+ display: -webkit-box;
244
+ -webkit-box-orient: vertical;
245
+ overflow: hidden;
246
+ word-break: break-word;
247
+ }
248
+
249
+ .sv-video-info__caption--lines-1 {
250
+ -webkit-line-clamp: 1;
251
+ }
252
+
253
+ .sv-video-info__caption--lines-2 {
254
+ -webkit-line-clamp: 2;
255
+ }
256
+
257
+ .sv-video-info__caption--lines-3 {
258
+ -webkit-line-clamp: 3;
259
+ }
260
+
261
+ .sv-video-info__caption--expanded {
262
+ -webkit-line-clamp: unset;
263
+ }
264
+
265
+ /* "more" button for expandable caption */
266
+ .sv-video-info__caption-more {
267
+ background: none;
268
+ border: none;
269
+ color: var(--sv-video-info-caption-more-color, rgba(255, 255, 255, 0.7));
270
+ font-size: var(--sv-video-info-caption-font-size, 14px);
271
+ padding: 0;
272
+ margin-left: 4px;
273
+ cursor: pointer;
274
+ }
275
+
276
+ .sv-video-info__caption-more:hover {
277
+ color: var(--sv-video-info-caption-color, #fff);
278
+ }
279
+
280
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
281
+ * Hashtags
282
+ * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
283
+
284
+ .sv-video-info__hashtags {
285
+ display: inline-flex;
286
+ flex-direction: row;
287
+ flex-wrap: wrap;
288
+ align-items: center;
289
+ gap: var(--sv-video-info-hashtag-gap, 6px);
290
+ font-size: var(--sv-video-info-hashtag-font-size, 14px);
291
+ }
292
+
293
+ /* Inline variant - more compact */
294
+ .sv-video-info__hashtags--inline {
295
+ gap: var(--sv-video-info-hashtag-gap-inline, 4px);
296
+ }
297
+
298
+ .sv-video-info__hashtag {
299
+ display: inline-flex;
300
+ color: var(--sv-video-info-hashtag-color, #fff);
301
+ font-weight: var(--sv-video-info-hashtag-font-weight, 500);
302
+ background: none;
303
+ border: none;
304
+ padding: 0;
305
+ cursor: pointer;
306
+ transition: opacity 0.15s ease;
307
+ white-space: nowrap;
308
+ }
309
+
310
+ .sv-video-info__hashtag:hover {
311
+ opacity: 0.8;
312
+ text-decoration: underline;
313
+ }
314
+
315
+ .sv-video-info__hashtag--not-clickable {
316
+ cursor: default;
317
+ }
318
+
319
+ .sv-video-info__hashtag--not-clickable:hover {
320
+ opacity: 1;
321
+ text-decoration: none;
322
+ }
323
+
324
+ /* Inline hashtags (inside caption) */
325
+ .sv-video-info__caption .sv-video-info__hashtag {
326
+ display: inline;
327
+ }
328
+
329
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
330
+ * Location
331
+ * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
332
+
333
+ .sv-video-info__location {
334
+ display: flex;
335
+ align-items: center;
336
+ gap: var(--sv-video-info-location-gap, 4px);
337
+ font-size: var(--sv-video-info-location-font-size, 13px);
338
+ color: var(--sv-video-info-location-color, rgba(255, 255, 255, 0.9));
339
+ cursor: pointer;
340
+ transition: opacity 0.15s ease;
341
+ }
342
+
343
+ .sv-video-info__location:hover {
344
+ opacity: 0.8;
345
+ }
346
+
347
+ .sv-video-info__location-icon {
348
+ font-size: 14px;
349
+ flex-shrink: 0;
350
+ }
351
+
352
+ .sv-video-info__location-text {
353
+ white-space: nowrap;
354
+ overflow: hidden;
355
+ text-overflow: ellipsis;
356
+ }
357
+
358
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
359
+ * Music
360
+ * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
361
+
362
+ .sv-video-info__music {
363
+ display: flex;
364
+ align-items: center;
365
+ gap: var(--sv-video-info-music-gap, 8px);
366
+ font-size: var(--sv-video-info-music-font-size, 13px);
367
+ color: var(--sv-video-info-music-color, #fff);
368
+ cursor: pointer;
369
+ transition: opacity 0.15s ease;
370
+ overflow: hidden;
371
+ }
372
+
373
+ .sv-video-info__music:hover {
374
+ opacity: 0.8;
375
+ }
376
+
377
+ .sv-video-info__music-icon {
378
+ font-size: 14px;
379
+ flex-shrink: 0;
380
+ }
381
+
382
+ .sv-video-info__music-text {
383
+ white-space: nowrap;
384
+ overflow: hidden;
385
+ text-overflow: ellipsis;
386
+ /* Marquee animation for long text */
387
+ }
388
+
389
+ /* Marquee animation for music text */
390
+ .sv-video-info__music-text--marquee {
391
+ animation: sv-video-info-marquee 8s linear infinite;
392
+ }
393
+
394
+ @keyframes sv-video-info-marquee {
395
+ 0% {
396
+ transform: translateX(0);
397
+ }
398
+ 100% {
399
+ transform: translateX(-50%);
400
+ }
401
+ }
402
+ `;
403
+ function DefaultLocationIcon() {
404
+ return /* @__PURE__ */ jsx("span", { className: "sv-video-info__location-icon", children: "\u{1F4CD}" });
405
+ }
406
+ function VideoLocation({
407
+ location,
408
+ icon,
409
+ onClick,
410
+ className,
411
+ children
412
+ }) {
413
+ const context = useOptionalVideoInfoContext();
414
+ const locationText = location ?? context?.state.location;
415
+ const handleClick = onClick ?? context?.actions.onLocationClick;
416
+ if (!locationText) {
417
+ return null;
418
+ }
419
+ const handleKeyDown = (e) => {
420
+ if ((e.key === "Enter" || e.key === " ") && handleClick) {
421
+ e.preventDefault();
422
+ e.stopPropagation();
423
+ handleClick();
424
+ }
425
+ };
426
+ return /* @__PURE__ */ jsx(
427
+ "div",
428
+ {
429
+ className: clsx("sv-video-info__location", className),
430
+ onClick: (e) => {
431
+ e.stopPropagation();
432
+ handleClick?.();
433
+ },
434
+ onKeyDown: handleKeyDown,
435
+ role: handleClick ? "button" : void 0,
436
+ tabIndex: handleClick ? 0 : void 0,
437
+ "aria-label": handleClick ? `View location: ${locationText}` : void 0,
438
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
439
+ icon ?? /* @__PURE__ */ jsx(DefaultLocationIcon, {}),
440
+ /* @__PURE__ */ jsx("span", { className: "sv-video-info__location-text", children: locationText })
441
+ ] })
442
+ }
443
+ );
444
+ }
445
+ function DefaultMusicIcon() {
446
+ return /* @__PURE__ */ jsx("span", { className: "sv-video-info__music-icon", children: "\u{1F3B5}" });
447
+ }
448
+ function VideoMusic({
449
+ title,
450
+ artist,
451
+ icon,
452
+ marquee = false,
453
+ onClick,
454
+ className,
455
+ children
456
+ }) {
457
+ const context = useOptionalVideoInfoContext();
458
+ const musicTitle = title ?? context?.state.music?.title;
459
+ const musicArtist = artist ?? context?.state.music?.artist;
460
+ const handleClick = onClick ?? context?.actions.onMusicClick;
461
+ if (!musicTitle && !musicArtist) {
462
+ return null;
463
+ }
464
+ const displayText = musicArtist ? `${musicTitle} - ${musicArtist}` : musicTitle;
465
+ const handleKeyDown = (e) => {
466
+ if ((e.key === "Enter" || e.key === " ") && handleClick) {
467
+ e.preventDefault();
468
+ e.stopPropagation();
469
+ handleClick();
470
+ }
471
+ };
472
+ return /* @__PURE__ */ jsx(
473
+ "div",
474
+ {
475
+ className: clsx("sv-video-info__music", className),
476
+ onClick: (e) => {
477
+ e.stopPropagation();
478
+ handleClick?.();
479
+ },
480
+ onKeyDown: handleKeyDown,
481
+ role: handleClick ? "button" : void 0,
482
+ tabIndex: handleClick ? 0 : void 0,
483
+ "aria-label": handleClick ? `View sound: ${displayText}` : void 0,
484
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
485
+ icon ?? /* @__PURE__ */ jsx(DefaultMusicIcon, {}),
486
+ /* @__PURE__ */ jsx(
487
+ "span",
488
+ {
489
+ className: clsx(
490
+ "sv-video-info__music-text",
491
+ marquee && "sv-video-info__music-text--marquee"
492
+ ),
493
+ children: displayText
494
+ }
495
+ )
496
+ ] })
497
+ }
498
+ );
499
+ }
500
+ function VideoInfoHeadlessRoot({
501
+ videoInfoState,
502
+ videoInfoActions,
503
+ overlay = false,
504
+ className,
505
+ children,
506
+ testId
507
+ }) {
508
+ useInsertionEffect(() => {
509
+ return injectComponentCSS("video-info", VIDEO_INFO_CSS);
510
+ }, []);
511
+ const contextValue = useMemo(
512
+ () => ({
513
+ state: videoInfoState,
514
+ actions: videoInfoActions
515
+ }),
516
+ [videoInfoState, videoInfoActions]
517
+ );
518
+ const stopPropagation = (e) => {
519
+ e.stopPropagation();
520
+ };
521
+ return /* @__PURE__ */ jsx(VideoInfoContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
522
+ "div",
523
+ {
524
+ className: clsx("sv-video-info", overlay && "sv-video-info--overlay", className),
525
+ onClick: stopPropagation,
526
+ onKeyDown: stopPropagation,
527
+ onPointerDown: stopPropagation,
528
+ onPointerUp: stopPropagation,
529
+ onTouchStart: stopPropagation,
530
+ onTouchEnd: stopPropagation,
531
+ "data-testid": testId,
532
+ children: children ?? // Default layout
533
+ /* @__PURE__ */ jsxs(Fragment, { children: [
534
+ /* @__PURE__ */ jsx(VideoAuthorName, { showAtPrefix: true, showVerified: true }),
535
+ /* @__PURE__ */ jsx(VideoCaption, { maxLines: 2 }),
536
+ /* @__PURE__ */ jsx(VideoHashtags, { clickable: true })
537
+ ] })
538
+ }
539
+ ) });
540
+ }
541
+ function VideoInfoContent({
542
+ children,
543
+ className
544
+ }) {
545
+ return /* @__PURE__ */ jsx("div", { className: clsx("sv-video-info__content", className), children });
546
+ }
547
+ var VideoInfoHeadless = Object.assign(VideoInfoHeadlessRoot, {
548
+ /** Author name with @ prefix and verified badge */
549
+ AuthorName: VideoAuthorName,
550
+ /** Video caption/title with line clamping */
551
+ Caption: VideoCaption,
552
+ /** Hashtags (inline or block) */
553
+ Hashtags: VideoHashtags,
554
+ /** Location info */
555
+ Location: VideoLocation,
556
+ /** Music/sound info */
557
+ Music: VideoMusic,
558
+ /** Content wrapper */
559
+ Content: VideoInfoContent
560
+ });
561
+
562
+ export { VIDEO_INFO_CSS, VideoAuthorName, VideoCaption, VideoHashtags, VideoInfoContext, VideoInfoHeadless, VideoLocation, VideoMusic, useOptionalVideoInfoContext, useVideoInfoContext };