@mieweb/ui 0.10.0-dev.29 → 0.10.0-dev.30

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 (118) hide show
  1. package/dist/brands/index.cjs +7 -7
  2. package/dist/brands/index.js +2 -2
  3. package/dist/{chunk-JYZW2PZS.cjs → chunk-254QK722.cjs} +7 -7
  4. package/dist/chunk-254QK722.cjs.map +1 -0
  5. package/dist/{chunk-FBLVPM2K.js → chunk-2JICT4A2.js} +26 -10
  6. package/dist/chunk-2JICT4A2.js.map +1 -0
  7. package/dist/{chunk-S45EERW3.cjs → chunk-35Y4ILCU.cjs} +4 -4
  8. package/dist/{chunk-S45EERW3.cjs.map → chunk-35Y4ILCU.cjs.map} +1 -1
  9. package/dist/{chunk-2BARVD4C.js → chunk-3RXZWHCU.js} +3 -3
  10. package/dist/{chunk-2BARVD4C.js.map → chunk-3RXZWHCU.js.map} +1 -1
  11. package/dist/{chunk-MWVW4APN.cjs → chunk-7UTBDFFL.cjs} +4 -4
  12. package/dist/{chunk-MWVW4APN.cjs.map → chunk-7UTBDFFL.cjs.map} +1 -1
  13. package/dist/{chunk-FJ7YPVKV.js → chunk-7X6R3JIA.js} +3 -3
  14. package/dist/{chunk-FJ7YPVKV.js.map → chunk-7X6R3JIA.js.map} +1 -1
  15. package/dist/{chunk-SYRLCTDV.js → chunk-7ZV67VZE.js} +3 -3
  16. package/dist/{chunk-SYRLCTDV.js.map → chunk-7ZV67VZE.js.map} +1 -1
  17. package/dist/chunk-BNH4SXJB.js +874 -0
  18. package/dist/chunk-BNH4SXJB.js.map +1 -0
  19. package/dist/{chunk-N2CJJHHB.cjs → chunk-BUT6MF5K.cjs} +27 -11
  20. package/dist/chunk-BUT6MF5K.cjs.map +1 -0
  21. package/dist/{chunk-6VFDHJSO.cjs → chunk-COM7RNZU.cjs} +2 -2
  22. package/dist/{chunk-6VFDHJSO.cjs.map → chunk-COM7RNZU.cjs.map} +1 -1
  23. package/dist/{chunk-SQHMGT4W.js → chunk-GW4RFJZ5.js} +7 -7
  24. package/dist/chunk-GW4RFJZ5.js.map +1 -0
  25. package/dist/{chunk-JID7AZTO.cjs → chunk-H3ROMJYI.cjs} +4 -4
  26. package/dist/{chunk-JID7AZTO.cjs.map → chunk-H3ROMJYI.cjs.map} +1 -1
  27. package/dist/{chunk-DZFGTWYF.js → chunk-JBIGSP2T.js} +3 -3
  28. package/dist/{chunk-DZFGTWYF.js.map → chunk-JBIGSP2T.js.map} +1 -1
  29. package/dist/{chunk-33DEVA3P.js → chunk-JHG3ELS2.js} +2 -2
  30. package/dist/{chunk-33DEVA3P.js.map → chunk-JHG3ELS2.js.map} +1 -1
  31. package/dist/{chunk-WWJIW2IH.cjs → chunk-JSXIB6EJ.cjs} +4 -4
  32. package/dist/{chunk-WWJIW2IH.cjs.map → chunk-JSXIB6EJ.cjs.map} +1 -1
  33. package/dist/{chunk-H5YXAMVH.js → chunk-MLAWXPRY.js} +44 -2
  34. package/dist/chunk-MLAWXPRY.js.map +1 -0
  35. package/dist/{chunk-AAYXCZMV.cjs → chunk-NKV76R65.cjs} +4 -4
  36. package/dist/{chunk-AAYXCZMV.cjs.map → chunk-NKV76R65.cjs.map} +1 -1
  37. package/dist/{chunk-PYJDVWXM.js → chunk-NUB5HRKS.js} +3 -3
  38. package/dist/{chunk-PYJDVWXM.js.map → chunk-NUB5HRKS.js.map} +1 -1
  39. package/dist/{chunk-LL6IWKI3.js → chunk-OW3SQJ7P.js} +3 -3
  40. package/dist/{chunk-LL6IWKI3.js.map → chunk-OW3SQJ7P.js.map} +1 -1
  41. package/dist/{chunk-OUZDYJ33.cjs → chunk-QURJVPRH.cjs} +44 -2
  42. package/dist/chunk-QURJVPRH.cjs.map +1 -0
  43. package/dist/{chunk-TZJQB4P2.js → chunk-RM2HPQIC.js} +72 -14
  44. package/dist/chunk-RM2HPQIC.js.map +1 -0
  45. package/dist/{chunk-P4VGVJVI.js → chunk-SD7ONW6S.js} +3 -3
  46. package/dist/{chunk-P4VGVJVI.js.map → chunk-SD7ONW6S.js.map} +1 -1
  47. package/dist/chunk-WXFGHDTV.cjs +896 -0
  48. package/dist/chunk-WXFGHDTV.cjs.map +1 -0
  49. package/dist/chunk-XDU65GLW.js +21 -0
  50. package/dist/chunk-XDU65GLW.js.map +1 -0
  51. package/dist/{chunk-MYHV2ND3.cjs → chunk-XRVMYT5G.cjs} +72 -13
  52. package/dist/chunk-XRVMYT5G.cjs.map +1 -0
  53. package/dist/{chunk-I4LXSHKO.cjs → chunk-YRHBSSQ3.cjs} +4 -4
  54. package/dist/{chunk-I4LXSHKO.cjs.map → chunk-YRHBSSQ3.cjs.map} +1 -1
  55. package/dist/chunk-YW2AKBLS.cjs +23 -0
  56. package/dist/chunk-YW2AKBLS.cjs.map +1 -0
  57. package/dist/{chunk-B2X732V3.cjs → chunk-ZTMYKAVU.cjs} +4 -4
  58. package/dist/{chunk-B2X732V3.cjs.map → chunk-ZTMYKAVU.cjs.map} +1 -1
  59. package/dist/components/Collapsible/index.cjs +5 -5
  60. package/dist/components/Collapsible/index.js +2 -2
  61. package/dist/components/MediaEditor/index.cjs +8 -8
  62. package/dist/components/MediaEditor/index.js +3 -3
  63. package/dist/components/MediaFeed/index.cjs +26 -0
  64. package/dist/components/MediaFeed/index.cjs.map +1 -0
  65. package/dist/components/MediaFeed/index.d.cts +104 -0
  66. package/dist/components/MediaFeed/index.d.ts +104 -0
  67. package/dist/components/MediaFeed/index.js +17 -0
  68. package/dist/components/MediaFeed/index.js.map +1 -0
  69. package/dist/components/MediaPlayer/index.cjs +5 -5
  70. package/dist/components/MediaPlayer/index.d.cts +18 -0
  71. package/dist/components/MediaPlayer/index.d.ts +18 -0
  72. package/dist/components/MediaPlayer/index.js +2 -2
  73. package/dist/components/RadialExplorer/index.cjs +3 -3
  74. package/dist/components/RadialExplorer/index.js +2 -2
  75. package/dist/components/SliderCalculator/index.cjs +4 -4
  76. package/dist/components/SliderCalculator/index.js +2 -2
  77. package/dist/components/SuperChat/index.cjs +654 -292
  78. package/dist/components/SuperChat/index.cjs.map +1 -1
  79. package/dist/components/SuperChat/index.d.cts +35 -6
  80. package/dist/components/SuperChat/index.d.ts +35 -6
  81. package/dist/components/SuperChat/index.js +648 -286
  82. package/dist/components/SuperChat/index.js.map +1 -1
  83. package/dist/components/SuperChat/plugins/index.d.cts +2 -2
  84. package/dist/components/SuperChat/plugins/index.d.ts +2 -2
  85. package/dist/components/ThemeProvider/index.cjs +9 -9
  86. package/dist/components/ThemeProvider/index.js +3 -3
  87. package/dist/components/Tooltip/index.cjs +3 -3
  88. package/dist/components/Tooltip/index.js +2 -2
  89. package/dist/esheet.cjs +6 -6
  90. package/dist/esheet.js +3 -3
  91. package/dist/globe.cjs +2 -2
  92. package/dist/globe.js +1 -1
  93. package/dist/hooks/index.cjs +4 -4
  94. package/dist/hooks/index.js +2 -2
  95. package/dist/index.cjs +164 -214
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.d.cts +120 -1
  98. package/dist/index.d.ts +120 -1
  99. package/dist/index.js +25 -82
  100. package/dist/index.js.map +1 -1
  101. package/dist/styles.css +1 -1
  102. package/dist/tailwind-preset.cjs +4 -4
  103. package/dist/tailwind-preset.js +1 -1
  104. package/dist/{types-DOxdTwmq.d.cts → types-Cx1FHLq2.d.cts} +130 -1
  105. package/dist/{types-DOxdTwmq.d.ts → types-Cx1FHLq2.d.ts} +130 -1
  106. package/package.json +1 -1
  107. package/dist/chunk-6HFFWEM3.cjs +0 -22
  108. package/dist/chunk-6HFFWEM3.cjs.map +0 -1
  109. package/dist/chunk-FBLVPM2K.js.map +0 -1
  110. package/dist/chunk-H5YXAMVH.js.map +0 -1
  111. package/dist/chunk-HB7C7NB5.js +0 -20
  112. package/dist/chunk-HB7C7NB5.js.map +0 -1
  113. package/dist/chunk-JYZW2PZS.cjs.map +0 -1
  114. package/dist/chunk-MYHV2ND3.cjs.map +0 -1
  115. package/dist/chunk-N2CJJHHB.cjs.map +0 -1
  116. package/dist/chunk-OUZDYJ33.cjs.map +0 -1
  117. package/dist/chunk-SQHMGT4W.js.map +0 -1
  118. package/dist/chunk-TZJQB4P2.js.map +0 -1
@@ -0,0 +1,874 @@
1
+ import { MediaPlayer } from './chunk-2JICT4A2.js';
2
+ import { Avatar } from './chunk-HZ4MBUC3.js';
3
+ import { Modal, ModalHeader, ModalTitle, ModalClose } from './chunk-ENMXHZNE.js';
4
+ import { usePrefersReducedMotion } from './chunk-XDU65GLW.js';
5
+ import { Button } from './chunk-7YO7DNUV.js';
6
+ import { cn } from './chunk-TQONQSJO.js';
7
+ import * as React2 from 'react';
8
+ import { ChevronUp, ChevronDown, Maximize2, Play, Pause } from 'lucide-react';
9
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
+
11
+ function sourceHref(src) {
12
+ try {
13
+ const url = new URL(src, "https://localhost/");
14
+ return ["http:", "https:"].includes(url.protocol) ? src : void 0;
15
+ } catch {
16
+ return void 0;
17
+ }
18
+ }
19
+ function SourceLink({ src, labels }) {
20
+ const href = sourceHref(src);
21
+ return href ? /* @__PURE__ */ jsx(
22
+ "a",
23
+ {
24
+ href,
25
+ target: "_blank",
26
+ rel: "noopener noreferrer",
27
+ className: "text-primary-700 dark:text-primary-300 focus-visible:ring-ring rounded underline underline-offset-4 focus-visible:ring-2 focus-visible:outline-none",
28
+ children: labels.openSource
29
+ }
30
+ ) : null;
31
+ }
32
+ function NativeFeedMedia({
33
+ media,
34
+ title,
35
+ active,
36
+ autoPlay,
37
+ muted,
38
+ loop,
39
+ labels,
40
+ onActivate,
41
+ playbackPosition,
42
+ playbackRequestId
43
+ }) {
44
+ const mediaElementRef = React2.useRef(null);
45
+ const [state, setState] = React2.useState("idle");
46
+ const [blocked, setBlocked] = React2.useState(false);
47
+ const [retryVersion, setRetryVersion] = React2.useState(0);
48
+ const wantsPlayback = React2.useRef(playbackPosition.playRequested ?? false);
49
+ const latestPlaybackRequest = React2.useRef(playbackRequestId);
50
+ latestPlaybackRequest.current = playbackRequestId;
51
+ const handledPlaybackRequest = React2.useRef(
52
+ void 0
53
+ );
54
+ const ownershipPause = React2.useRef(false);
55
+ const attemptId = React2.useRef(0);
56
+ const activeRef = React2.useRef(active);
57
+ const canSavePosition = React2.useRef(false);
58
+ activeRef.current = active;
59
+ const tryPlay = React2.useCallback(async () => {
60
+ const element = mediaElementRef.current;
61
+ if (!element || !activeRef.current) return;
62
+ const attempt = ++attemptId.current;
63
+ setBlocked(false);
64
+ try {
65
+ await element.play();
66
+ if (!activeRef.current || element !== mediaElementRef.current)
67
+ element.pause();
68
+ } catch (error) {
69
+ if (!activeRef.current || attempt !== attemptId.current) return;
70
+ if (error instanceof Error && error.name === "AbortError") return;
71
+ setBlocked(true);
72
+ }
73
+ }, []);
74
+ const pauseForOwnership = React2.useCallback(
75
+ (element) => {
76
+ if (element && element === mediaElementRef.current && !element.paused)
77
+ ownershipPause.current = true;
78
+ element?.pause();
79
+ },
80
+ []
81
+ );
82
+ React2.useEffect(() => {
83
+ const element = mediaElementRef.current;
84
+ const attempts = attemptId;
85
+ const restorePosition = () => {
86
+ if (!element || !activeRef.current) return;
87
+ if (playbackPosition.timeMs > 0) {
88
+ element.currentTime = playbackPosition.timeMs / 1e3;
89
+ }
90
+ canSavePosition.current = true;
91
+ };
92
+ if (active && element) {
93
+ wantsPlayback.current = playbackPosition.playRequested ?? false;
94
+ if (element.readyState >= 1) restorePosition();
95
+ else element.addEventListener("loadedmetadata", restorePosition);
96
+ }
97
+ if (active && (latestPlaybackRequest.current === void 0 || latestPlaybackRequest.current === handledPlaybackRequest.current) && (autoPlay || wantsPlayback.current)) {
98
+ void tryPlay();
99
+ } else if (!active) {
100
+ attemptId.current++;
101
+ pauseForOwnership(element);
102
+ setBlocked(false);
103
+ }
104
+ return () => {
105
+ attempts.current++;
106
+ element?.removeEventListener("loadedmetadata", restorePosition);
107
+ if (element && canSavePosition.current)
108
+ playbackPosition.timeMs = element.currentTime * 1e3;
109
+ pauseForOwnership(element);
110
+ };
111
+ }, [
112
+ active,
113
+ autoPlay,
114
+ pauseForOwnership,
115
+ playbackPosition,
116
+ retryVersion,
117
+ tryPlay
118
+ ]);
119
+ React2.useEffect(() => {
120
+ if (!active || playbackRequestId === void 0 || playbackRequestId === handledPlaybackRequest.current)
121
+ return;
122
+ handledPlaybackRequest.current = playbackRequestId;
123
+ wantsPlayback.current = true;
124
+ playbackPosition.playRequested = true;
125
+ void tryPlay();
126
+ }, [active, playbackRequestId, playbackPosition, tryPlay]);
127
+ const togglePlayback = () => {
128
+ if (state === "playing") {
129
+ wantsPlayback.current = false;
130
+ playbackPosition.playRequested = false;
131
+ mediaElementRef.current?.pause();
132
+ } else {
133
+ wantsPlayback.current = true;
134
+ playbackPosition.playRequested = true;
135
+ onActivate();
136
+ if (active) void tryPlay();
137
+ }
138
+ };
139
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 w-full flex-col items-center justify-center gap-3", children: [
140
+ /* @__PURE__ */ jsx(
141
+ MediaPlayer,
142
+ {
143
+ src: media.src,
144
+ kind: media.kind === "audio" ? "audio" : "video",
145
+ mediaElementRef,
146
+ muted,
147
+ loop,
148
+ poster: media.poster,
149
+ preload: active ? "metadata" : "none",
150
+ controls: active,
151
+ "aria-label": title,
152
+ onStateChange: (next) => {
153
+ setState(next);
154
+ if (next === "playing") ownershipPause.current = false;
155
+ if (next === "paused") {
156
+ if (activeRef.current && !ownershipPause.current) {
157
+ wantsPlayback.current = false;
158
+ playbackPosition.playRequested = false;
159
+ }
160
+ ownershipPause.current = false;
161
+ }
162
+ },
163
+ onTimeUpdate: (timeMs) => {
164
+ if (canSavePosition.current) playbackPosition.timeMs = timeMs;
165
+ },
166
+ onRetry: () => {
167
+ ownershipPause.current = false;
168
+ canSavePosition.current = false;
169
+ setState("idle");
170
+ setBlocked(false);
171
+ setRetryVersion((version) => version + 1);
172
+ },
173
+ labels: { error: labels.mediaError, retry: labels.retry },
174
+ className: "min-h-0 flex-1 [&_video]:h-full [&_video]:w-full"
175
+ }
176
+ ),
177
+ state !== "error" && /* @__PURE__ */ jsx(
178
+ Button,
179
+ {
180
+ variant: "secondary",
181
+ size: "sm",
182
+ leftIcon: state === "playing" ? /* @__PURE__ */ jsx(Pause, { className: "h-4 w-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Play, { className: "h-4 w-4", "aria-hidden": "true" }),
183
+ onClick: togglePlayback,
184
+ children: state === "playing" ? labels.pause : labels.play
185
+ }
186
+ ),
187
+ blocked && /* @__PURE__ */ jsx(
188
+ "p",
189
+ {
190
+ role: "status",
191
+ className: "text-muted-foreground m-0 text-center text-sm",
192
+ children: labels.blockedAutoplay
193
+ }
194
+ ),
195
+ state === "error" && /* @__PURE__ */ jsx(SourceLink, { src: media.src, labels })
196
+ ] });
197
+ }
198
+ function ImageFeedMedia({ media, title, labels }) {
199
+ const [failed, setFailed] = React2.useState(false);
200
+ return failed ? /* @__PURE__ */ jsxs("div", { role: "alert", className: "flex flex-col gap-3 p-6 text-center", children: [
201
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground m-0", children: labels.mediaError }),
202
+ /* @__PURE__ */ jsx(SourceLink, { src: media.src, labels })
203
+ ] }) : /* @__PURE__ */ jsx(
204
+ "img",
205
+ {
206
+ src: media.src,
207
+ alt: media.alt ?? title,
208
+ loading: "lazy",
209
+ className: "h-full max-h-full w-full object-contain",
210
+ onError: () => setFailed(true)
211
+ }
212
+ );
213
+ }
214
+ function youtubeId(src) {
215
+ const validId = /^[A-Za-z0-9_-]{11}$/;
216
+ if (validId.test(src)) return src;
217
+ try {
218
+ const url = new URL(src);
219
+ const host = url.hostname.toLowerCase();
220
+ const parts = url.pathname.split("/").filter(Boolean);
221
+ let id = null;
222
+ if (host === "youtu.be" || host === "www.youtu.be") {
223
+ id = parts[0] ?? null;
224
+ } else if ([
225
+ "youtube.com",
226
+ "www.youtube.com",
227
+ "m.youtube.com",
228
+ "youtube-nocookie.com",
229
+ "www.youtube-nocookie.com"
230
+ ].includes(host)) {
231
+ id = url.searchParams.get("v") ?? (["embed", "shorts", "live"].includes(parts[0]) ? parts[1] : null);
232
+ }
233
+ return id && validId.test(id) ? id : null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ function YouTubeFeedMedia({
239
+ media,
240
+ title,
241
+ active,
242
+ autoPlay,
243
+ muted,
244
+ loop,
245
+ labels,
246
+ onActivate,
247
+ playbackRequestId
248
+ }) {
249
+ const id = youtubeId(media.src);
250
+ const [requestedPlayback, setRequestedPlayback] = React2.useState(false);
251
+ const handledPlaybackRequest = React2.useRef(
252
+ void 0
253
+ );
254
+ const pendingPlayback = active && playbackRequestId !== void 0 && playbackRequestId !== handledPlaybackRequest.current;
255
+ React2.useEffect(() => {
256
+ if (!active) setRequestedPlayback(false);
257
+ else if (playbackRequestId !== void 0 && playbackRequestId !== handledPlaybackRequest.current) {
258
+ handledPlaybackRequest.current = playbackRequestId;
259
+ setRequestedPlayback(true);
260
+ }
261
+ }, [active, playbackRequestId]);
262
+ const embedUrl = new URL(
263
+ `https://www.youtube-nocookie.com/embed/${id ?? ""}`
264
+ );
265
+ for (const [key, value] of Object.entries({
266
+ autoplay: autoPlay || requestedPlayback || pendingPlayback ? "1" : "0",
267
+ mute: muted ? "1" : "0",
268
+ controls: "1",
269
+ playsinline: "1",
270
+ rel: "0",
271
+ loop: loop ? "1" : "0",
272
+ ...loop && id ? { playlist: id } : {}
273
+ }))
274
+ embedUrl.searchParams.set(key, value);
275
+ const [failed, setFailed] = React2.useState(false);
276
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 w-full flex-col items-center gap-3", children: [
277
+ active && id && !failed ? /* @__PURE__ */ jsx(
278
+ "iframe",
279
+ {
280
+ src: embedUrl.href,
281
+ title,
282
+ className: "min-h-0 w-full flex-1 border-0",
283
+ allow: "autoplay; encrypted-media; picture-in-picture; fullscreen",
284
+ referrerPolicy: "strict-origin-when-cross-origin",
285
+ allowFullScreen: true,
286
+ onError: () => setFailed(true)
287
+ }
288
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 w-full flex-1 flex-col items-center justify-center gap-4", children: [
289
+ media.poster && /* @__PURE__ */ jsx(
290
+ "img",
291
+ {
292
+ src: media.poster,
293
+ alt: "",
294
+ loading: "lazy",
295
+ className: "min-h-0 max-w-full flex-1 object-contain"
296
+ }
297
+ ),
298
+ id && !failed ? /* @__PURE__ */ jsx(
299
+ Button,
300
+ {
301
+ variant: "secondary",
302
+ leftIcon: /* @__PURE__ */ jsx(Play, { className: "h-4 w-4", "aria-hidden": "true" }),
303
+ onClick: () => {
304
+ setRequestedPlayback(true);
305
+ onActivate();
306
+ },
307
+ children: labels.play
308
+ }
309
+ ) : /* @__PURE__ */ jsx("p", { role: "alert", className: "text-muted-foreground m-0", children: labels.mediaError })
310
+ ] }),
311
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground m-0 text-center text-sm", children: labels.youtubeNotice }),
312
+ /* @__PURE__ */ jsx(
313
+ SourceLink,
314
+ {
315
+ src: id ? `https://www.youtube.com/watch?v=${id}` : media.src,
316
+ labels
317
+ }
318
+ )
319
+ ] });
320
+ }
321
+ function FeedMedia(props) {
322
+ if (props.media.kind === "image") return /* @__PURE__ */ jsx(ImageFeedMedia, { ...props });
323
+ if (props.media.kind === "youtube") return /* @__PURE__ */ jsx(YouTubeFeedMedia, { ...props });
324
+ return /* @__PURE__ */ jsx(NativeFeedMedia, { ...props });
325
+ }
326
+ var defaultLabels = {
327
+ feed: "Media feed",
328
+ loading: "Loading media\u2026",
329
+ empty: "No media to display.",
330
+ error: "Unable to load the media feed.",
331
+ retry: "Retry",
332
+ fullscreen: "Open fullscreen",
333
+ close: "Close fullscreen",
334
+ previous: "Previous item",
335
+ next: "Next item",
336
+ play: "Play",
337
+ pause: "Pause",
338
+ blockedAutoplay: "Playback did not start. Press Play to try again.",
339
+ mediaError: "This media is unavailable.",
340
+ openSource: "Open source",
341
+ youtubeNotice: "If embedded playback is unavailable, open the source.",
342
+ item: (position, total) => `Media item ${position} of ${total}`,
343
+ position: (position, total) => `${position} / ${total}`
344
+ };
345
+ function MediaFeedInner({
346
+ items,
347
+ getId,
348
+ getMedia,
349
+ getTitle,
350
+ getCaption,
351
+ getAuthor,
352
+ renderActions,
353
+ renderMedia,
354
+ activeItemId,
355
+ defaultActiveItemId,
356
+ onActiveItemChange,
357
+ playbackRequest,
358
+ loading = false,
359
+ error,
360
+ onRetry,
361
+ autoPlay = true,
362
+ muted = true,
363
+ loop = true,
364
+ labels: labelOverrides,
365
+ classNames = {},
366
+ className,
367
+ ...rest
368
+ }, forwardedRef) {
369
+ const labels = { ...defaultLabels, ...labelOverrides };
370
+ const prefersReducedMotion = usePrefersReducedMotion();
371
+ const allowAutoPlay = autoPlay && !prefersReducedMotion;
372
+ const ids = items.map(getId);
373
+ const idsKey = JSON.stringify(ids);
374
+ const [internalActiveId, setInternalActiveId] = React2.useState(
375
+ () => defaultActiveItemId ?? ids[0]
376
+ );
377
+ const requestedId = activeItemId ?? internalActiveId;
378
+ const activeId = requestedId !== void 0 && ids.includes(requestedId) ? requestedId : ids[0];
379
+ const activeIndex = activeId === void 0 ? -1 : ids.indexOf(activeId);
380
+ const [fullscreen, setFullscreen] = React2.useState(false);
381
+ const [retrying, setRetrying] = React2.useState(false);
382
+ const [retryFailed, setRetryFailed] = React2.useState(false);
383
+ const rootRef = React2.useRef(null);
384
+ const viewportRef = React2.useRef(null);
385
+ const cardRefs = React2.useRef(/* @__PURE__ */ new Map());
386
+ const playbackPositions = React2.useRef(
387
+ /* @__PURE__ */ new Map()
388
+ );
389
+ const consumedPlaybackRequests = React2.useRef(
390
+ /* @__PURE__ */ new Map()
391
+ );
392
+ const fullscreenTriggerRef = React2.useRef(null);
393
+ const hasObserver = typeof IntersectionObserver !== "undefined";
394
+ const [visibleId, setVisibleId] = React2.useState(null);
395
+ const visibleIdRef = React2.useRef(visibleId);
396
+ const [viewportVisible, setViewportVisible] = React2.useState(false);
397
+ const [pageVisible, setPageVisible] = React2.useState(
398
+ () => typeof document === "undefined" || document.visibilityState !== "hidden"
399
+ );
400
+ const latest = React2.useRef({
401
+ items,
402
+ ids,
403
+ activeId,
404
+ activeItemId,
405
+ onActiveItemChange,
406
+ fullscreen
407
+ });
408
+ latest.current = {
409
+ items,
410
+ ids,
411
+ activeId,
412
+ activeItemId,
413
+ onActiveItemChange,
414
+ fullscreen
415
+ };
416
+ const setRoot = React2.useCallback(
417
+ (node) => {
418
+ rootRef.current = node;
419
+ if (typeof forwardedRef === "function") forwardedRef(node);
420
+ else if (forwardedRef) forwardedRef.current = node;
421
+ },
422
+ [forwardedRef]
423
+ );
424
+ const select = React2.useCallback((id) => {
425
+ const current = latest.current;
426
+ const index = current.ids.indexOf(id);
427
+ if (index < 0 || id === current.activeId) return;
428
+ if (current.activeItemId === void 0) setInternalActiveId(id);
429
+ current.onActiveItemChange?.(current.items[index]);
430
+ }, []);
431
+ React2.useEffect(() => {
432
+ const root = rootRef.current;
433
+ if (!root || typeof IntersectionObserver === "undefined") {
434
+ setViewportVisible(true);
435
+ return;
436
+ }
437
+ const observer = new IntersectionObserver(([entry]) => {
438
+ setViewportVisible(entry.isIntersecting);
439
+ });
440
+ observer.observe(root);
441
+ return () => observer.disconnect();
442
+ }, []);
443
+ React2.useEffect(() => {
444
+ const viewport = viewportRef.current;
445
+ if (!viewport || typeof IntersectionObserver === "undefined") return;
446
+ const ratios = /* @__PURE__ */ new Map();
447
+ const observer = new IntersectionObserver(
448
+ (entries) => {
449
+ for (const entry of entries) {
450
+ const id = entry.target.dataset.mediaId;
451
+ if (id !== void 0)
452
+ ratios.set(id, entry.isIntersecting ? entry.intersectionRatio : 0);
453
+ }
454
+ let bestId = null;
455
+ let bestRatio = 0;
456
+ for (const [id, ratio] of ratios) {
457
+ if (ratio > bestRatio) {
458
+ bestId = id;
459
+ bestRatio = ratio;
460
+ }
461
+ }
462
+ visibleIdRef.current = bestId;
463
+ setVisibleId(bestId);
464
+ if (bestId !== null && !latest.current.fullscreen) select(bestId);
465
+ },
466
+ { root: viewport, threshold: [0, 0.25, 0.5, 0.75, 1] }
467
+ );
468
+ cardRefs.current.forEach((card) => observer.observe(card));
469
+ return () => observer.disconnect();
470
+ }, [idsKey, loading, error, select]);
471
+ React2.useEffect(() => {
472
+ const update = () => setPageVisible(document.visibilityState !== "hidden");
473
+ document.addEventListener("visibilitychange", update);
474
+ return () => document.removeEventListener("visibilitychange", update);
475
+ }, []);
476
+ React2.useEffect(() => {
477
+ if (activeId === void 0) return;
478
+ if (!hasObserver) {
479
+ visibleIdRef.current = activeId;
480
+ setVisibleId(activeId);
481
+ }
482
+ if (visibleIdRef.current === activeId && hasObserver) return;
483
+ const card = cardRefs.current.get(activeId);
484
+ if (card && viewportRef.current) {
485
+ viewportRef.current.scrollTo?.({ top: card.offsetTop, behavior: "auto" });
486
+ }
487
+ }, [activeId, idsKey, hasObserver, loading, error]);
488
+ React2.useEffect(() => {
489
+ if (!items.length) setFullscreen(false);
490
+ }, [items.length]);
491
+ React2.useEffect(() => {
492
+ if (loading || error || activeIndex < 0 || !playbackRequest || playbackRequest.itemId !== activeId)
493
+ return;
494
+ if (!pageVisible || !fullscreen && (!viewportVisible || visibleId !== activeId))
495
+ return;
496
+ consumedPlaybackRequests.current.set(
497
+ playbackRequest.itemId,
498
+ playbackRequest.requestId
499
+ );
500
+ }, [
501
+ activeId,
502
+ activeIndex,
503
+ fullscreen,
504
+ pageVisible,
505
+ viewportVisible,
506
+ visibleId,
507
+ playbackRequest,
508
+ loading,
509
+ error
510
+ ]);
511
+ React2.useEffect(() => {
512
+ const positions = playbackPositions.current;
513
+ for (const id of positions.keys()) {
514
+ if (!latest.current.ids.includes(id)) positions.delete(id);
515
+ }
516
+ }, [idsKey]);
517
+ React2.useEffect(() => {
518
+ if (fullscreen || !fullscreenTriggerRef.current) return;
519
+ if (fullscreenTriggerRef.current.isConnected)
520
+ fullscreenTriggerRef.current.focus();
521
+ else viewportRef.current?.focus();
522
+ fullscreenTriggerRef.current = null;
523
+ }, [fullscreen]);
524
+ const navigate = (index, focus = false) => {
525
+ const id = ids[Math.max(0, Math.min(index, ids.length - 1))];
526
+ if (id === void 0) return;
527
+ select(id);
528
+ const card = cardRefs.current.get(id);
529
+ if (card && viewportRef.current) {
530
+ viewportRef.current.scrollTo?.({ top: card.offsetTop, behavior: "auto" });
531
+ if (focus) card.focus({ preventScroll: true });
532
+ }
533
+ };
534
+ const handleKeyDown = (event) => {
535
+ const target = event.target;
536
+ if (event.target !== event.currentTarget && target.dataset?.slot !== "item")
537
+ return;
538
+ let next;
539
+ switch (event.key) {
540
+ case "ArrowDown":
541
+ case "PageDown":
542
+ next = activeIndex + 1;
543
+ break;
544
+ case "ArrowUp":
545
+ case "PageUp":
546
+ next = activeIndex - 1;
547
+ break;
548
+ case "Home":
549
+ next = 0;
550
+ break;
551
+ case "End":
552
+ next = items.length - 1;
553
+ break;
554
+ default:
555
+ return;
556
+ }
557
+ event.preventDefault();
558
+ navigate(next, event.target !== event.currentTarget);
559
+ };
560
+ const retry = async () => {
561
+ if (!onRetry || retrying) return;
562
+ setRetrying(true);
563
+ setRetryFailed(false);
564
+ try {
565
+ await onRetry();
566
+ } catch {
567
+ setRetryFailed(true);
568
+ } finally {
569
+ setRetrying(false);
570
+ }
571
+ };
572
+ const renderCard = (item, index, immersive) => {
573
+ const id = getId(item);
574
+ const media = getMedia(item);
575
+ let playbackPosition = playbackPositions.current.get(id);
576
+ if (!playbackPosition || playbackPosition.src !== media.src) {
577
+ playbackPosition = { src: media.src, timeMs: 0 };
578
+ playbackPositions.current.set(id, playbackPosition);
579
+ }
580
+ const title = getTitle?.(item) || labels.item(index + 1, items.length);
581
+ const author = getAuthor?.(item);
582
+ const caption = getCaption?.(item);
583
+ const active = !loading && !error && pageVisible && (immersive ? fullscreen && id === activeId : !fullscreen && viewportVisible && id === activeId && id === visibleId);
584
+ const playbackRequestId = active && playbackRequest?.itemId === id && consumedPlaybackRequests.current.get(id) !== playbackRequest.requestId ? playbackRequest.requestId : void 0;
585
+ return /* @__PURE__ */ jsxs(
586
+ "article",
587
+ {
588
+ ref: immersive ? void 0 : (node) => {
589
+ if (node) cardRefs.current.set(id, node);
590
+ else cardRefs.current.delete(id);
591
+ },
592
+ "data-slot": "item",
593
+ "data-media-id": id,
594
+ "data-active": active,
595
+ "aria-label": title,
596
+ "aria-posinset": index + 1,
597
+ "aria-setsize": items.length,
598
+ tabIndex: id === activeId ? 0 : -1,
599
+ className: cn(
600
+ "border-border bg-card text-card-foreground focus-visible:ring-ring flex min-h-0 w-full snap-start flex-col overflow-hidden border focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset",
601
+ immersive ? "h-full flex-1 border-0" : "h-full shrink-0 rounded-xl",
602
+ classNames.item
603
+ ),
604
+ children: [
605
+ /* @__PURE__ */ jsx(
606
+ "div",
607
+ {
608
+ "data-slot": "media",
609
+ className: cn(
610
+ "bg-muted flex min-h-0 flex-1 items-center justify-center p-3",
611
+ classNames.media
612
+ ),
613
+ children: renderMedia ? renderMedia(media, {
614
+ item,
615
+ active,
616
+ autoPlay: allowAutoPlay,
617
+ muted,
618
+ loop,
619
+ playbackRequestId
620
+ }) : /* @__PURE__ */ jsx(
621
+ FeedMedia,
622
+ {
623
+ media,
624
+ title,
625
+ active,
626
+ autoPlay: allowAutoPlay,
627
+ muted,
628
+ loop,
629
+ labels,
630
+ onActivate: () => select(id),
631
+ playbackPosition,
632
+ playbackRequestId
633
+ },
634
+ `${id}:${media.kind}:${media.src}:${immersive}`
635
+ )
636
+ }
637
+ ),
638
+ /* @__PURE__ */ jsxs(
639
+ "div",
640
+ {
641
+ "data-slot": "details",
642
+ className: cn(
643
+ "flex max-h-[40%] shrink-0 flex-col gap-2 overflow-y-auto p-4",
644
+ classNames.details
645
+ ),
646
+ children: [
647
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
648
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
649
+ author && /* @__PURE__ */ jsxs(
650
+ "div",
651
+ {
652
+ "data-slot": "author",
653
+ className: cn(
654
+ "mb-2 flex items-center gap-2",
655
+ classNames.author
656
+ ),
657
+ children: [
658
+ /* @__PURE__ */ jsx(
659
+ Avatar,
660
+ {
661
+ name: author.name,
662
+ src: author.avatar,
663
+ alt: "",
664
+ size: "sm"
665
+ }
666
+ ),
667
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium", children: author.name })
668
+ ]
669
+ }
670
+ ),
671
+ /* @__PURE__ */ jsx(
672
+ "h3",
673
+ {
674
+ "data-slot": "title",
675
+ className: cn("m-0 text-base font-semibold", classNames.title),
676
+ children: title
677
+ }
678
+ )
679
+ ] }),
680
+ !immersive && /* @__PURE__ */ jsx(
681
+ Button,
682
+ {
683
+ variant: "ghost",
684
+ size: "sm",
685
+ "aria-label": labels.fullscreen,
686
+ title: labels.fullscreen,
687
+ onClick: (event) => {
688
+ fullscreenTriggerRef.current = event.currentTarget;
689
+ select(id);
690
+ setFullscreen(true);
691
+ },
692
+ children: /* @__PURE__ */ jsx(Maximize2, { className: "h-4 w-4", "aria-hidden": "true" })
693
+ }
694
+ )
695
+ ] }),
696
+ caption != null && /* @__PURE__ */ jsx(
697
+ "div",
698
+ {
699
+ "data-slot": "caption",
700
+ className: cn(
701
+ "text-muted-foreground text-sm",
702
+ classNames.caption
703
+ ),
704
+ children: caption
705
+ }
706
+ ),
707
+ renderActions && /* @__PURE__ */ jsx(
708
+ "div",
709
+ {
710
+ "data-slot": "actions",
711
+ className: cn(
712
+ "flex flex-wrap items-center gap-2",
713
+ classNames.actions
714
+ ),
715
+ children: renderActions(item)
716
+ }
717
+ )
718
+ ]
719
+ }
720
+ )
721
+ ]
722
+ },
723
+ id
724
+ );
725
+ };
726
+ const controls = /* @__PURE__ */ jsxs(
727
+ "div",
728
+ {
729
+ "data-slot": "controls",
730
+ className: cn(
731
+ "flex shrink-0 items-center justify-between gap-3 py-2",
732
+ classNames.controls
733
+ ),
734
+ children: [
735
+ /* @__PURE__ */ jsx(
736
+ Button,
737
+ {
738
+ variant: "ghost",
739
+ size: "sm",
740
+ className: "h-auto min-h-8 flex-1 py-2 [&_[data-slot=button-label]]:whitespace-normal",
741
+ disabled: activeIndex <= 0,
742
+ onClick: () => navigate(activeIndex - 1),
743
+ leftIcon: /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4", "aria-hidden": "true" }),
744
+ children: labels.previous
745
+ }
746
+ ),
747
+ /* @__PURE__ */ jsx(
748
+ "span",
749
+ {
750
+ role: "status",
751
+ "aria-live": "polite",
752
+ className: "text-muted-foreground shrink-0 text-sm whitespace-nowrap",
753
+ children: labels.position(activeIndex + 1, items.length)
754
+ }
755
+ ),
756
+ /* @__PURE__ */ jsx(
757
+ Button,
758
+ {
759
+ variant: "ghost",
760
+ size: "sm",
761
+ className: "h-auto min-h-8 flex-1 py-2 [&_[data-slot=button-label]]:whitespace-normal",
762
+ disabled: activeIndex >= items.length - 1,
763
+ onClick: () => navigate(activeIndex + 1),
764
+ rightIcon: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4", "aria-hidden": "true" }),
765
+ children: labels.next
766
+ }
767
+ )
768
+ ]
769
+ }
770
+ );
771
+ return /* @__PURE__ */ jsxs(
772
+ "div",
773
+ {
774
+ ref: setRoot,
775
+ "data-slot": "root",
776
+ "data-component": "media-feed",
777
+ className: cn(
778
+ "bg-background text-foreground flex h-[min(48rem,80dvh)] min-h-0 w-full flex-col",
779
+ classNames.root,
780
+ className
781
+ ),
782
+ ...rest,
783
+ children: [
784
+ loading || error || !items.length ? /* @__PURE__ */ jsxs(
785
+ "div",
786
+ {
787
+ "data-slot": "status",
788
+ role: error ? "alert" : "status",
789
+ "aria-busy": loading || retrying,
790
+ className: cn(
791
+ "flex min-h-0 flex-1 flex-col items-center justify-center gap-4 p-6 text-center",
792
+ classNames.status
793
+ ),
794
+ children: [
795
+ /* @__PURE__ */ jsx("p", { className: "m-0", children: loading ? labels.loading : error || labels.empty }),
796
+ error && onRetry && /* @__PURE__ */ jsx(
797
+ Button,
798
+ {
799
+ variant: "outline",
800
+ isLoading: retrying,
801
+ loadingText: labels.loading,
802
+ onClick: () => {
803
+ void retry();
804
+ },
805
+ children: labels.retry
806
+ }
807
+ ),
808
+ retryFailed && /* @__PURE__ */ jsx("p", { role: "alert", className: "m-0", children: labels.error })
809
+ ]
810
+ }
811
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
812
+ /* @__PURE__ */ jsx(
813
+ "div",
814
+ {
815
+ ref: viewportRef,
816
+ "data-slot": "viewport",
817
+ role: "feed",
818
+ "aria-label": labels.feed,
819
+ "aria-busy": false,
820
+ tabIndex: 0,
821
+ onKeyDown: handleKeyDown,
822
+ className: cn(
823
+ "focus-visible:ring-ring relative flex min-h-0 flex-1 snap-y snap-mandatory flex-col gap-4 overflow-y-auto overscroll-contain focus-visible:ring-2 focus-visible:outline-none",
824
+ classNames.viewport
825
+ ),
826
+ children: items.map((item, index) => renderCard(item, index, false))
827
+ }
828
+ ),
829
+ controls
830
+ ] }),
831
+ /* @__PURE__ */ jsxs(
832
+ Modal,
833
+ {
834
+ open: fullscreen && !loading && !error && !!items.length,
835
+ onOpenChange: setFullscreen,
836
+ size: "full",
837
+ className: "h-dvh min-h-0 sm:h-[calc(100dvh-2rem)]",
838
+ children: [
839
+ /* @__PURE__ */ jsxs(ModalHeader, { children: [
840
+ /* @__PURE__ */ jsx(ModalTitle, { children: labels.feed }),
841
+ /* @__PURE__ */ jsx(ModalClose, { "aria-label": labels.close })
842
+ ] }),
843
+ /* @__PURE__ */ jsxs(
844
+ "div",
845
+ {
846
+ "data-slot": "fullscreen",
847
+ "data-component": "media-feed",
848
+ role: "feed",
849
+ "aria-label": labels.feed,
850
+ tabIndex: 0,
851
+ onKeyDown: handleKeyDown,
852
+ className: cn(
853
+ "flex min-h-0 flex-1 flex-col px-2 pb-2 sm:px-4",
854
+ classNames.fullscreen
855
+ ),
856
+ children: [
857
+ activeIndex >= 0 && renderCard(items[activeIndex], activeIndex, true),
858
+ controls
859
+ ]
860
+ }
861
+ )
862
+ ]
863
+ }
864
+ )
865
+ ]
866
+ }
867
+ );
868
+ }
869
+ var MediaFeed = React2.forwardRef(MediaFeedInner);
870
+ MediaFeed.displayName = "MediaFeed";
871
+
872
+ export { MediaFeed };
873
+ //# sourceMappingURL=chunk-BNH4SXJB.js.map
874
+ //# sourceMappingURL=chunk-BNH4SXJB.js.map