@dataimago/ui 0.1.0-alpha.42 → 0.1.0-alpha.43

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/README.md CHANGED
@@ -21,7 +21,7 @@ Current public exports:
21
21
  - `<Logo>` — manifest-driven mark renderer backed by `@dataimago/brand`.
22
22
  - `<BrandAnimation>` — decorative brand animation with reduced-motion poster fallback.
23
23
  - `<NavbarLogo>` — animated navbar mark with reduced-motion poster fallback.
24
- - `<CaptionedVideo>` — a video with the video-foundry's caption treatment drawn over it (frosted panel, word tone, MathML), from a release's `captions.<lang>.json`; the WebVTT file stays as the hidden native track and the fallback text. Fullscreen takes the whole player so the captions come along. `<CaptionLayer>` draws one moment of it (pure presentation), and the caption functions (`pageAt`, `wordTone`, `captionLook`, …) match the foundry's burned-in treatment (the foundry imports them once this ships; #407). Needs `@dataimago/css` loaded for the palette, `--font-family-sans` and the self-hosted Noto Sans Math.
24
+ - `<CaptionedVideo>` — a video with the video-foundry's caption treatment drawn over it (frosted panel, word tone, MathML), from a release's `captions.<lang>.json`; the WebVTT file stays as the hidden native track and the fallback text. Fullscreen takes the whole player so the captions come along. `<CaptionLayer>` draws one moment of it (pure presentation), and the caption functions (`pageAt`, `wordTone`, `captionLook`, …) match the foundry's burned-in treatment (the foundry imports them once this ships; #407). Needs `@dataimago/css` loaded for the palette, `--font-family-sans` and the self-hosted Noto Sans Math. `poster` is required and nothing autoplays. Under `prefers-reduced-motion: reduce` the player plays `reducedSrc` if given; otherwise it says the video is animated throughout and offers `storyboard` (a link) first, and plays the full cut only if the reader chooses it.
25
25
 
26
26
  ```tsx
27
27
  <CaptionedVideo
package/dist/index.cjs CHANGED
@@ -924,7 +924,7 @@ function cueLines(cue) {
924
924
  var CONTROLS_HOLD_MS = 2500;
925
925
  var CONTROLS_HEIGHT = 56;
926
926
  var usePrefersReducedMotion3 = () => {
927
- const [reduced, setReduced] = (0, import_react2.useState)(true);
927
+ const [reduced, setReduced] = (0, import_react2.useState)(null);
928
928
  (0, import_react2.useEffect)(() => {
929
929
  const q = window.matchMedia?.("(prefers-reduced-motion: reduce)");
930
930
  if (!q) {
@@ -938,6 +938,37 @@ var usePrefersReducedMotion3 = () => {
938
938
  }, []);
939
939
  return reduced;
940
940
  };
941
+ var linkStyle = {
942
+ display: "inline-flex",
943
+ alignItems: "center",
944
+ justifyContent: "center",
945
+ minWidth: 48,
946
+ minHeight: 48,
947
+ padding: "0 8px",
948
+ color: "var(--color-palette-ink-900)",
949
+ textUnderlineOffset: "0.2em"
950
+ };
951
+ var videoOnlyFullscreen = () => typeof navigator !== "undefined" && /iPhone|iPod/.test(navigator.userAgent);
952
+ var gateStyle = {
953
+ position: "absolute",
954
+ left: "50%",
955
+ top: "50%",
956
+ transform: "translate(-50%, -50%)",
957
+ maxWidth: "min(90%, 26rem)",
958
+ display: "flex",
959
+ flexDirection: "column",
960
+ alignItems: "center",
961
+ gap: 10,
962
+ padding: "14px 18px",
963
+ textAlign: "center",
964
+ fontFamily: "var(--font-family-sans)",
965
+ fontSize: 15,
966
+ lineHeight: 1.4,
967
+ color: "var(--color-palette-ink-900)",
968
+ background: "var(--color-palette-stone-50)",
969
+ border: "1px solid var(--color-palette-ink-200)",
970
+ borderRadius: 6
971
+ };
941
972
  var buttonStyle = {
942
973
  font: "inherit",
943
974
  fontFamily: "var(--font-family-sans)",
@@ -976,6 +1007,8 @@ var CaptionedVideo = ({
976
1007
  captions,
977
1008
  fallbackCaptions,
978
1009
  chapters,
1010
+ reducedSrc,
1011
+ storyboard,
979
1012
  title,
980
1013
  lang = "en",
981
1014
  className,
@@ -996,7 +1029,12 @@ var CaptionedVideo = ({
996
1029
  const [paused, setPaused] = (0, import_react2.useState)(true);
997
1030
  const [active, setActive] = (0, import_react2.useState)(false);
998
1031
  const [focusWithin, setFocusWithin] = (0, import_react2.useState)(false);
999
- const reducedMotion = usePrefersReducedMotion3();
1032
+ const prefersReduced = usePrefersReducedMotion3();
1033
+ const reducedMotion = prefersReduced !== false;
1034
+ const [chosen, setChosen] = (0, import_react2.useState)(false);
1035
+ const noticeId = (0, import_react2.useId)();
1036
+ const gated = prefersReduced === true && !reducedSrc && !chosen;
1037
+ const videoSrc = prefersReduced && reducedSrc ? reducedSrc : prefersReduced === null && reducedSrc ? void 0 : src;
1000
1038
  (0, import_react2.useEffect)(() => setOn(readCaptionsChoice()), []);
1001
1039
  (0, import_react2.useEffect)(() => {
1002
1040
  let live = true;
@@ -1051,7 +1089,7 @@ var CaptionedVideo = ({
1051
1089
  cancelAnimationFrame(raf);
1052
1090
  for (const e of events) v.removeEventListener(e, sync);
1053
1091
  };
1054
- }, [src]);
1092
+ }, [videoSrc]);
1055
1093
  (0, import_react2.useEffect)(() => {
1056
1094
  const el = stageRef.current;
1057
1095
  if (!el || typeof ResizeObserver === "undefined") return;
@@ -1126,21 +1164,32 @@ var CaptionedVideo = ({
1126
1164
  holdRef.current = window.setTimeout(() => setActive(false), CONTROLS_HOLD_MS);
1127
1165
  }, []);
1128
1166
  (0, import_react2.useEffect)(() => () => window.clearTimeout(holdRef.current), []);
1167
+ (0, import_react2.useEffect)(() => {
1168
+ const v = videoRef.current;
1169
+ if (!v) return;
1170
+ v.addEventListener("play", wake);
1171
+ return () => v.removeEventListener("play", wake);
1172
+ }, [wake]);
1129
1173
  const toggleCaptions = (0, import_react2.useCallback)(() => setOn((x) => {
1130
1174
  writeCaptionsChoice(!x);
1131
1175
  return !x;
1132
1176
  }), []);
1133
1177
  const toggleFullscreen = (0, import_react2.useCallback)(() => {
1134
- const wrap = wrapRef.current, v = videoRef.current;
1178
+ const wrap = wrapRef.current;
1179
+ const v = videoRef.current;
1135
1180
  if (document.fullscreenElement === wrap) {
1136
1181
  void document.exitFullscreen();
1137
1182
  return;
1138
1183
  }
1139
- if (wrap?.requestFullscreen) {
1140
- wrap.requestFullscreen().catch(() => v?.webkitEnterFullscreen?.());
1184
+ if (v?.webkitDisplayingFullscreen) {
1185
+ v.webkitExitFullscreen?.();
1186
+ return;
1187
+ }
1188
+ if (!wrap?.requestFullscreen || !document.fullscreenEnabled || videoOnlyFullscreen()) {
1189
+ v?.webkitEnterFullscreen?.();
1141
1190
  return;
1142
1191
  }
1143
- v?.webkitEnterFullscreen?.();
1192
+ wrap.requestFullscreen().catch(() => v?.webkitEnterFullscreen?.());
1144
1193
  }, []);
1145
1194
  const onKey = (e) => {
1146
1195
  const t = e.target;
@@ -1164,6 +1213,7 @@ var CaptionedVideo = ({
1164
1213
  ref: wrapRef,
1165
1214
  className,
1166
1215
  onPointerMove: wake,
1216
+ onPointerDown: wake,
1167
1217
  onFocus: () => {
1168
1218
  setFocusWithin(true);
1169
1219
  wake();
@@ -1185,14 +1235,15 @@ var CaptionedVideo = ({
1185
1235
  "video",
1186
1236
  {
1187
1237
  ref: videoRef,
1188
- src,
1238
+ src: videoSrc,
1189
1239
  poster,
1190
- controls: true,
1240
+ controls: !gated,
1191
1241
  controlsList: "nofullscreen",
1192
1242
  "data-dataimago-captioned-video": "",
1193
1243
  playsInline: true,
1194
1244
  preload: "metadata",
1195
1245
  "aria-label": title,
1246
+ "aria-hidden": gated || void 0,
1196
1247
  style: { display: "block", width: "100%", height: "100%" },
1197
1248
  children: [
1198
1249
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("track", { ref: trackRef, kind: "captions", src: fallbackCaptions, srcLang: lang, label: "Captions", default: true }),
@@ -1211,11 +1262,28 @@ var CaptionedVideo = ({
1211
1262
  page: track ? void 0 : cue
1212
1263
  }
1213
1264
  ) : null,
1265
+ gated ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { role: "group", "aria-labelledby": noticeId, style: gateStyle, children: [
1266
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { id: noticeId, style: { margin: 0 }, children: "This video is animated throughout." }),
1267
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1268
+ "button",
1269
+ {
1270
+ type: "button",
1271
+ style: { ...buttonStyle, pointerEvents: "auto" },
1272
+ onClick: () => {
1273
+ setChosen(true);
1274
+ void videoRef.current?.play().catch(() => {
1275
+ });
1276
+ },
1277
+ children: "Play the video"
1278
+ }
1279
+ ),
1280
+ storyboard ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("a", { href: storyboard, style: linkStyle, children: "View the storyboard instead" }) : null
1281
+ ] }) : null,
1214
1282
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: {
1215
1283
  position: "absolute",
1216
1284
  top: 8,
1217
1285
  right: 8,
1218
- display: "flex",
1286
+ display: gated ? "none" : "flex",
1219
1287
  gap: 6,
1220
1288
  pointerEvents: "none",
1221
1289
  // hidden once faded: an invisible button must not take a click or a Tab stop