bobinejs 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,8 @@
1
1
  "use client";
2
2
 
3
- // ../../lib/utils.ts
3
+ // src/utils.ts
4
4
  import { clsx } from "clsx";
5
- import { extendTailwindMerge } from "tailwind-merge";
6
- var twMerge = extendTailwindMerge({
7
- extend: { theme: { text: ["display", "title", "body", "action", "meta", "code"] } }
8
- });
5
+ import { twMerge } from "tailwind-merge";
9
6
  function cn(...inputs) {
10
7
  return twMerge(clsx(inputs));
11
8
  }
@@ -135,6 +132,7 @@ var MEDIA_EVENTS = [
135
132
  "volumechange",
136
133
  "ratechange",
137
134
  "emptied",
135
+ "loadstart",
138
136
  "error",
139
137
  "enterpictureinpicture",
140
138
  "leavepictureinpicture"
@@ -154,6 +152,7 @@ var initialState = {
154
152
  fullscreen: false,
155
153
  pictureInPicture: false,
156
154
  error: null,
155
+ hasSource: false,
157
156
  textTracks: [],
158
157
  textTrack: -1,
159
158
  cues: []
@@ -174,7 +173,13 @@ function captionTracks(media) {
174
173
  );
175
174
  }
176
175
  function cueText(cue) {
177
- return (cue.text ?? "").replace(/<[^>]+>/g, "").trim();
176
+ const vtt = cue;
177
+ const text = typeof vtt.getCueAsHTML === "function" ? vtt.getCueAsHTML().textContent : vtt.text?.replace(/<[^>]+>/g, "");
178
+ return (text ?? "").trim();
179
+ }
180
+ function defaultTrack(media, tracks) {
181
+ const el = Array.from(media?.querySelectorAll("track") ?? []).find((track) => track.default);
182
+ return el ? tracks.indexOf(el.track) : -1;
178
183
  }
179
184
  var sameList = (a, b) => a.length === b.length && a.every((value, i) => Object.is(value, b[i]));
180
185
  var sameTracks = (a, b) => a.length === b.length && a.every((t, i) => t.label === b[i].label && t.language === b[i].language && t.kind === b[i].kind);
@@ -191,6 +196,7 @@ function createMediaStore({ persistKey } = {}) {
191
196
  let container = null;
192
197
  let cueTrack = null;
193
198
  let metadata = null;
199
+ let trackChosen = false;
194
200
  const listeners = /* @__PURE__ */ new Set();
195
201
  const set = (patch) => {
196
202
  let changed = false;
@@ -220,7 +226,8 @@ function createMediaStore({ persistKey } = {}) {
220
226
  muted: media.muted,
221
227
  playbackRate: media.playbackRate,
222
228
  pictureInPicture: typeof document !== "undefined" && document.pictureInPictureElement === media,
223
- error: media.error
229
+ error: media.error,
230
+ hasSource: Boolean(media.src || media.currentSrc || media.querySelector("source[src]"))
224
231
  });
225
232
  };
226
233
  const readCues = () => {
@@ -235,7 +242,7 @@ function createMediaStore({ persistKey } = {}) {
235
242
  };
236
243
  const syncTracks = (requested = state.textTrack) => {
237
244
  const tracks = captionTracks(media);
238
- let active = requested;
245
+ let active = requested === -1 && !trackChosen ? defaultTrack(media, tracks) : requested;
239
246
  tracks.forEach((track, i) => {
240
247
  if (track.mode === "showing") active = i;
241
248
  });
@@ -444,6 +451,7 @@ function createMediaStore({ persistKey } = {}) {
444
451
  }
445
452
  };
446
453
  const syncTracksExplicit = (index) => {
454
+ trackChosen = true;
447
455
  captionTracks(media).forEach((track, i) => {
448
456
  if (i !== index && track.mode === "showing") track.mode = "disabled";
449
457
  });
@@ -462,13 +470,14 @@ function createMediaStore({ persistKey } = {}) {
462
470
  ...actions
463
471
  };
464
472
  }
465
- function formatTime(seconds) {
473
+ function formatTime(seconds, reference = seconds) {
466
474
  if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;
467
475
  const s = Math.floor(seconds % 60);
468
476
  const m = Math.floor(seconds / 60 % 60);
469
477
  const h = Math.floor(seconds / 3600);
470
478
  const ss = s.toString().padStart(2, "0");
471
- return h > 0 ? `${h}:${m.toString().padStart(2, "0")}:${ss}` : `${m}:${ss}`;
479
+ const hours = h > 0 || Number.isFinite(reference) && reference >= 3600;
480
+ return hours ? `${h}:${m.toString().padStart(2, "0")}:${ss}` : `${m}:${ss}`;
472
481
  }
473
482
  function describeMediaError(error) {
474
483
  switch (error?.code) {
@@ -479,7 +488,7 @@ function describeMediaError(error) {
479
488
  case 3:
480
489
  return "This media could not be decoded.";
481
490
  case 4:
482
- return "This format or source isn't supported.";
491
+ return "This media couldn't be loaded.";
483
492
  default:
484
493
  return "Something went wrong during playback.";
485
494
  }
@@ -502,8 +511,8 @@ function useMedia(selector) {
502
511
  () => selector(store.getServerState())
503
512
  );
504
513
  }
505
- function useComposedRefs(...refs) {
506
- return React2.useCallback(composeRefs(...refs), refs);
514
+ function useComposedRefs(a, b, c) {
515
+ return React2.useCallback((node) => composeRefs(a, b, c)(node), [a, b, c]);
507
516
  }
508
517
  function composeRefs(...refs) {
509
518
  return (node) => {
@@ -643,14 +652,14 @@ function useMediaElement(ref, src, loader) {
643
652
  if (!loader || !src || !media) return;
644
653
  return loader(media, src) ?? void 0;
645
654
  }, [store, loader, src]);
646
- return { mediaRef };
655
+ return mediaRef;
647
656
  }
648
657
  function MediaVideo({ className, ref, src, loader, ...props }) {
649
- const media = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
658
+ const mediaRef = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
650
659
  return /* @__PURE__ */ jsx2(
651
660
  "video",
652
661
  {
653
- ref: media.mediaRef,
662
+ ref: mediaRef,
654
663
  "data-slot": "media-video",
655
664
  playsInline: true,
656
665
  preload: "metadata",
@@ -662,11 +671,11 @@ function MediaVideo({ className, ref, src, loader, ...props }) {
662
671
  );
663
672
  }
664
673
  function MediaAudio({ ref, src, loader, ...props }) {
665
- const media = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
674
+ const mediaRef = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
666
675
  return /* @__PURE__ */ jsx2(
667
676
  "audio",
668
677
  {
669
- ref: media.mediaRef,
678
+ ref: mediaRef,
670
679
  "data-slot": "media-audio",
671
680
  preload: "metadata",
672
681
  ...props,
@@ -717,12 +726,14 @@ function IconSwap({ show, a, b }) {
717
726
  function MediaPlayButton({ onClick, ...props }) {
718
727
  const store = useMediaStore();
719
728
  const paused = useMedia((s) => s.paused);
729
+ const hasSource = useMedia((s) => s.hasSource);
720
730
  return /* @__PURE__ */ jsx2(
721
731
  MediaButton,
722
732
  {
723
733
  "aria-label": paused ? "Play" : "Pause",
724
734
  tooltip: paused ? "Play" : "Pause",
725
735
  shortcut: "K",
736
+ disabled: !hasSource,
726
737
  onClick: (event) => {
727
738
  onClick?.(event);
728
739
  store.toggle();
@@ -810,6 +821,7 @@ function MediaPlaybackRateButton({
810
821
  {
811
822
  "aria-label": `Playback speed ${rate}x`,
812
823
  tooltip: "Playback speed",
824
+ dir: "ltr",
813
825
  className: cn("w-(--media-rate-width) min-w-7 px-1.5 font-mono text-xs tabular-nums", className),
814
826
  style: { "--media-rate-width": `calc(${chars}ch + 0.75rem)`, ...style },
815
827
  onClick: (event) => {
@@ -1054,7 +1066,7 @@ function MediaSeek({
1054
1066
  previewClassName
1055
1067
  ),
1056
1068
  style: { left: `${hover * 100}%` },
1057
- children: formatTime(hover * duration)
1069
+ children: formatTime(hover * duration, duration)
1058
1070
  }
1059
1071
  )
1060
1072
  ]
@@ -1146,19 +1158,21 @@ var timeNames = { current: "elapsed", duration: "duration", remaining: "remainin
1146
1158
  function MediaTime({ type = "current", toggle = false, className, ...props }) {
1147
1159
  const [picked, setPicked] = React2.useState(type);
1148
1160
  const shown = toggle ? picked : type;
1161
+ const duration = useMedia((s) => s.duration);
1149
1162
  const value = useMedia((s) => {
1150
- if (shown === "duration") return Math.floor(s.duration);
1163
+ if (shown === "duration") return Math.floor(s.duration) || Math.ceil(s.duration);
1151
1164
  if (shown === "remaining") return Math.ceil(Math.max(s.duration - s.currentTime, 0));
1152
1165
  return Math.floor(s.currentTime);
1153
1166
  });
1154
- const text = shown === "remaining" ? `-${formatTime(value)}` : formatTime(value);
1167
+ const time = formatTime(value, duration);
1168
+ const text = shown === "remaining" ? `-${time}` : time;
1155
1169
  const other = type === "remaining" ? "current" : "remaining";
1156
1170
  const next = shown === type ? other : type;
1157
- return /* @__PURE__ */ jsx2("span", { "data-slot": "media-time", className: cn("font-mono text-xs tabular-nums", className), ...props, children: toggle ? /* @__PURE__ */ jsxs2(
1171
+ return /* @__PURE__ */ jsx2("span", { "data-slot": "media-time", dir: "ltr", className: cn("font-mono text-xs tabular-nums", className), ...props, children: toggle ? /* @__PURE__ */ jsxs2(
1158
1172
  "button",
1159
1173
  {
1160
1174
  type: "button",
1161
- "aria-label": `${shown === "remaining" ? "Time remaining" : shown === "duration" ? "Duration" : "Time elapsed"} ${formatTime(value)}`,
1175
+ "aria-label": `${shown === "remaining" ? "Time remaining" : shown === "duration" ? "Duration" : "Time elapsed"} ${time}`,
1162
1176
  onClick: () => setPicked(next),
1163
1177
  className: "group/media-button relative cursor-pointer rounded-sm outline-none transition-colors duration-150 ease-media hover:text-current focus-visible:ring-2 focus-visible:ring-current/60",
1164
1178
  children: [
@@ -1194,7 +1208,7 @@ function MediaCaptions({ className, ...props }) {
1194
1208
  "data-slot": "media-captions",
1195
1209
  className: cn("pointer-events-none flex flex-col items-center gap-1 text-center", className),
1196
1210
  ...props,
1197
- children: cues.map((cue, i) => /* @__PURE__ */ jsx2("span", { className: "whitespace-pre-line text-balance rounded-md px-2 py-0.5 box-decoration-clone", children: cue }, `${i}-${cue}`))
1211
+ children: cues.map((cue, i) => /* @__PURE__ */ jsx2("span", { dir: "auto", className: "whitespace-pre-line text-balance rounded-md px-2 py-0.5 box-decoration-clone", children: cue }, `${i}-${cue}`))
1198
1212
  }
1199
1213
  );
1200
1214
  }
@@ -1229,20 +1243,31 @@ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelC
1229
1243
  const rootEl = React2.useRef(null);
1230
1244
  const triggerEl = React2.useRef(null);
1231
1245
  const panelEl = React2.useRef(null);
1246
+ const listEl = React2.useRef(null);
1232
1247
  const panelId = React2.useId();
1233
1248
  React2.useLayoutEffect(() => {
1234
1249
  const panel = panelEl.current;
1250
+ const list = listEl.current;
1235
1251
  const trigger = triggerEl.current;
1236
1252
  const player = rootEl.current?.closest("[data-slot='media-root']");
1237
- if (!open || !panel || !trigger || !player) return;
1253
+ if (!open || !panel || !list || !trigger || !player) return;
1254
+ const edge = () => {
1255
+ const left = Math.min(24, list.scrollHeight - list.clientHeight - list.scrollTop);
1256
+ list.style.maskImage = left > 1 ? `linear-gradient(to bottom, black calc(100% - ${left}px), rgb(0 0 0 / 0.3))` : "";
1257
+ };
1238
1258
  const fit = () => {
1239
1259
  const space = trigger.getBoundingClientRect().top - player.getBoundingClientRect().top - 16;
1240
1260
  panel.style.maxHeight = `${Math.max(96, Math.min(288, space))}px`;
1261
+ edge();
1241
1262
  };
1242
1263
  fit();
1243
1264
  const observer = new ResizeObserver(fit);
1244
1265
  observer.observe(player);
1245
- return () => observer.disconnect();
1266
+ list.addEventListener("scroll", edge, { passive: true });
1267
+ return () => {
1268
+ observer.disconnect();
1269
+ list.removeEventListener("scroll", edge);
1270
+ };
1246
1271
  }, [open]);
1247
1272
  React2.useEffect(() => {
1248
1273
  if (!open) return;
@@ -1283,10 +1308,10 @@ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelC
1283
1308
  select();
1284
1309
  close();
1285
1310
  },
1286
- className: "flex h-8 w-full cursor-pointer items-center justify-between gap-6 rounded-md px-2 text-left text-sm outline-none transition-colors duration-150 ease-media hover:bg-current/10 focus-visible:bg-current/15",
1311
+ className: "flex min-h-8 w-full shrink-0 cursor-pointer items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-sm leading-5 outline-none transition-colors duration-150 ease-media hover:bg-current/10 focus-visible:bg-current/15",
1287
1312
  children: [
1288
- /* @__PURE__ */ jsx2("span", { children: label }),
1289
- /* @__PURE__ */ jsx2(CheckIcon, { className: cn("size-3.5 transition-opacity duration-150", checked ? "opacity-100" : "opacity-0") })
1313
+ /* @__PURE__ */ jsx2("span", { dir: "auto", className: "min-w-0", children: label }),
1314
+ /* @__PURE__ */ jsx2(CheckIcon, { className: cn("size-3.5 shrink-0 transition-opacity duration-150", checked ? "opacity-100" : "opacity-0") })
1290
1315
  ]
1291
1316
  },
1292
1317
  label
@@ -1313,7 +1338,7 @@ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelC
1313
1338
  children: /* @__PURE__ */ jsx2(SettingsIcon, { className: cn("transition-transform duration-300 ease-media motion-reduce:transition-none", open && "rotate-90") })
1314
1339
  }
1315
1340
  ),
1316
- open && /* @__PURE__ */ jsxs2(
1341
+ open && /* @__PURE__ */ jsx2(
1317
1342
  "div",
1318
1343
  {
1319
1344
  ref: panelEl,
@@ -1322,40 +1347,47 @@ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelC
1322
1347
  "aria-label": "Playback settings",
1323
1348
  onKeyDown: onPanelKeyDown,
1324
1349
  className: cn(
1325
- "absolute right-0 bottom-full z-10 mb-2 flex max-h-72 w-44 origin-bottom-right flex-col gap-2 overflow-y-auto overscroll-contain rounded-xl p-1.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
1350
+ "absolute right-0 bottom-full z-10 mb-2 flex max-h-72 w-48 origin-bottom-right flex-col overflow-hidden rounded-xl",
1326
1351
  "transition-[opacity,translate,scale] duration-150 ease-media starting:translate-y-1 starting:scale-95 starting:opacity-0 motion-reduce:transition-none",
1327
1352
  panelClassName
1328
1353
  ),
1329
- children: [
1330
- /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": "Speed", className: "flex flex-col", children: [
1331
- /* @__PURE__ */ jsx2("span", { className: "px-2 pt-1 pb-1.5 text-xs opacity-60", children: "Speed" }),
1332
- /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-3 gap-1", children: rates.map((r) => /* @__PURE__ */ jsx2(
1333
- "button",
1334
- {
1335
- type: "button",
1336
- role: "menuitemradio",
1337
- "aria-checked": rate === r,
1338
- "aria-label": r === 1 ? "Normal speed" : `${r}\xD7 speed`,
1339
- tabIndex: rate === r ? 0 : -1,
1340
- onClick: () => {
1341
- store.setPlaybackRate(r);
1342
- close();
1343
- },
1344
- className: cn(
1345
- "h-8 cursor-pointer rounded-md text-sm tabular-nums outline-none transition-colors duration-150 ease-media hover:bg-current/10 focus-visible:ring-1 focus-visible:ring-current/40",
1346
- rate === r && "bg-current/15"
1347
- ),
1348
- children: `${r}\xD7`
1349
- },
1350
- r
1351
- )) })
1352
- ] }),
1353
- tracks.length > 0 && /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": "Captions", className: "flex flex-col", children: [
1354
- /* @__PURE__ */ jsx2("span", { className: "px-2 pt-1 pb-1.5 text-xs opacity-60", children: "Captions" }),
1355
- option("Off", textTrack === -1, () => store.setTextTrack(-1)),
1356
- tracks.map((track, i) => option(track.label, textTrack === i, () => store.setTextTrack(i)))
1357
- ] })
1358
- ]
1354
+ children: /* @__PURE__ */ jsxs2(
1355
+ "div",
1356
+ {
1357
+ ref: listEl,
1358
+ className: "flex min-h-0 flex-col gap-2 overflow-y-auto overscroll-contain p-1.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
1359
+ children: [
1360
+ /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": "Speed", className: "flex flex-col", children: [
1361
+ /* @__PURE__ */ jsx2("span", { className: "px-2 pt-1 pb-1.5 text-xs opacity-60", children: "Speed" }),
1362
+ /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-3 gap-1", children: rates.map((r) => /* @__PURE__ */ jsx2(
1363
+ "button",
1364
+ {
1365
+ type: "button",
1366
+ role: "menuitemradio",
1367
+ "aria-checked": rate === r,
1368
+ "aria-label": r === 1 ? "Normal speed" : `${r}\xD7 speed`,
1369
+ tabIndex: rate === r ? 0 : -1,
1370
+ onClick: () => {
1371
+ store.setPlaybackRate(r);
1372
+ close();
1373
+ },
1374
+ className: cn(
1375
+ "h-8 cursor-pointer rounded-md text-sm tabular-nums outline-none transition-colors duration-150 ease-media hover:bg-current/10 focus-visible:ring-1 focus-visible:ring-current/40",
1376
+ rate === r && "bg-current/15"
1377
+ ),
1378
+ children: `${r}\xD7`
1379
+ },
1380
+ r
1381
+ )) })
1382
+ ] }),
1383
+ tracks.length > 0 && /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": "Captions", className: "flex flex-col", children: [
1384
+ /* @__PURE__ */ jsx2("span", { className: "px-2 pt-1 pb-1.5 text-xs opacity-60", children: "Captions" }),
1385
+ option("Off", textTrack === -1, () => store.setTextTrack(-1)),
1386
+ tracks.map((track, i) => option(track.label, textTrack === i, () => store.setTextTrack(i)))
1387
+ ] })
1388
+ ]
1389
+ }
1390
+ )
1359
1391
  }
1360
1392
  )
1361
1393
  ]
@@ -1380,7 +1412,7 @@ function MediaErrorMessage({ className, message, ...props }) {
1380
1412
  {
1381
1413
  type: "button",
1382
1414
  onClick: () => store.retry(),
1383
- className: "inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm outline-none ring-1 ring-current/25 transition-[background-color,scale] duration-150 ease-media hover:bg-current/10 focus-visible:ring-2 focus-visible:ring-current/60 motion-safe:active:scale-[0.96]",
1415
+ className: "inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm whitespace-nowrap outline-none ring-1 ring-current/25 transition-[background-color,scale] duration-150 ease-media hover:bg-current/10 focus-visible:ring-2 focus-visible:ring-current/60 motion-safe:active:scale-[0.96]",
1384
1416
  children: [
1385
1417
  /* @__PURE__ */ jsx2(RetryIcon, { className: "size-3.5" }),
1386
1418
  "Try again"
@@ -1462,7 +1494,8 @@ function VideoPlayer({
1462
1494
  MediaCaptions,
1463
1495
  {
1464
1496
  className: cn(
1465
- "absolute inset-x-4 bottom-4 text-sm leading-snug transition-transform duration-200 ease-media motion-reduce:transition-none sm:text-base",
1497
+ /* sized by the player, not the viewport, so a small player keeps its cues */
1498
+ "absolute inset-x-4 bottom-4 text-xs leading-snug transition-transform duration-200 ease-media motion-reduce:transition-none @xs/player:text-sm @2xl/player:text-base",
1466
1499
  "group-data-active/media:-translate-y-10 group-has-focus-visible/media:-translate-y-10 group-data-paused/media:group-data-started/media:-translate-y-10",
1467
1500
  "[&>span]:bg-media-scrim/75 [&>span]:text-media-foreground"
1468
1501
  )
@@ -1479,29 +1512,36 @@ function VideoPlayer({
1479
1512
  )
1480
1513
  }
1481
1514
  ),
1482
- /* @__PURE__ */ jsxs3(MediaControls, { className: "absolute inset-x-2 bottom-2 h-9 gap-1 px-1 **:data-[slot=media-tooltip]:bg-media-foreground **:data-[slot=media-tooltip]:text-media-scrim", children: [
1483
- /* @__PURE__ */ jsx3(MediaPlayButton, { tooltipAlign: "start" }),
1484
- /* @__PURE__ */ jsx3(MediaSeek, { className: "mx-2", previewClassName: "bg-media-foreground text-media-scrim" }),
1485
- /* @__PURE__ */ jsx3(MediaTime, { type: "remaining", toggle: true, className: "min-w-10 text-right text-media-foreground/80" }),
1486
- /* @__PURE__ */ jsxs3("div", { className: "group/volume-menu relative", children: [
1487
- /* @__PURE__ */ jsx3(MediaMuteButton, { tooltip: false }),
1488
- /* @__PURE__ */ jsx3(
1489
- "div",
1490
- {
1491
- className: cn(
1492
- "pointer-events-none absolute bottom-full left-1/2 hidden -translate-x-1/2 translate-y-1 pb-2 opacity-0 pointer-fine:block",
1493
- "transition-[opacity,translate] duration-150 ease-media motion-reduce:transition-none",
1494
- volumeOpen
1495
- ),
1496
- children: /* @__PURE__ */ jsx3("div", { className: cn("flex h-24 w-7 justify-center rounded-full py-3", glass), children: /* @__PURE__ */ jsx3(MediaVolume, { orientation: "vertical", className: "h-full" }) })
1497
- }
1498
- )
1499
- ] }),
1500
- /* @__PURE__ */ jsx3(MediaCaptionsButton, {}),
1501
- /* @__PURE__ */ jsx3(MediaSettings, { panelClassName: glass }),
1502
- /* @__PURE__ */ jsx3(MediaPictureInPictureButton, { tooltipAlign: "end", className: "hidden @sm/player:inline-grid" }),
1503
- /* @__PURE__ */ jsx3(MediaFullscreenButton, { tooltipAlign: "end" })
1504
- ] }),
1515
+ /* @__PURE__ */ jsxs3(
1516
+ MediaControls,
1517
+ {
1518
+ dir: "ltr",
1519
+ className: "absolute inset-x-2 bottom-2 h-9 gap-1 px-1 **:data-[slot=media-tooltip]:bg-media-foreground **:data-[slot=media-tooltip]:text-media-scrim",
1520
+ children: [
1521
+ /* @__PURE__ */ jsx3(MediaPlayButton, { tooltipAlign: "start" }),
1522
+ /* @__PURE__ */ jsx3(MediaSeek, { className: "mx-2", previewClassName: "bg-media-foreground text-media-scrim" }),
1523
+ /* @__PURE__ */ jsx3(MediaTime, { type: "remaining", toggle: true, className: "hidden min-w-10 text-right text-media-foreground/80 @xs/player:inline" }),
1524
+ /* @__PURE__ */ jsxs3("div", { className: "group/volume-menu relative", children: [
1525
+ /* @__PURE__ */ jsx3(MediaMuteButton, { tooltip: false }),
1526
+ /* @__PURE__ */ jsx3(
1527
+ "div",
1528
+ {
1529
+ className: cn(
1530
+ "pointer-events-none absolute bottom-full left-1/2 hidden -translate-x-1/2 translate-y-1 pb-2 opacity-0 pointer-fine:block",
1531
+ "transition-[opacity,translate] duration-150 ease-media motion-reduce:transition-none",
1532
+ volumeOpen
1533
+ ),
1534
+ children: /* @__PURE__ */ jsx3("div", { className: cn("flex h-24 w-7 justify-center rounded-full py-3", glass), children: /* @__PURE__ */ jsx3(MediaVolume, { orientation: "vertical", className: "h-full" }) })
1535
+ }
1536
+ )
1537
+ ] }),
1538
+ /* @__PURE__ */ jsx3(MediaCaptionsButton, {}),
1539
+ /* @__PURE__ */ jsx3(MediaSettings, { panelClassName: glass }),
1540
+ /* @__PURE__ */ jsx3(MediaPictureInPictureButton, { tooltipAlign: "end", className: "hidden @sm/player:inline-grid" }),
1541
+ /* @__PURE__ */ jsx3(MediaFullscreenButton, { tooltipAlign: "end" })
1542
+ ]
1543
+ }
1544
+ ),
1505
1545
  /* @__PURE__ */ jsx3(MediaErrorMessage, { className: "absolute inset-0 bg-media-scrim/85 px-6 text-media-foreground backdrop-blur-sm" })
1506
1546
  ]
1507
1547
  }
@@ -1509,6 +1549,7 @@ function VideoPlayer({
1509
1549
  }
1510
1550
 
1511
1551
  // ../../components/ui/audio-player.tsx
1552
+ import * as React3 from "react";
1512
1553
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1513
1554
  var muted = "text-muted-foreground hover:text-foreground";
1514
1555
  var volumeOpen2 = "group-hover/volume-menu:pointer-events-auto group-hover/volume-menu:translate-y-0 group-hover/volume-menu:opacity-100 group-has-focus-visible/volume-menu:pointer-events-auto group-has-focus-visible/volume-menu:translate-y-0 group-has-focus-visible/volume-menu:opacity-100";
@@ -1531,6 +1572,8 @@ function AudioPlayer({
1531
1572
  artwork: artwork ? [{ src: artwork }] : void 0
1532
1573
  } : void 0);
1533
1574
  const hasInfo = Boolean(title || description);
1575
+ const [failedArtwork, setFailedArtwork] = React3.useState(null);
1576
+ const showArtwork = artwork && failedArtwork !== artwork;
1534
1577
  return /* @__PURE__ */ jsxs4(
1535
1578
  MediaRoot,
1536
1579
  {
@@ -1544,7 +1587,7 @@ function AudioPlayer({
1544
1587
  children: [
1545
1588
  /* @__PURE__ */ jsx4(MediaAudio, { className, loader, ...props }),
1546
1589
  /* @__PURE__ */ jsx4("div", { className: cn("flex items-center", hasInfo && "row-span-2 @max-sm/audio:row-span-1"), children: /* @__PURE__ */ jsxs4("span", { className: "relative shrink-0", children: [
1547
- artwork ? (
1590
+ showArtwork ? (
1548
1591
  /**
1549
1592
  * The tile presses as one piece. Scaling the button alone pulled
1550
1593
  * its scrim in from the edges and bared a band of the image. No
@@ -1556,7 +1599,12 @@ function AudioPlayer({
1556
1599
  {
1557
1600
  src: artwork,
1558
1601
  alt: "",
1559
- className: "size-full rounded-lg object-cover outline outline-black/10 -outline-offset-1 dark:outline-white/10"
1602
+ draggable: false,
1603
+ onError: () => setFailedArtwork(artwork),
1604
+ ref: (img) => {
1605
+ if (img?.complete && img.naturalWidth === 0) setFailedArtwork(artwork);
1606
+ },
1607
+ className: "size-full rounded-lg object-cover outline select-none outline-black/10 -outline-offset-1 dark:outline-white/10"
1560
1608
  }
1561
1609
  ),
1562
1610
  /* @__PURE__ */ jsx4(
@@ -1577,17 +1625,26 @@ function AudioPlayer({
1577
1625
  /* @__PURE__ */ jsx4(
1578
1626
  MediaLoading,
1579
1627
  {
1580
- className: artwork ? "absolute inset-0 m-auto size-5 text-background" : "absolute -inset-1 size-auto border-current/15 border-t-current text-foreground"
1628
+ className: showArtwork ? "absolute inset-0 m-auto size-5 text-background" : "absolute -inset-1 size-auto border-current/15 border-t-current text-foreground"
1581
1629
  }
1582
1630
  )
1583
1631
  ] }) }),
1584
1632
  hasInfo && /* @__PURE__ */ jsxs4("div", { className: "col-start-2 flex min-w-0 items-baseline gap-2 self-end text-sm leading-5 @max-sm/audio:col-span-3 @max-sm/audio:col-start-1 @max-sm/audio:row-start-2 @max-sm/audio:pt-1", children: [
1585
- title && /* @__PURE__ */ jsx4("span", { className: "truncate font-medium", children: title }),
1586
- description && /* @__PURE__ */ jsx4("span", { className: "truncate text-muted-foreground", children: description })
1633
+ title && /* @__PURE__ */ jsx4("span", { dir: "auto", title: typeof title === "string" ? title : void 0, className: "truncate font-medium", children: title }),
1634
+ description && /* @__PURE__ */ jsx4(
1635
+ "span",
1636
+ {
1637
+ dir: "auto",
1638
+ title: typeof description === "string" ? description : void 0,
1639
+ className: "min-w-12 shrink-[4] truncate text-muted-foreground",
1640
+ children: description
1641
+ }
1642
+ )
1587
1643
  ] }),
1588
1644
  /* @__PURE__ */ jsxs4(
1589
1645
  "div",
1590
1646
  {
1647
+ dir: "ltr",
1591
1648
  className: cn(
1592
1649
  "col-start-2 flex items-center gap-2.5 text-muted-foreground",
1593
1650
  hasInfo && "row-start-2 self-start @max-sm/audio:col-span-3 @max-sm/audio:col-start-1 @max-sm/audio:row-start-3"