@xhub-short/ui 0.1.0-beta.9 → 1.0.0-beta.18

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 (74) hide show
  1. package/dist/CommentSheet.css-DuBy01rU.d.ts +219 -0
  2. package/dist/VideoSlotPlayIndicator-DPs8Xt5C.d.ts +51 -0
  3. package/dist/chunk-3OB3OVYR.js +349 -0
  4. package/dist/chunk-4RIMQOBR.js +58 -0
  5. package/dist/chunk-5MKYDI4X.js +1 -0
  6. package/dist/chunk-5Y43HNRG.js +296 -0
  7. package/dist/chunk-7WXAQHJI.js +350 -0
  8. package/dist/chunk-BADA7OLG.js +564 -0
  9. package/dist/chunk-BEJAJFV6.js +191 -0
  10. package/dist/chunk-BNI7CYRI.js +334 -0
  11. package/dist/{chunk-RMLTPW5S.js → chunk-CAWE42LH.js} +4 -3
  12. package/dist/chunk-DGKMO3AE.js +717 -0
  13. package/dist/chunk-DR7KR7OT.js +103 -0
  14. package/dist/chunk-GSNIZ6DF.js +605 -0
  15. package/dist/chunk-HXQPEZRG.js +105 -0
  16. package/dist/{chunk-FNXTPQ6L.js → chunk-IC2KUU4V.js} +31 -1340
  17. package/dist/chunk-IWSBYOSS.js +91 -0
  18. package/dist/chunk-MFJS65C5.js +368 -0
  19. package/dist/{chunk-AC2IFAJR.js → chunk-NJXIYSDZ.js} +12 -1
  20. package/dist/{chunk-CL6BS7GB.js → chunk-OM4L7RE5.js} +12 -2
  21. package/dist/chunk-QCRRF76W.js +75 -0
  22. package/dist/chunk-QUEJHA24.js +508 -0
  23. package/dist/chunk-UYBQTE4M.js +337 -0
  24. package/dist/chunk-VJ744W5N.js +603 -0
  25. package/dist/chunk-VXW7AOGM.js +285 -0
  26. package/dist/{chunk-AQHD6LPS.js → chunk-YB7AXTX7.js} +1 -1
  27. package/dist/components/ActionBar/index.d.ts +7 -2
  28. package/dist/components/ActionBar/index.js +1 -1
  29. package/dist/components/AdvanceMenu/index.d.ts +80 -0
  30. package/dist/components/AdvanceMenu/index.js +1 -0
  31. package/dist/components/ArticleSlot/index.d.ts +213 -0
  32. package/dist/components/ArticleSlot/index.js +1 -0
  33. package/dist/components/AuthorInfo/index.js +1 -1
  34. package/dist/components/BottomSheet/index.d.ts +87 -0
  35. package/dist/components/BottomSheet/index.js +1 -0
  36. package/dist/components/CleanModeOverlay/index.d.ts +60 -0
  37. package/dist/components/CleanModeOverlay/index.js +1 -0
  38. package/dist/components/CommentSheet/index.d.ts +1 -1
  39. package/dist/components/CommentSheet/index.js +1 -1
  40. package/dist/components/DetailView/index.d.ts +314 -0
  41. package/dist/components/DetailView/index.js +1 -0
  42. package/dist/components/ErrorBoundary/index.js +1 -1
  43. package/dist/components/{VideoFeed → Feed}/index.d.ts +64 -45
  44. package/dist/components/Feed/index.js +1 -0
  45. package/dist/components/ImageCarousel/index.d.ts +50 -0
  46. package/dist/components/ImageCarousel/index.js +1 -0
  47. package/dist/components/Playlist/index.d.ts +117 -0
  48. package/dist/components/Playlist/index.js +1 -0
  49. package/dist/components/ProgressBar/index.js +1 -1
  50. package/dist/components/QualityPicker/index.d.ts +35 -0
  51. package/dist/components/QualityPicker/index.js +1 -0
  52. package/dist/components/ReportSheet/index.d.ts +74 -0
  53. package/dist/components/ReportSheet/index.js +1 -0
  54. package/dist/components/Skeleton/index.js +1 -1
  55. package/dist/components/SpeedPicker/index.d.ts +32 -0
  56. package/dist/components/SpeedPicker/index.js +1 -0
  57. package/dist/components/VideoInfo/index.d.ts +7 -3
  58. package/dist/components/VideoInfo/index.js +1 -1
  59. package/dist/components/VideoPlayer/index.js +1 -1
  60. package/dist/components/VideoSlot/index.d.ts +31 -68
  61. package/dist/components/VideoSlot/index.js +2 -1
  62. package/dist/components/VirtualSlider/index.js +1 -1
  63. package/dist/index.d.ts +40 -10
  64. package/dist/index.js +23 -13
  65. package/package.json +8 -4
  66. package/dist/CommentSheet.css-BeCrEaUG.d.ts +0 -221
  67. package/dist/chunk-2FSDVYER.js +0 -737
  68. package/dist/chunk-ECR42RKK.js +0 -571
  69. package/dist/chunk-KWHMZ6H5.js +0 -562
  70. package/dist/chunk-SZXFH334.js +0 -204
  71. package/dist/chunk-UNV3NWN6.js +0 -148
  72. package/dist/chunk-WCRDTBCZ.js +0 -357
  73. package/dist/chunk-XDIH66C4.js +0 -1519
  74. package/dist/components/VideoFeed/index.js +0 -1
@@ -0,0 +1,314 @@
1
+ import * as react from 'react';
2
+ import { ReactElement, ReactNode } from 'react';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { Article, MusicInfo } from '@xhub-short/contracts';
5
+
6
+ interface DetailViewContextValue {
7
+ /** The article data */
8
+ article: Article;
9
+ /** Current image index */
10
+ currentImageIndex: number;
11
+ /** Set current image index */
12
+ setCurrentImageIndex: (index: number) => void;
13
+ /** Close detail view */
14
+ close: () => void;
15
+ /** Whether view is animating */
16
+ isAnimating: boolean;
17
+ /** Actions */
18
+ actions: {
19
+ onLike?: () => void;
20
+ onComment?: () => void;
21
+ onShare?: () => void;
22
+ onBookmark?: () => void;
23
+ onFollow?: () => void;
24
+ onGestureLock?: (isLocked: boolean) => void;
25
+ };
26
+ /** States */
27
+ states: {
28
+ isLiked: boolean;
29
+ isBookmarked: boolean;
30
+ isFollowing: boolean;
31
+ };
32
+ }
33
+ /**
34
+ * Hook to access DetailView context
35
+ * @throws if used outside DetailViewHeadless
36
+ */
37
+ declare function useDetailViewContext(): DetailViewContextValue;
38
+ declare function BackIcon(): react_jsx_runtime.JSX.Element;
39
+ declare function HeartIcon({ filled }: {
40
+ filled?: boolean;
41
+ }): react_jsx_runtime.JSX.Element;
42
+ declare function CommentIcon(): react_jsx_runtime.JSX.Element;
43
+ declare function BookmarkIcon({ filled }: {
44
+ filled?: boolean;
45
+ }): react_jsx_runtime.JSX.Element;
46
+ declare function ShareIcon(): react_jsx_runtime.JSX.Element;
47
+ interface DetailViewHeadlessProps {
48
+ /** Whether the detail view is open */
49
+ isOpen: boolean;
50
+ /** Close handler */
51
+ onClose: () => void;
52
+ /** The article to display */
53
+ article: Article;
54
+ /** Initial image index */
55
+ initialImageIndex?: number;
56
+ /** Animation duration in ms */
57
+ animationDuration?: number;
58
+ /** Enable swipe down to close */
59
+ enableSwipeToClose?: boolean;
60
+ /** Swipe threshold (percentage) to trigger close */
61
+ swipeCloseThreshold?: number;
62
+ /** Custom header title (fallback if no author) */
63
+ headerTitle?: string;
64
+ /** Show drag indicator */
65
+ showDragIndicator?: boolean;
66
+ /** Whether post is liked */
67
+ isLiked?: boolean;
68
+ /** Whether post is bookmarked */
69
+ isBookmarked?: boolean;
70
+ /** Whether following author */
71
+ isFollowing?: boolean;
72
+ /** Like button handler */
73
+ onLike?: () => void;
74
+ /** Comment button handler */
75
+ onComment?: () => void;
76
+ /** Share button handler */
77
+ onShare?: () => void;
78
+ /** Bookmark button handler */
79
+ onBookmark?: () => void;
80
+ /** Follow button handler */
81
+ onFollow?: () => void;
82
+ /** Callback to lock/unlock global gestures */
83
+ onGestureLock?: (isLocked: boolean) => void;
84
+ /**
85
+ * Custom bottom bar renderer.
86
+ * When provided, replaces the entire default bottom bar.
87
+ * Use compound components (DetailViewBottomBar, DetailViewActionButton, etc.)
88
+ * to build custom layouts.
89
+ *
90
+ * @example
91
+ * ```tsx
92
+ * <DetailViewHeadless
93
+ * renderBottomBar={() => (
94
+ * <DetailViewBottomBar>
95
+ * <DetailViewActionButton type="like" />
96
+ * <DetailViewActionButton type="share" />
97
+ * </DetailViewBottomBar>
98
+ * )}
99
+ * />
100
+ * ```
101
+ */
102
+ renderBottomBar?: () => ReactNode;
103
+ /**
104
+ * Hide the default bottom bar completely.
105
+ * Use when you want to render your own bottom bar via children.
106
+ */
107
+ hideBottomBar?: boolean;
108
+ /** Additional class name */
109
+ className?: string;
110
+ /** Children */
111
+ children?: ReactNode;
112
+ }
113
+ declare function formatCount(count: number): string;
114
+ declare function DetailViewHeadlessBase({ isOpen, onClose, article, initialImageIndex, animationDuration, enableSwipeToClose, swipeCloseThreshold, headerTitle, showDragIndicator, isLiked, isBookmarked, isFollowing, onLike, onComment, onShare, onBookmark, onFollow, renderBottomBar, hideBottomBar, onGestureLock, className, children, }: DetailViewHeadlessProps): ReactElement | null;
115
+ declare const DetailViewHeadless: react.MemoExoticComponent<typeof DetailViewHeadlessBase>;
116
+ /**
117
+ * Image Gallery for detail view
118
+ */
119
+ interface DetailViewGalleryProps {
120
+ /** Enable zoom on images */
121
+ enableZoom?: boolean;
122
+ /** Class name */
123
+ className?: string;
124
+ }
125
+ declare function DetailViewGallery({ enableZoom, className, }: DetailViewGalleryProps): ReactElement;
126
+ /**
127
+ * Author section for detail view
128
+ */
129
+ interface DetailViewAuthorProps {
130
+ /** Whether user is following this author */
131
+ isFollowing?: boolean;
132
+ /** Follow button click handler */
133
+ onFollowClick?: () => void;
134
+ /** Show follow button */
135
+ showFollowButton?: boolean;
136
+ /** Format date function */
137
+ formatDate?: (date: Date | string) => string;
138
+ /** Follow button label */
139
+ followLabel?: string;
140
+ /** Following button label */
141
+ followingLabel?: string;
142
+ /** Class name */
143
+ className?: string;
144
+ }
145
+ declare function DetailViewAuthor({ isFollowing, onFollowClick, showFollowButton, formatDate, followLabel, followingLabel, className, }: DetailViewAuthorProps): ReactElement | null;
146
+ /**
147
+ * Caption display for detail view
148
+ */
149
+ interface DetailViewCaptionProps {
150
+ /** Render hashtags as links */
151
+ renderHashtag?: (hashtag: string) => ReactNode;
152
+ /** Class name */
153
+ className?: string;
154
+ }
155
+ declare function DetailViewCaption({ renderHashtag, className, }: DetailViewCaptionProps): ReactElement | null;
156
+ /**
157
+ * Stats display for detail view
158
+ */
159
+ interface DetailViewStatsProps {
160
+ /** Format number function */
161
+ formatNumber?: (num: number) => string;
162
+ /** Labels */
163
+ labels?: {
164
+ likes?: string;
165
+ comments?: string;
166
+ shares?: string;
167
+ views?: string;
168
+ };
169
+ /** Class name */
170
+ className?: string;
171
+ }
172
+ declare function DetailViewStats({ formatNumber, labels, className, }: DetailViewStatsProps): ReactElement;
173
+ /**
174
+ * Music bar for detail view
175
+ */
176
+ interface DetailViewMusicProps {
177
+ /** Music info */
178
+ music?: MusicInfo;
179
+ /** Whether music is playing */
180
+ isPlaying?: boolean;
181
+ /** Click handler */
182
+ onClick?: () => void;
183
+ /** Class name */
184
+ className?: string;
185
+ }
186
+ declare function DetailViewMusic({ music, isPlaying, onClick, className, }: DetailViewMusicProps): ReactElement | null;
187
+ /**
188
+ * Bottom bar container
189
+ *
190
+ * Use this to wrap custom bottom bar content.
191
+ * Handles event propagation and styling.
192
+ *
193
+ * @example
194
+ * ```tsx
195
+ * <DetailViewHeadless
196
+ * renderBottomBar={() => (
197
+ * <DetailViewBottomBar>
198
+ * <DetailViewCommentInput />
199
+ * <DetailViewActions>
200
+ * <DetailViewActionButton type="like" />
201
+ * <DetailViewActionButton type="share" />
202
+ * </DetailViewActions>
203
+ * </DetailViewBottomBar>
204
+ * )}
205
+ * />
206
+ * ```
207
+ */
208
+ interface DetailViewBottomBarProps {
209
+ /** Children */
210
+ children?: ReactNode;
211
+ /** Class name */
212
+ className?: string;
213
+ }
214
+ declare function DetailViewBottomBar({ children, className, }: DetailViewBottomBarProps): ReactElement;
215
+ /**
216
+ * Comment input for bottom bar
217
+ *
218
+ * @example
219
+ * ```tsx
220
+ * <DetailViewCommentInput
221
+ * placeholder="Write a comment..."
222
+ * onSubmit={(value) => submitComment(value)}
223
+ * />
224
+ * ```
225
+ */
226
+ interface DetailViewCommentInputProps {
227
+ /** Placeholder text */
228
+ placeholder?: string;
229
+ /** Called when user submits comment (presses Enter) */
230
+ onSubmit?: (value: string) => void;
231
+ /** Class name */
232
+ className?: string;
233
+ }
234
+ declare function DetailViewCommentInput({ placeholder, onSubmit, className, }: DetailViewCommentInputProps): ReactElement;
235
+ /**
236
+ * Actions container for bottom bar
237
+ *
238
+ * @example
239
+ * ```tsx
240
+ * <DetailViewActions>
241
+ * <DetailViewActionButton type="like" />
242
+ * <DetailViewActionButton type="comment" />
243
+ * </DetailViewActions>
244
+ * ```
245
+ */
246
+ interface DetailViewActionsProps {
247
+ /** Children (action buttons) */
248
+ children?: ReactNode;
249
+ /** Class name */
250
+ className?: string;
251
+ }
252
+ declare function DetailViewActions({ children, className }: DetailViewActionsProps): ReactElement;
253
+ /**
254
+ * Action button types
255
+ */
256
+ type DetailViewActionType = 'like' | 'comment' | 'bookmark' | 'share';
257
+ /**
258
+ * Single action button for bottom bar
259
+ *
260
+ * Can be used with predefined types (like, comment, bookmark, share)
261
+ * or with custom icon/content.
262
+ *
263
+ * @example
264
+ * ```tsx
265
+ * // Using predefined type
266
+ * <DetailViewActionButton type="like" isActive={isLiked} count={123} onClick={handleLike} />
267
+ *
268
+ * // Using custom icon
269
+ * <DetailViewActionButton icon={<MyCustomIcon />} onClick={handleCustomAction} />
270
+ * ```
271
+ */
272
+ interface DetailViewActionButtonProps {
273
+ /** Predefined action type */
274
+ type?: DetailViewActionType;
275
+ /** Custom icon (overrides type) */
276
+ icon?: ReactNode;
277
+ /** Whether button is active (e.g., liked, bookmarked) */
278
+ isActive?: boolean;
279
+ /** Count to display */
280
+ count?: number;
281
+ /** Click handler */
282
+ onClick?: () => void;
283
+ /** Aria label */
284
+ ariaLabel?: string;
285
+ /** Class name */
286
+ className?: string;
287
+ /** Children (additional content) */
288
+ children?: ReactNode;
289
+ }
290
+ declare function DetailViewActionButton({ type, icon, isActive, count, onClick, ariaLabel, className, children, }: DetailViewActionButtonProps): ReactElement;
291
+ /**
292
+ * Export icons for host apps that want to build custom bottom bars
293
+ * but still use consistent icons.
294
+ */
295
+ declare const DetailViewIcons: {
296
+ Back: typeof BackIcon;
297
+ Heart: typeof HeartIcon;
298
+ Comment: typeof CommentIcon;
299
+ Bookmark: typeof BookmarkIcon;
300
+ Share: typeof ShareIcon;
301
+ };
302
+
303
+ /**
304
+ * DetailView CSS - Injected at runtime
305
+ *
306
+ * Features:
307
+ * - Full-screen detail view
308
+ * - Expand/collapse animation
309
+ * - Scrollable content
310
+ * - Back gesture (swipe down)
311
+ */
312
+ declare const DETAIL_VIEW_CSS = "\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n DETAIL VIEW - Container\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view {\n position: fixed;\n inset: 0;\n z-index: var(--sv-detail-view-z-index, 1000);\n background: var(--sv-detail-view-bg, #000);\n overflow: hidden;\n touch-action: none;\n}\n\n/* States */\n.sv-detail-view--entering {\n animation: sv-detail-view-enter 0.3s cubic-bezier(0.32, 0.72, 0, 1) forwards;\n}\n\n.sv-detail-view--exiting {\n animation: sv-detail-view-exit 0.3s cubic-bezier(0.32, 0.72, 0, 1) forwards;\n}\n\n.sv-detail-view--dragging {\n transition: none;\n}\n\n@keyframes sv-detail-view-enter {\n from {\n opacity: 0;\n transform: translateY(100%) scale(0.9);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@keyframes sv-detail-view-exit {\n from {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n to {\n opacity: 0;\n transform: translateY(100%) scale(0.9);\n }\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n BACKDROP\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__backdrop {\n position: absolute;\n inset: 0;\n background: var(--sv-detail-view-backdrop-bg, rgba(0, 0, 0, 0.8));\n opacity: 1;\n transition: opacity 0.3s ease;\n}\n\n.sv-detail-view--exiting .sv-detail-view__backdrop {\n opacity: 0;\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n CONTENT CONTAINER\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__container {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n will-change: transform;\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n HEADER\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__header {\n position: sticky;\n top: 0;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--sv-detail-view-header-padding, 12px 16px);\n background: var(--sv-detail-view-header-bg, rgba(0, 0, 0, 0.8));\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n}\n\n.sv-detail-view__back-button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--sv-detail-view-back-bg, rgba(255, 255, 255, 0.1));\n color: var(--sv-detail-view-back-color, #fff);\n cursor: pointer;\n transition: background 0.2s ease;\n}\n\n.sv-detail-view__back-button:hover {\n background: var(--sv-detail-view-back-hover-bg, rgba(255, 255, 255, 0.2));\n}\n\n.sv-detail-view__back-button svg {\n width: 24px;\n height: 24px;\n}\n\n.sv-detail-view__title {\n font-size: var(--sv-detail-view-title-font-size, 16px);\n font-weight: 600;\n color: var(--sv-detail-view-title-color, #fff);\n}\n\n.sv-detail-view__actions {\n display: flex;\n gap: 8px;\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n DRAG INDICATOR\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__drag-indicator {\n position: absolute;\n top: 8px;\n left: 50%;\n transform: translateX(-50%);\n width: 36px;\n height: 5px;\n background: var(--sv-detail-view-drag-indicator-bg, rgba(255, 255, 255, 0.3));\n border-radius: 3px;\n z-index: 20;\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n SCROLL CONTENT\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__scroll {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n -webkit-overflow-scrolling: touch;\n overscroll-behavior-y: contain;\n /* Allow vertical scroll but block horizontal (prevents Feed swipe) */\n touch-action: pan-y;\n /* Padding bottom for bottom bar */\n padding-bottom: 70px;\n}\n\n.sv-detail-view__content {\n min-height: 100%;\n /* No horizontal padding - gallery should be full width */\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n IMAGE GALLERY\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__gallery {\n position: relative;\n width: 100%;\n aspect-ratio: 1;\n background: var(--sv-detail-view-gallery-bg, #111);\n overflow: hidden;\n}\n\n.sv-detail-view__gallery-image {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n/* Gallery pagination */\n.sv-detail-view__gallery-pagination {\n position: absolute;\n bottom: 12px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 6px;\n padding: 6px 12px;\n background: rgba(0, 0, 0, 0.5);\n border-radius: 20px;\n}\n\n.sv-detail-view__gallery-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.5);\n border: none;\n padding: 0;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n\n.sv-detail-view__gallery-dot--active {\n background: #fff;\n transform: scale(1.2);\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n AUTHOR SECTION\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__author {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 16px 0;\n border-bottom: 1px solid var(--sv-detail-view-border-color, rgba(255, 255, 255, 0.1));\n}\n\n.sv-detail-view__author-avatar {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n object-fit: cover;\n}\n\n.sv-detail-view__author-info {\n flex: 1;\n}\n\n.sv-detail-view__author-name {\n font-size: 15px;\n font-weight: 600;\n color: var(--sv-detail-view-text-color, #fff);\n}\n\n.sv-detail-view__author-date {\n font-size: 13px;\n color: var(--sv-detail-view-secondary-color, rgba(255, 255, 255, 0.6));\n margin-top: 2px;\n}\n\n.sv-detail-view__follow-button {\n padding: 8px 20px;\n background: var(--sv-detail-view-follow-bg, #fe2c55);\n border: none;\n border-radius: 4px;\n font-size: 14px;\n font-weight: 600;\n color: #fff;\n cursor: pointer;\n transition: opacity 0.2s ease;\n}\n\n.sv-detail-view__follow-button:hover {\n opacity: 0.9;\n}\n\n.sv-detail-view__follow-button--following {\n background: var(--sv-detail-view-following-bg, rgba(255, 255, 255, 0.1));\n color: var(--sv-detail-view-following-color, #fff);\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n CAPTION SECTION\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__caption {\n padding: 16px;\n font-size: var(--sv-detail-view-caption-font-size, 15px);\n line-height: 1.5;\n color: var(--sv-detail-view-text-color, #fff);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* Hashtags in caption */\n.sv-detail-view__hashtag {\n color: var(--sv-detail-view-hashtag-color, #4dabff);\n cursor: pointer;\n}\n\n.sv-detail-view__hashtag:hover {\n text-decoration: underline;\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n STATS SECTION\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__stats {\n display: flex;\n gap: 24px;\n padding: 16px 0;\n border-top: 1px solid var(--sv-detail-view-border-color, rgba(255, 255, 255, 0.1));\n}\n\n.sv-detail-view__stat {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n color: var(--sv-detail-view-secondary-color, rgba(255, 255, 255, 0.6));\n}\n\n.sv-detail-view__stat-value {\n font-weight: 600;\n color: var(--sv-detail-view-text-color, #fff);\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n ACTION BAR (Bottom)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__action-bar {\n position: sticky;\n bottom: 0;\n display: flex;\n justify-content: space-around;\n padding: 12px 16px;\n background: var(--sv-detail-view-action-bar-bg, rgba(0, 0, 0, 0.9));\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n border-top: 1px solid var(--sv-detail-view-border-color, rgba(255, 255, 255, 0.1));\n}\n\n.sv-detail-view__action {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n padding: 8px 16px;\n border: none;\n background: transparent;\n color: var(--sv-detail-view-action-color, #fff);\n cursor: pointer;\n transition: opacity 0.2s ease;\n}\n\n.sv-detail-view__action:hover {\n opacity: 0.8;\n}\n\n.sv-detail-view__action--active {\n color: var(--sv-detail-view-action-active-color, #fe2c55);\n}\n\n.sv-detail-view__action svg {\n width: 24px;\n height: 24px;\n}\n\n.sv-detail-view__action-label {\n font-size: 12px;\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n MUSIC BAR\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__music {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px;\n margin: 0 16px 16px;\n background: var(--sv-detail-view-music-bg, rgba(255, 255, 255, 0.05));\n border-radius: 8px;\n border: none;\n width: calc(100% - 32px);\n cursor: pointer;\n text-align: left;\n}\n\n.sv-detail-view__music-cover {\n width: 48px;\n height: 48px;\n border-radius: 4px;\n object-fit: cover;\n}\n\n.sv-detail-view__music-info {\n flex: 1;\n overflow: hidden;\n}\n\n.sv-detail-view__music-title {\n font-size: 14px;\n font-weight: 500;\n color: var(--sv-detail-view-text-color, #fff);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sv-detail-view__music-artist {\n font-size: 12px;\n color: var(--sv-detail-view-secondary-color, rgba(255, 255, 255, 0.6));\n margin-top: 2px;\n}\n\n.sv-detail-view__music-icon {\n width: 24px;\n height: 24px;\n color: var(--sv-detail-view-secondary-color, rgba(255, 255, 255, 0.6));\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n HEADER - Author Info (Plan Layout)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__header-author {\n display: flex;\n align-items: center;\n gap: 10px;\n flex: 1;\n min-width: 0;\n margin-left: 12px;\n}\n\n.sv-detail-view__header-avatar {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n object-fit: cover;\n flex-shrink: 0;\n}\n\n.sv-detail-view__header-name {\n font-size: 15px;\n font-weight: 600;\n color: var(--sv-detail-view-text-color, #fff);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sv-detail-view__follow-btn {\n padding: 6px 16px;\n font-size: 13px;\n font-weight: 600;\n border: none;\n border-radius: 4px;\n background: var(--sv-detail-view-follow-bg, #fe2c55);\n color: #fff;\n cursor: pointer;\n transition: background 0.2s ease, opacity 0.2s ease;\n flex-shrink: 0;\n}\n\n.sv-detail-view__follow-btn:hover {\n opacity: 0.9;\n}\n\n.sv-detail-view__follow-btn--following {\n background: var(--sv-detail-view-following-bg, rgba(255, 255, 255, 0.12));\n color: var(--sv-detail-view-text-color, #fff);\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n BOTTOM ACTION BAR (Plan Layout)\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view__bottom-bar {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n padding-bottom: max(10px, env(safe-area-inset-bottom));\n background: var(--sv-detail-view-bottom-bar-bg, rgba(0, 0, 0, 0.95));\n border-top: 1px solid var(--sv-detail-view-border-color, rgba(255, 255, 255, 0.1));\n z-index: 20;\n}\n\n.sv-detail-view__comment-input {\n flex: 1;\n display: flex;\n align-items: center;\n background: var(--sv-detail-view-input-bg, rgba(255, 255, 255, 0.08));\n border-radius: 18px;\n min-width: 0;\n overflow: hidden;\n}\n\n.sv-detail-view__bottom-bar .sv-detail-view__comment-input input,\n.sv-detail-view__comment-field,\ninput.sv-detail-view__comment-field {\n flex: 1;\n width: 100%;\n height: auto;\n padding: 10px 14px;\n margin: 0;\n border: none;\n border-radius: 0;\n background: transparent;\n background-color: transparent;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.4;\n color: var(--sv-detail-view-text-color, #fff);\n outline: none;\n box-shadow: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.sv-detail-view__bottom-bar .sv-detail-view__comment-input input::placeholder,\n.sv-detail-view__comment-field::placeholder,\ninput.sv-detail-view__comment-field::placeholder {\n color: var(--sv-detail-view-placeholder-color, rgba(255, 255, 255, 0.5));\n opacity: 1;\n}\n\n.sv-detail-view__bottom-bar .sv-detail-view__comment-input input:focus,\n.sv-detail-view__comment-field:focus,\ninput.sv-detail-view__comment-field:focus {\n outline: none;\n box-shadow: none;\n border: none;\n}\n\n.sv-detail-view__bottom-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-shrink: 0;\n}\n\n.sv-detail-view__action-btn {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n min-width: 40px;\n padding: 4px 6px;\n background: none;\n border: none;\n color: var(--sv-detail-view-text-color, #fff);\n cursor: pointer;\n transition: transform 0.15s ease, color 0.15s ease;\n}\n\n.sv-detail-view__action-btn:hover {\n transform: scale(1.05);\n}\n\n.sv-detail-view__action-btn:active {\n transform: scale(0.95);\n}\n\n.sv-detail-view__action-btn--active {\n color: var(--sv-detail-view-active-color, #fe2c55);\n}\n\n.sv-detail-view__action-btn svg {\n width: 22px;\n height: 22px;\n}\n\n.sv-detail-view__action-count {\n font-size: 11px;\n margin-top: 2px;\n color: var(--sv-detail-view-secondary-color, rgba(255, 255, 255, 0.7));\n}\n\n.sv-detail-view__action-btn--active .sv-detail-view__action-count {\n color: var(--sv-detail-view-active-color, #fe2c55);\n}\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n ACCESSIBILITY\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-detail-view:focus-visible {\n outline: 2px solid var(--sv-detail-view-focus-color, #4dabff);\n outline-offset: -2px;\n}\n\n.sv-detail-view__sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n";
313
+
314
+ export { DETAIL_VIEW_CSS, DetailViewActionButton, type DetailViewActionButtonProps, type DetailViewActionType, DetailViewActions, type DetailViewActionsProps, DetailViewAuthor, type DetailViewAuthorProps, DetailViewBottomBar, type DetailViewBottomBarProps, DetailViewCaption, type DetailViewCaptionProps, DetailViewCommentInput, type DetailViewCommentInputProps, type DetailViewContextValue, DetailViewGallery, type DetailViewGalleryProps, DetailViewHeadless, type DetailViewHeadlessProps, DetailViewIcons, DetailViewMusic, type DetailViewMusicProps, DetailViewStats, type DetailViewStatsProps, formatCount as formatDetailViewCount, useDetailViewContext };
@@ -0,0 +1 @@
1
+ export { DETAIL_VIEW_CSS, DetailViewActionButton, DetailViewActions, DetailViewAuthor, DetailViewBottomBar, DetailViewCaption, DetailViewCommentInput, DetailViewGallery, DetailViewHeadless, DetailViewIcons, DetailViewMusic, DetailViewStats, formatCount as formatDetailViewCount, useDetailViewContext } from '../../chunk-BADA7OLG.js';
@@ -1 +1 @@
1
- export { ErrorBoundary } from '../../chunk-SZXFH334.js';
1
+ export { ErrorBoundary } from '../../chunk-IWSBYOSS.js';
@@ -1,16 +1,16 @@
1
- import { UIFeedState, UISwipeState, VideoItem } from '@xhub-short/contracts';
1
+ import { UIFeedState, UISwipeState, ContentItem, VideoItem } from '@xhub-short/contracts';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
4
 
5
5
  /**
6
- * VideoFeedHeadless Component
6
+ * FeedHeadless Component
7
7
  *
8
- * Position-based virtual scroller for video feed (TikTok-style).
8
+ * Position-based virtual scroller for mixed content feed (Videos, Articles, etc.).
9
9
  * Receives state via props (headless pattern).
10
10
  *
11
- * This component wraps VirtualSlider<VideoItem> and adds:
12
- * - Video-specific context (VideoFeedContext)
13
- * - Video-specific CSS classes (sv-video-feed prefix)
11
+ * This component wraps VirtualSlider<ContentItem> and adds:
12
+ * - Feed-specific context (FeedContext)
13
+ * - Feed-specific CSS classes (sv-feed prefix)
14
14
  * - Backwards-compatible API
15
15
  *
16
16
  * Features:
@@ -22,19 +22,19 @@ import { ReactNode } from 'react';
22
22
  *
23
23
  * @example
24
24
  * ```tsx
25
- * <VideoFeedHeadless
25
+ * <FeedHeadless
26
26
  * feedState={feedState}
27
27
  * swipeState={swipeState}
28
- * renderSlot={(video, index) => <VideoSlot video={video} />}
28
+ * renderSlot={(item, index) => <ArticleSlot item={item} />}
29
29
  * onEndReached={() => feedState.loadMore()}
30
30
  * />
31
31
  * ```
32
32
  */
33
33
 
34
34
  /**
35
- * Props for VideoFeedHeadless
35
+ * Props for FeedHeadless
36
36
  */
37
- interface VideoFeedHeadlessProps {
37
+ interface FeedHeadlessProps {
38
38
  /** Feed state (videos, loading, etc.) */
39
39
  feedState: UIFeedState;
40
40
  /** Swipe/scroll state */
@@ -43,8 +43,8 @@ interface VideoFeedHeadlessProps {
43
43
  height?: number | string;
44
44
  /** Additional CSS classes */
45
45
  className?: string;
46
- /** Render function for each video slot */
47
- renderSlot: (video: VideoItem, index: number) => ReactNode;
46
+ /** Render function for each content slot */
47
+ renderSlot: (item: ContentItem, index: number, isActive: boolean) => ReactNode;
48
48
  /** Called when active index changes */
49
49
  onIndexChange?: (index: number) => void;
50
50
  /** Called when reaching near end of feed */
@@ -62,8 +62,8 @@ interface VideoFeedHeadlessProps {
62
62
  /**
63
63
  * Disable inline transform styles on slots.
64
64
  *
65
- * When true, VideoFeedHeadless will NOT apply inline `transform` styles
66
- * to slot elements. This allows parent components (like VideoFeed wired)
65
+ * When true, FeedHeadless will NOT apply inline `transform` styles
66
+ * to slot elements. This allows parent components (like Feed wired)
67
67
  * to control transforms entirely via direct DOM manipulation.
68
68
  *
69
69
  * Use this when implementing 60fps swipe with ref-based transforms.
@@ -71,26 +71,45 @@ interface VideoFeedHeadlessProps {
71
71
  * @default false
72
72
  */
73
73
  disableInlineTransforms?: boolean;
74
+ /**
75
+ * Custom empty state component.
76
+ * Receives `text` prop (localized empty text).
77
+ */
78
+ renderEmpty?: (text: string) => ReactNode;
79
+ /**
80
+ * Custom loading state component.
81
+ * Receives `text` prop (localized loading text).
82
+ */
83
+ renderLoading?: (text: string) => ReactNode;
84
+ /**
85
+ * Custom end of feed component.
86
+ * Receives `text` prop (localized end text).
87
+ */
88
+ renderEnd?: (text: string) => ReactNode;
74
89
  }
90
+ /** @deprecated Use FeedHeadlessProps instead. Will be removed in v3.0 */
91
+ type VideoFeedHeadlessProps = FeedHeadlessProps;
75
92
  /**
76
- * VideoFeed Headless Component
93
+ * Feed Headless Component
77
94
  *
78
- * Renders a vertical video feed with virtual scrolling.
79
- * Position-based: each video takes full viewport height.
95
+ * Renders a vertical feed with virtual scrolling.
96
+ * Position-based: each item takes full viewport height.
80
97
  *
81
- * Internally uses VirtualSlider<VideoItem> for the core slider logic,
82
- * adding video-specific context and styling.
98
+ * Internally uses VirtualSlider<ContentItem> for the core slider logic,
99
+ * adding feed-specific context and styling.
83
100
  */
84
- declare function VideoFeedHeadless({ feedState, swipeState, height, className, renderSlot, onIndexChange, onEndReached, endReachedThreshold, bufferSize, loadingText, emptyText, endText, disableInlineTransforms, }: VideoFeedHeadlessProps): React.ReactElement;
85
- declare namespace VideoFeedHeadless {
101
+ declare function FeedHeadless({ feedState, swipeState, height, className, renderSlot, onIndexChange, onEndReached, endReachedThreshold, bufferSize, renderEmpty, renderLoading, renderEnd, loadingText, emptyText, endText, disableInlineTransforms, }: FeedHeadlessProps): React.ReactElement;
102
+ declare namespace FeedHeadless {
86
103
  var displayName: string;
87
104
  }
88
105
 
89
106
  /**
90
- * VideoFeed context value
107
+ * Feed context value
91
108
  */
92
- interface VideoFeedContextValue {
93
- /** List of videos */
109
+ interface FeedContextValue {
110
+ /** List of items */
111
+ items: ContentItem[];
112
+ /** @deprecated Use items instead. Will be removed in v3.0 */
94
113
  videos: VideoItem[];
95
114
  /** Current active index */
96
115
  activeIndex: number;
@@ -102,28 +121,28 @@ interface VideoFeedContextValue {
102
121
  containerHeight: number;
103
122
  /** Go to specific index */
104
123
  goToIndex: (index: number) => void;
105
- /** Total video count */
124
+ /** Total item count */
106
125
  totalCount: number;
107
126
  }
108
127
  /**
109
- * VideoFeed context (internal use)
128
+ * Feed context (internal use)
110
129
  */
111
- declare const VideoFeedContext: react.Context<VideoFeedContextValue | null>;
130
+ declare const FeedContext: react.Context<FeedContextValue | null>;
112
131
  /**
113
- * Hook to access VideoFeed context
132
+ * Hook to access Feed context
114
133
  *
115
- * @throws Error if used outside VideoFeed
134
+ * @throws Error if used outside Feed
116
135
  */
117
- declare function useVideoFeedContext(): VideoFeedContextValue;
136
+ declare function useFeedContext(): FeedContextValue;
118
137
  /**
119
- * Hook to check if inside VideoFeed (doesn't throw)
138
+ * Hook to check if inside Feed (doesn't throw)
120
139
  */
121
- declare function useOptionalVideoFeedContext(): VideoFeedContextValue | null;
140
+ declare function useOptionalFeedContext(): FeedContextValue | null;
122
141
 
123
142
  /**
124
- * useVideoFeedPosition Hook
143
+ * useFeedPosition Hook
125
144
  *
126
- * Calculates position and visibility for video slots in the feed.
145
+ * Calculates position and visibility for content slots in the feed.
127
146
  * Uses position-based virtual scrolling (TikTok-style).
128
147
  *
129
148
  * Architecture:
@@ -165,8 +184,8 @@ interface SlotPosition {
165
184
  /** Opacity for transition effect */
166
185
  opacity: number;
167
186
  }
168
- interface UseVideoFeedPositionOptions {
169
- /** Total number of videos */
187
+ interface UseFeedPositionOptions {
188
+ /** Total number of items */
170
189
  totalCount: number;
171
190
  /** Current active index */
172
191
  activeIndex: number;
@@ -184,7 +203,7 @@ interface UseVideoFeedPositionOptions {
184
203
  /** Number of slots to render before/after active (default: 1) */
185
204
  bufferSize?: number;
186
205
  }
187
- interface UseVideoFeedPositionReturn {
206
+ interface UseFeedPositionReturn {
188
207
  /** Positions for all slots that should render */
189
208
  slots: SlotPosition[];
190
209
  /** Current scroll offset (for container transform) */
@@ -211,8 +230,8 @@ interface UseVideoFeedPositionReturn {
211
230
  *
212
231
  * @example
213
232
  * ```tsx
214
- * const { slots, isAtEnd } = useVideoFeedPosition({
215
- * totalCount: videos.length,
233
+ * const { slots, isAtEnd } = useFeedPosition({
234
+ * totalCount: items.length,
216
235
  * activeIndex,
217
236
  * containerHeight: 800,
218
237
  * dragOffset,
@@ -225,7 +244,7 @@ interface UseVideoFeedPositionReturn {
225
244
  * });
226
245
  * ```
227
246
  */
228
- declare function useVideoFeedPosition(options: UseVideoFeedPositionOptions): UseVideoFeedPositionReturn;
247
+ declare function useFeedPosition(options: UseFeedPositionOptions): UseFeedPositionReturn;
229
248
  /**
230
249
  * Calculate the slot index from a Y position
231
250
  * Useful for snap-to-slot logic
@@ -238,9 +257,9 @@ declare function useVideoFeedPosition(options: UseVideoFeedPositionOptions): Use
238
257
  declare function getSlotIndexFromPosition(yPosition: number, containerHeight: number, totalCount: number): number;
239
258
 
240
259
  /**
241
- * VideoFeed CSS
260
+ * Feed CSS
242
261
  */
243
- declare const VIDEO_FEED_CSS = "\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n * VideoFeed Styles\n * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-video-feed {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: var(--sv-bg-primary, #000);\n touch-action: pan-y;\n user-select: none;\n -webkit-user-select: none;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Feed Container (holds all slots)\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-video-feed__container {\n position: relative;\n width: 100%;\n height: 100%;\n will-change: transform;\n}\n\n/* Smooth transition when not swiping */\n.sv-video-feed__container--snapping {\n transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n}\n\n/* No transition during swipe for immediate feedback */\n.sv-video-feed__container--swiping {\n transition: none;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Video Slot Wrapper\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-video-feed__slot {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n will-change: transform, opacity;\n backface-visibility: hidden;\n -webkit-backface-visibility: hidden;\n}\n\n/* Active slot */\n.sv-video-feed__slot--active {\n z-index: 2;\n}\n\n/* Adjacent slots (for preview) */\n.sv-video-feed__slot--adjacent {\n z-index: 1;\n pointer-events: none;\n}\n\n/* Transition for smooth snap animations (when not swiping) */\n.sv-video-feed__slot--transitioning {\n transition: transform 0.175s cubic-bezier(0.25, 0.46, 0.45, 0.94),\n opacity 0.2s ease;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Loading State\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-video-feed__loading {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--sv-spacing-md, 16px);\n color: var(--sv-text-secondary, #999);\n}\n\n.sv-video-feed__loading-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid rgba(255, 255, 255, 0.2);\n border-top-color: var(--sv-color-primary, #fe2c55);\n border-radius: 50%;\n animation: sv-feed-spinner 0.8s linear infinite;\n}\n\n@keyframes sv-feed-spinner {\n to {\n transform: rotate(360deg);\n }\n}\n\n.sv-video-feed__loading-text {\n font-size: var(--sv-font-size-sm, 13px);\n font-family: var(--sv-font-family, 'Urbanist', sans-serif);\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Empty State\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-video-feed__empty {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n color: var(--sv-text-secondary, #999);\n font-family: var(--sv-font-family, 'Urbanist', sans-serif);\n}\n\n.sv-video-feed__empty-icon {\n font-size: 48px;\n margin-bottom: var(--sv-spacing-md, 16px);\n}\n\n.sv-video-feed__empty-text {\n font-size: var(--sv-font-size-md, 14px);\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * End of Feed\n * Note: Positioned outside container but above slots. Uses safe-area-inset\n * to avoid being covered by device notch/home indicator.\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-video-feed__end {\n position: absolute;\n /* Safe bottom positioning - accounts for device UI */\n bottom: calc(var(--sv-spacing-xl, 32px) + env(safe-area-inset-bottom, 0px));\n left: 50%;\n transform: translateX(-50%);\n z-index: 10; /* Above slots (z-index 1-2) */\n padding: var(--sv-spacing-sm, 8px) var(--sv-spacing-md, 16px);\n background: rgba(0, 0, 0, 0.7);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: var(--sv-border-radius-md, 8px);\n color: var(--sv-text-secondary, #999);\n font-size: var(--sv-font-size-sm, 13px);\n font-family: var(--sv-font-family, 'Urbanist', sans-serif);\n pointer-events: none; /* Don't block interactions */\n animation: sv-feed-end-fade-in 0.3s ease-out;\n}\n\n@keyframes sv-feed-end-fade-in {\n from {\n opacity: 0;\n transform: translateX(-50%) translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Reduced Motion\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n@media (prefers-reduced-motion: reduce) {\n .sv-video-feed__container--snapping {\n transition: none;\n }\n \n .sv-video-feed__loading-spinner {\n animation: none;\n }\n}\n";
262
+ declare const FEED_CSS = "\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n * Feed Styles\n * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.sv-feed {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: var(--sv-bg-primary, #000);\n touch-action: pan-y;\n user-select: none;\n -webkit-user-select: none;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Feed Container (holds all slots)\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-feed__container {\n position: relative;\n width: 100%;\n height: 100%;\n will-change: transform;\n}\n\n/* Smooth transition when not swiping */\n.sv-feed__container--snapping {\n transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n}\n\n/* No transition during swipe for immediate feedback */\n.sv-feed__container--swiping {\n transition: none;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Content Slot Wrapper\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-feed__slot {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n will-change: transform, opacity;\n backface-visibility: hidden;\n -webkit-backface-visibility: hidden;\n}\n\n/* Active slot */\n.sv-feed__slot--active {\n z-index: 2;\n}\n\n/* Adjacent slots (for preview) */\n.sv-feed__slot--adjacent {\n z-index: 1;\n pointer-events: none;\n}\n\n/* Transition for smooth snap animations (when not swiping) */\n.sv-feed__slot--transitioning {\n transition: transform 0.175s cubic-bezier(0.25, 0.46, 0.45, 0.94),\n opacity 0.2s ease;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Loading State\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-feed__loading {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--sv-spacing-md, 16px);\n color: var(--sv-text-secondary, #999);\n}\n\n.sv-feed__loading-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid rgba(255, 255, 255, 0.2);\n border-top-color: var(--sv-color-primary, #fe2c55);\n border-radius: 50%;\n animation: sv-feed-spinner 0.8s linear infinite;\n}\n\n@keyframes sv-feed-spinner {\n to {\n transform: rotate(360deg);\n }\n}\n\n.sv-feed__loading-text {\n font-size: var(--sv-font-size-sm, 13px);\n font-family: var(--sv-font-family, 'Urbanist', sans-serif);\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Empty State\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-feed__empty {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n color: var(--sv-text-secondary, #999);\n font-family: var(--sv-font-family, 'Urbanist', sans-serif);\n}\n\n.sv-feed__empty-icon {\n font-size: 48px;\n margin-bottom: var(--sv-spacing-md, 16px);\n}\n\n.sv-feed__empty-text {\n font-size: var(--sv-font-size-md, 14px);\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * End of Feed\n * Note: Positioned outside container but above slots. Uses safe-area-inset\n * to avoid being covered by device notch/home indicator.\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sv-feed__end {\n position: absolute;\n /* Safe bottom positioning - accounts for device UI */\n bottom: calc(var(--sv-spacing-xl, 32px) + env(safe-area-inset-bottom, 0px));\n left: 50%;\n transform: translateX(-50%);\n z-index: 10; /* Above slots (z-index 1-2) */\n padding: var(--sv-spacing-sm, 8px) var(--sv-spacing-md, 16px);\n background: rgba(0, 0, 0, 0.7);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: var(--sv-border-radius-md, 8px);\n color: var(--sv-text-secondary, #999);\n font-size: var(--sv-font-size-sm, 13px);\n font-family: var(--sv-font-family, 'Urbanist', sans-serif);\n pointer-events: none; /* Don't block interactions */\n animation: sv-feed-end-fade-in 0.3s ease-out;\n}\n\n@keyframes sv-feed-end-fade-in {\n from {\n opacity: 0;\n transform: translateX(-50%) translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Reduced Motion\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n@media (prefers-reduced-motion: reduce) {\n .sv-feed__container--snapping {\n transition: none;\n }\n \n .sv-feed__loading-spinner {\n animation: none;\n }\n}\n";
244
263
 
245
264
  /**
246
265
  * VideoFeed Constants
@@ -278,8 +297,8 @@ declare const SLOT_INDEX_DATASET_KEY = "slotIndex";
278
297
  */
279
298
  declare const SLOT_ACTIVE_DATASET_KEY = "slotActive";
280
299
  /**
281
- * CSS class prefix for VideoFeed components.
300
+ * CSS class prefix for Feed components.
282
301
  */
283
- declare const VIDEO_FEED_CLASS_PREFIX = "sv-video-feed";
302
+ declare const FEED_CLASS_PREFIX = "sv-feed";
284
303
 
285
- export { SLOT_ACTIVE_ATTR, SLOT_ACTIVE_DATASET_KEY, SLOT_INDEX_ATTR, SLOT_INDEX_DATASET_KEY, type SlotPosition, type UseVideoFeedPositionOptions, type UseVideoFeedPositionReturn, VIDEO_FEED_CLASS_PREFIX, VIDEO_FEED_CSS, VideoFeedContext, type VideoFeedContextValue, VideoFeedHeadless, type VideoFeedHeadlessProps, getSlotIndexFromPosition, useOptionalVideoFeedContext, useVideoFeedContext, useVideoFeedPosition };
304
+ export { FEED_CLASS_PREFIX, FEED_CSS, FeedContext, type FeedContextValue, FeedHeadless, type FeedHeadlessProps, SLOT_ACTIVE_ATTR, SLOT_ACTIVE_DATASET_KEY, SLOT_INDEX_ATTR, SLOT_INDEX_DATASET_KEY, type SlotPosition, type UseFeedPositionOptions, type UseFeedPositionReturn, FEED_CLASS_PREFIX as VIDEO_FEED_CLASS_PREFIX, FEED_CSS as VIDEO_FEED_CSS, FeedContext as VideoFeedContext, type FeedContextValue as VideoFeedContextValue, FeedHeadless as VideoFeedHeadless, type VideoFeedHeadlessProps, getSlotIndexFromPosition, useFeedContext, useFeedPosition, useOptionalFeedContext, useOptionalFeedContext as useOptionalVideoFeedContext, useFeedContext as useVideoFeedContext, useFeedPosition as useVideoFeedPosition };
@@ -0,0 +1 @@
1
+ export { FEED_CLASS_PREFIX, FEED_CSS, FeedContext, FeedHeadless, SLOT_ACTIVE_ATTR, SLOT_ACTIVE_DATASET_KEY, SLOT_INDEX_ATTR, SLOT_INDEX_DATASET_KEY, FEED_CLASS_PREFIX as VIDEO_FEED_CLASS_PREFIX, FEED_CSS as VIDEO_FEED_CSS, FeedContext as VideoFeedContext, FeedHeadless as VideoFeedHeadless, getSlotIndexFromPosition, useFeedContext, useFeedPosition, useOptionalFeedContext, useOptionalFeedContext as useOptionalVideoFeedContext, useFeedContext as useVideoFeedContext, useFeedPosition as useVideoFeedPosition } from '../../chunk-BEJAJFV6.js';