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

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,6 +21,18 @@ 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.
25
+
26
+ ```tsx
27
+ <CaptionedVideo
28
+ src="/research/liwld/video/liwld-draft-16x9.mp4"
29
+ poster="/research/liwld/video/liwld-draft-16x9.poster.png"
30
+ captions="/research/liwld/video/liwld-draft-16x9.captions.en.json"
31
+ fallbackCaptions="/research/liwld/video/liwld-draft-16x9.captions.en.vtt"
32
+ chapters="/research/liwld/video/liwld-draft-16x9.chapters.en.vtt"
33
+ title="Growth without linkage"
34
+ />
35
+ ```
24
36
 
25
37
  ```tsx
26
38
  import { BrandAnimation, Button, Logo, NavbarLogo } from '@dataimago/ui';
package/dist/index.cjs CHANGED
@@ -33,6 +33,10 @@ var index_exports = {};
33
33
  __export(index_exports, {
34
34
  BrandAnimation: () => BrandAnimation,
35
35
  Button: () => Button,
36
+ CAPTIONS_SCHEMA: () => CAPTIONS_SCHEMA,
37
+ CAPTIONS_STORAGE_KEY: () => CAPTIONS_STORAGE_KEY,
38
+ CaptionLayer: () => CaptionLayer,
39
+ CaptionedVideo: () => CaptionedVideo,
36
40
  CopulaLockup: () => CopulaLockup,
37
41
  Logo: () => Logo,
38
42
  LogoMonogram: () => LogoMonogram,
@@ -43,13 +47,20 @@ __export(index_exports, {
43
47
  THEME_STORAGE_KEY: () => THEME_STORAGE_KEY,
44
48
  ThemeScript: () => ThemeScript,
45
49
  ThemeToggle: () => ThemeToggle,
50
+ captionLook: () => captionLook,
51
+ isCaptionTrack: () => isCaptionTrack,
46
52
  nextThemePreference: () => nextThemePreference,
53
+ pageAt: () => pageAt,
54
+ paletteVar: () => paletteVar,
47
55
  readThemePreference: () => readThemePreference,
48
56
  resolveTheme: () => resolveTheme,
49
57
  setThemePreference: () => setThemePreference,
58
+ splitMath: () => splitMath,
50
59
  themeInitScript: () => themeInitScript,
60
+ toneColor: () => toneColor,
51
61
  useResolvedTheme: () => useResolvedTheme,
52
- useThemePreference: () => useThemePreference
62
+ useThemePreference: () => useThemePreference,
63
+ wordTone: () => wordTone
53
64
  });
54
65
  module.exports = __toCommonJS(index_exports);
55
66
 
@@ -661,10 +672,581 @@ var import_jsx_runtime10 = require("react/jsx-runtime");
661
672
  function ThemeScript({ nonce }) {
662
673
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("script", { nonce, dangerouslySetInnerHTML: { __html: themeInitScript } });
663
674
  }
675
+
676
+ // src/video/CaptionedVideo.tsx
677
+ var import_react2 = require("react");
678
+
679
+ // src/video/CaptionLayer.tsx
680
+ var import_react = require("react");
681
+
682
+ // src/video/captions.ts
683
+ var CAPTIONS_SCHEMA = "dataimago.video_captions.v1";
684
+ function pageAt(pages, t) {
685
+ let lo = 0, hi = pages.length - 1;
686
+ while (lo <= hi) {
687
+ const mid = lo + hi >> 1, p = pages[mid];
688
+ if (t < p.start) hi = mid - 1;
689
+ else if (t >= p.end) lo = mid + 1;
690
+ else return p;
691
+ }
692
+ return void 0;
693
+ }
694
+ function wordTone(at, t, span) {
695
+ if (span <= 0) return t >= at ? 1 : 0;
696
+ return Math.min(1, Math.max(0, (t - at) / span));
697
+ }
698
+ function toneColor(tone, ink, inkMute) {
699
+ if (tone >= 1) return ink;
700
+ if (tone <= 0) return inkMute;
701
+ return `color-mix(in srgb, ${ink} ${Math.round(tone * 100)}%, ${inkMute})`;
702
+ }
703
+ function splitMath(text) {
704
+ const m = text.match(/^(.*?)\$([^$\s]+)\$(.*)$/);
705
+ return m ? { before: m[1], tex: m[2], after: m[3] } : null;
706
+ }
707
+ function paletteVar(token) {
708
+ return /^color\.palette\.[a-z]+\.\d{2,3}$/.test(token) ? `var(--${token.replace(/\./g, "-")})` : null;
709
+ }
710
+ function captionLook(treatment, o) {
711
+ const { size, scale, ink, paper } = o;
712
+ const halo = [2, 6, 14, 24].map((r) => `0 0 ${r * scale}px ${paper}`).join(", ");
713
+ const panel = treatment === "boxed" ? { background: ink, padding: "0.35em 0.7em", borderRadius: 4 * scale } : treatment === "frosted" ? {
714
+ background: `color-mix(in srgb, ${paper} 82%, transparent)`,
715
+ backdropFilter: `blur(${14 * scale}px)`,
716
+ WebkitBackdropFilter: `blur(${14 * scale}px)`,
717
+ padding: "0.5em 0.95em",
718
+ borderRadius: size * 0.6,
719
+ boxShadow: `0 ${size * 0.15}px ${size * 0.8}px color-mix(in srgb, ${ink} 10%, transparent)`,
720
+ border: `${Math.max(1, scale)}px solid color-mix(in srgb, ${ink} 8%, transparent)`
721
+ } : { padding: "0.2em 0.4em", textShadow: halo };
722
+ const text = {
723
+ fontFamily: o.family,
724
+ fontWeight: treatment === "boxed" ? 400 : o.weight,
725
+ fontSize: size,
726
+ lineHeight: o.lineHeight,
727
+ textAlign: "center",
728
+ letterSpacing: treatment === "boxed" ? 0 : "0.005em",
729
+ ...treatment === "boxed" ? { color: paper } : {}
730
+ };
731
+ return { panel, text, lift: treatment === "frosted" ? Math.round(size * 0.95) : 0, toned: treatment !== "boxed" };
732
+ }
733
+
734
+ // src/video/mathml.ts
735
+ var MATHML_NS = "http://www.w3.org/1998/Math/MathML";
736
+ var ELEMENTS = /* @__PURE__ */ new Set([
737
+ "math",
738
+ "mrow",
739
+ "mi",
740
+ "mn",
741
+ "mo",
742
+ "ms",
743
+ "mtext",
744
+ "mspace",
745
+ "msub",
746
+ "msup",
747
+ "msubsup",
748
+ "mfrac",
749
+ "msqrt",
750
+ "mroot",
751
+ "mover",
752
+ "munder",
753
+ "munderover",
754
+ "mtable",
755
+ "mtr",
756
+ "mtd",
757
+ "mpadded",
758
+ "mphantom",
759
+ "mstyle",
760
+ "menclose",
761
+ "semantics",
762
+ "annotation"
763
+ ]);
764
+ var ATTRIBUTES = /* @__PURE__ */ new Set([
765
+ "display",
766
+ "mathvariant",
767
+ "stretchy",
768
+ "fence",
769
+ "separator",
770
+ "form",
771
+ "lspace",
772
+ "rspace",
773
+ "accent",
774
+ "accentunder",
775
+ "movablelimits",
776
+ "largeop",
777
+ "symmetric",
778
+ "minsize",
779
+ "maxsize",
780
+ "width",
781
+ "height",
782
+ "depth",
783
+ "voffset",
784
+ "linethickness",
785
+ "columnalign",
786
+ "rowalign",
787
+ "columnspacing",
788
+ "rowspacing",
789
+ "scriptlevel",
790
+ "displaystyle",
791
+ "notation",
792
+ "encoding"
793
+ ]);
794
+ function rebuild(node, doc) {
795
+ if (node.nodeType === Node.TEXT_NODE) return doc.createTextNode(node.textContent ?? "");
796
+ if (node.nodeType !== Node.ELEMENT_NODE) return null;
797
+ const el = node;
798
+ const tag = el.localName;
799
+ if (el.namespaceURI !== MATHML_NS || !ELEMENTS.has(tag)) return null;
800
+ if (tag === "annotation") return null;
801
+ const out = doc.createElementNS(MATHML_NS, tag);
802
+ for (const a of Array.from(el.attributes)) {
803
+ if (!ATTRIBUTES.has(a.name)) continue;
804
+ out.setAttribute(a.name, a.value);
805
+ }
806
+ for (const child of Array.from(el.childNodes)) {
807
+ const c = rebuild(child, doc);
808
+ if (c) out.appendChild(c);
809
+ }
810
+ return out;
811
+ }
812
+ var cache = /* @__PURE__ */ new Map();
813
+ function safeMathML(markup, doc = document) {
814
+ if (!cache.has(markup)) {
815
+ const parsed = new DOMParser().parseFromString(`<!doctype html><body>${markup}`, "text/html");
816
+ const root = parsed.body.firstElementChild;
817
+ const math = root && root.localName === "math" ? rebuild(root, doc) : null;
818
+ cache.set(markup, math);
819
+ }
820
+ const hit = cache.get(markup);
821
+ return hit ? hit.cloneNode(true) : null;
822
+ }
823
+
824
+ // src/video/CaptionLayer.tsx
825
+ var import_jsx_runtime11 = require("react/jsx-runtime");
826
+ var INK = "var(--color-palette-ink-900)";
827
+ var INK_MUTE = "var(--color-palette-ink-500)";
828
+ var PAPER = "var(--color-palette-stone-50)";
829
+ var MathSpan = ({ markup, tex }) => {
830
+ const ref = (0, import_react.useRef)(null);
831
+ (0, import_react.useLayoutEffect)(() => {
832
+ const host = ref.current;
833
+ if (!host) return;
834
+ const math = safeMathML(markup, host.ownerDocument);
835
+ if (!math) {
836
+ host.textContent = tex;
837
+ return;
838
+ }
839
+ math.style.fontFamily = "var(--font-family-math)";
840
+ math.style.fontStyle = "normal";
841
+ host.replaceChildren(math);
842
+ }, [markup, tex]);
843
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { ref, style: { whiteSpace: "nowrap" } });
844
+ };
845
+ var CaptionLayer = ({ track, time, width, reducedMotion = true, clearance = 0, page: given }) => {
846
+ const page = given === void 0 ? pageAt(track.pages, time) : given;
847
+ const scale = width / track.frame.width;
848
+ const size = track.frame.fontSize * scale;
849
+ const { safe } = track.frame;
850
+ const c = track.style.colors;
851
+ const ink = (c && paletteVar(c.ink)) ?? INK, inkMute = (c && paletteVar(c.inkMute)) ?? INK_MUTE, paper = (c && paletteVar(c.paper)) ?? PAPER;
852
+ const look = captionLook(track.style.treatment, {
853
+ size,
854
+ scale,
855
+ weight: track.style.weight,
856
+ lineHeight: track.style.lineHeight,
857
+ family: "var(--font-family-sans)",
858
+ ink,
859
+ paper
860
+ });
861
+ const toned = look.toned && !reducedMotion && given === void 0;
862
+ const lines = page ? page.lines.map((_, li) => page.words.filter((w) => w.line === li)) : [];
863
+ const plain = page ? page.lines.map((l) => l.replace(/\$([^$\s]+)\$/g, "$1")).join(" ") : "";
864
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
865
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { "data-caption-text": "", lang: track.lang || void 0, style: { position: "absolute", width: 1, height: 1, margin: -1, padding: 0, overflow: "hidden", clip: "rect(0 0 0 0)", clipPath: "inset(50%)", whiteSpace: "nowrap", border: 0 }, children: plain }),
866
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "aria-hidden": "true", style: { position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" }, children: page ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: {
867
+ position: "absolute",
868
+ left: safe.left * scale,
869
+ right: safe.right * scale,
870
+ bottom: Math.max(safe.bottom * scale + look.lift, clearance),
871
+ display: "flex",
872
+ justifyContent: "center"
873
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { ...look.panel, ...look.text }, children: lines.map((ws, li) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { children: ws.map((w, wi) => {
874
+ const m = splitMath(w.text);
875
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { style: look.toned ? { color: toned ? toneColor(wordTone(w.at, time, track.style.toneSeconds), ink, inkMute) : ink } : void 0, children: [
876
+ m && w.math ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
877
+ m.before,
878
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MathSpan, { markup: w.math, tex: m.tex }),
879
+ m.after
880
+ ] }) : w.text,
881
+ wi < ws.length - 1 ? " " : ""
882
+ ] }, wi);
883
+ }) }, li)) }) }) : null })
884
+ ] });
885
+ };
886
+
887
+ // src/video/CaptionedVideo.tsx
888
+ var import_jsx_runtime12 = require("react/jsx-runtime");
889
+ var CAPTIONS_STORAGE_KEY = "dataimago:video-captions";
890
+ var readCaptionsChoice = () => {
891
+ try {
892
+ return window.localStorage.getItem(CAPTIONS_STORAGE_KEY) !== "off";
893
+ } catch {
894
+ return true;
895
+ }
896
+ };
897
+ var writeCaptionsChoice = (on) => {
898
+ try {
899
+ window.localStorage.setItem(CAPTIONS_STORAGE_KEY, on ? "on" : "off");
900
+ } catch {
901
+ }
902
+ };
903
+ var num = (x) => typeof x === "number" && Number.isFinite(x);
904
+ var strings = (x) => Array.isArray(x) && x.every((y) => typeof y === "string");
905
+ function isCaptionTrack(x) {
906
+ const t = x;
907
+ if (!t || typeof t !== "object" || t.schema !== CAPTIONS_SCHEMA) return false;
908
+ const f = t.frame, st = t.style;
909
+ if (!f || !num(f.width) || f.width <= 0 || !num(f.height) || f.height <= 0 || !num(f.fontSize) || f.fontSize <= 0 || !f.safe || !["top", "right", "bottom", "left"].every((k) => num(f.safe[k]))) return false;
910
+ if (!st || !["editorial", "frosted", "boxed"].includes(st.treatment) || !num(st.weight) || !num(st.toneSeconds) || !num(st.lineHeight)) return false;
911
+ if (st.colors !== void 0 && !(st.colors && [st.colors.paper, st.colors.ink, st.colors.inkMute].every((x2) => typeof x2 === "string" && paletteVar(x2)))) return false;
912
+ return Array.isArray(t.pages) && t.pages.length > 0 && t.pages.every((p, i) => p && num(p.start) && num(p.end) && p.start < p.end && (i === 0 || p.start >= t.pages[i - 1].end) && strings(p.lines) && p.lines.length > 0 && Array.isArray(p.words) && p.words.length > 0 && p.words.every((w) => w && typeof w.text === "string" && num(w.at) && Number.isInteger(w.line) && w.line >= 0 && w.line < p.lines.length && (w.math === void 0 || typeof w.math === "string")));
913
+ }
914
+ function cueLines(cue) {
915
+ let text = "";
916
+ const walk = (n) => {
917
+ if (n.nodeType === Node.TEXT_NODE) text += n.textContent ?? "";
918
+ else if (n.localName === "br") text += "\n";
919
+ else n.childNodes.forEach(walk);
920
+ };
921
+ walk(cue.getCueAsHTML());
922
+ return text.split("\n").map((l) => l.trim()).filter(Boolean);
923
+ }
924
+ var CONTROLS_HOLD_MS = 2500;
925
+ var CONTROLS_HEIGHT = 56;
926
+ var usePrefersReducedMotion3 = () => {
927
+ const [reduced, setReduced] = (0, import_react2.useState)(true);
928
+ (0, import_react2.useEffect)(() => {
929
+ const q = window.matchMedia?.("(prefers-reduced-motion: reduce)");
930
+ if (!q) {
931
+ setReduced(false);
932
+ return;
933
+ }
934
+ setReduced(q.matches);
935
+ const on = () => setReduced(q.matches);
936
+ q.addEventListener("change", on);
937
+ return () => q.removeEventListener("change", on);
938
+ }, []);
939
+ return reduced;
940
+ };
941
+ var buttonStyle = {
942
+ font: "inherit",
943
+ fontFamily: "var(--font-family-sans)",
944
+ fontWeight: 600,
945
+ fontSize: 13,
946
+ lineHeight: 1,
947
+ color: "var(--color-palette-paper-50)",
948
+ background: "color-mix(in srgb, var(--color-palette-ink-900) 72%, transparent)",
949
+ border: "1px solid color-mix(in srgb, var(--color-palette-paper-50) 35%, transparent)",
950
+ borderRadius: 4,
951
+ padding: "6px 9px",
952
+ cursor: "pointer",
953
+ pointerEvents: "auto",
954
+ // the house floor for touch targets ([[accessibility-beyond-compliance]]), as ThemeToggle
955
+ minWidth: 48,
956
+ minHeight: 48
957
+ };
958
+ var NATIVE_FULLSCREEN_RULE = [
959
+ "video[data-dataimago-captioned-video]::-webkit-media-controls-fullscreen-button { display: none; }",
960
+ "video[data-dataimago-captioned-video]::-webkit-media-controls-toggle-closed-captions-button { display: none; }"
961
+ ].join("\n");
962
+ var nativeRuleAdded = false;
963
+ function hideNativeFullscreenButton() {
964
+ if (nativeRuleAdded || typeof document === "undefined" || !("adoptedStyleSheets" in document)) return;
965
+ try {
966
+ const sheet = new CSSStyleSheet();
967
+ sheet.replaceSync(NATIVE_FULLSCREEN_RULE);
968
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
969
+ nativeRuleAdded = true;
970
+ } catch {
971
+ }
972
+ }
973
+ var CaptionedVideo = ({
974
+ src,
975
+ poster,
976
+ captions,
977
+ fallbackCaptions,
978
+ chapters,
979
+ title,
980
+ lang = "en",
981
+ className,
982
+ style
983
+ }) => {
984
+ const wrapRef = (0, import_react2.useRef)(null);
985
+ const stageRef = (0, import_react2.useRef)(null);
986
+ const videoRef = (0, import_react2.useRef)(null);
987
+ const trackRef = (0, import_react2.useRef)(null);
988
+ const chaptersRef = (0, import_react2.useRef)(null);
989
+ const [track, setTrack] = (0, import_react2.useState)(null);
990
+ const [failed, setFailed] = (0, import_react2.useState)(false);
991
+ const [cue, setCue] = (0, import_react2.useState)(null);
992
+ const [time, setTime] = (0, import_react2.useState)(0);
993
+ const [width, setWidth] = (0, import_react2.useState)(0);
994
+ const [on, setOn] = (0, import_react2.useState)(true);
995
+ const [full, setFull] = (0, import_react2.useState)(false);
996
+ const [paused, setPaused] = (0, import_react2.useState)(true);
997
+ const [active, setActive] = (0, import_react2.useState)(false);
998
+ const [focusWithin, setFocusWithin] = (0, import_react2.useState)(false);
999
+ const reducedMotion = usePrefersReducedMotion3();
1000
+ (0, import_react2.useEffect)(() => setOn(readCaptionsChoice()), []);
1001
+ (0, import_react2.useEffect)(() => {
1002
+ let live = true;
1003
+ setTrack(null);
1004
+ setFailed(false);
1005
+ fetch(captions, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : Promise.reject(new Error(String(r.status)))).then((j) => {
1006
+ if (!live) return;
1007
+ if (isCaptionTrack(j)) setTrack(j);
1008
+ else setFailed(true);
1009
+ }).catch(() => {
1010
+ if (live) setFailed(true);
1011
+ });
1012
+ return () => {
1013
+ live = false;
1014
+ };
1015
+ }, [captions]);
1016
+ (0, import_react2.useEffect)(() => {
1017
+ const el = trackRef.current, t = el?.track;
1018
+ if (!el || !t) return;
1019
+ const change = () => {
1020
+ const c = t.activeCues?.[0];
1021
+ const lines = c ? cueLines(c) : [];
1022
+ setCue(c ? { start: c.startTime, end: c.endTime, lines, words: lines.flatMap((l, line) => l.split(" ").map((w) => ({ text: w, at: c.startTime, line }))) } : null);
1023
+ };
1024
+ t.addEventListener("cuechange", change);
1025
+ el.addEventListener("load", change);
1026
+ t.mode = nativeViewRef.current && onRef.current ? "showing" : "hidden";
1027
+ change();
1028
+ return () => {
1029
+ t.removeEventListener("cuechange", change);
1030
+ el.removeEventListener("load", change);
1031
+ };
1032
+ }, [fallbackCaptions]);
1033
+ (0, import_react2.useEffect)(() => {
1034
+ const v = videoRef.current;
1035
+ if (!v) return;
1036
+ let raf = 0;
1037
+ const tick = () => {
1038
+ setTime(v.currentTime);
1039
+ raf = requestAnimationFrame(tick);
1040
+ };
1041
+ const sync = () => {
1042
+ setPaused(v.paused);
1043
+ setTime(v.currentTime);
1044
+ cancelAnimationFrame(raf);
1045
+ if (!v.paused && !v.ended) raf = requestAnimationFrame(tick);
1046
+ };
1047
+ const events = ["play", "playing", "pause", "seeking", "seeked", "timeupdate", "ended", "emptied", "loadstart", "loadedmetadata"];
1048
+ for (const e of events) v.addEventListener(e, sync);
1049
+ sync();
1050
+ return () => {
1051
+ cancelAnimationFrame(raf);
1052
+ for (const e of events) v.removeEventListener(e, sync);
1053
+ };
1054
+ }, [src]);
1055
+ (0, import_react2.useEffect)(() => {
1056
+ const el = stageRef.current;
1057
+ if (!el || typeof ResizeObserver === "undefined") return;
1058
+ const ro = new ResizeObserver(([e]) => setWidth(e.contentRect.width));
1059
+ ro.observe(el);
1060
+ return () => ro.disconnect();
1061
+ }, []);
1062
+ const [nativeView, setNativeView] = (0, import_react2.useState)(false);
1063
+ const nativeViewRef = (0, import_react2.useRef)(false), onRef = (0, import_react2.useRef)(true);
1064
+ nativeViewRef.current = nativeView;
1065
+ onRef.current = on;
1066
+ (0, import_react2.useEffect)(() => {
1067
+ const v = videoRef.current;
1068
+ if (!v) return;
1069
+ let fs = false, pip = false;
1070
+ const update = () => setNativeView(fs || pip);
1071
+ const change = () => {
1072
+ setFull(document.fullscreenElement === wrapRef.current);
1073
+ fs = document.fullscreenElement === v;
1074
+ update();
1075
+ };
1076
+ const flag = (set, x) => () => {
1077
+ set(x);
1078
+ update();
1079
+ };
1080
+ const handlers = [
1081
+ [document, "fullscreenchange", change],
1082
+ [v, "webkitbeginfullscreen", flag((x) => {
1083
+ fs = x;
1084
+ }, true)],
1085
+ [v, "webkitendfullscreen", flag((x) => {
1086
+ fs = x;
1087
+ }, false)],
1088
+ [v, "enterpictureinpicture", flag((x) => {
1089
+ pip = x;
1090
+ }, true)],
1091
+ [v, "leavepictureinpicture", flag((x) => {
1092
+ pip = x;
1093
+ }, false)]
1094
+ ];
1095
+ for (const [t, e, h] of handlers) t.addEventListener(e, h);
1096
+ return () => {
1097
+ for (const [t, e, h] of handlers) t.removeEventListener(e, h);
1098
+ };
1099
+ }, []);
1100
+ (0, import_react2.useEffect)(() => {
1101
+ const v = videoRef.current;
1102
+ if (!v) return;
1103
+ const want = nativeView && on ? "showing" : "hidden";
1104
+ const hold = () => {
1105
+ const t = trackRef.current?.track;
1106
+ if (t && t.mode !== want) t.mode = want;
1107
+ };
1108
+ hold();
1109
+ v.textTracks.addEventListener("change", hold);
1110
+ trackRef.current?.addEventListener("load", hold);
1111
+ const el = trackRef.current;
1112
+ return () => {
1113
+ v.textTracks.removeEventListener("change", hold);
1114
+ el?.removeEventListener("load", hold);
1115
+ };
1116
+ }, [nativeView, on, fallbackCaptions]);
1117
+ (0, import_react2.useEffect)(hideNativeFullscreenButton, []);
1118
+ (0, import_react2.useEffect)(() => {
1119
+ const t = chaptersRef.current?.track;
1120
+ if (t) t.mode = "hidden";
1121
+ }, [chapters]);
1122
+ const holdRef = (0, import_react2.useRef)(0);
1123
+ const wake = (0, import_react2.useCallback)(() => {
1124
+ setActive(true);
1125
+ window.clearTimeout(holdRef.current);
1126
+ holdRef.current = window.setTimeout(() => setActive(false), CONTROLS_HOLD_MS);
1127
+ }, []);
1128
+ (0, import_react2.useEffect)(() => () => window.clearTimeout(holdRef.current), []);
1129
+ const toggleCaptions = (0, import_react2.useCallback)(() => setOn((x) => {
1130
+ writeCaptionsChoice(!x);
1131
+ return !x;
1132
+ }), []);
1133
+ const toggleFullscreen = (0, import_react2.useCallback)(() => {
1134
+ const wrap = wrapRef.current, v = videoRef.current;
1135
+ if (document.fullscreenElement === wrap) {
1136
+ void document.exitFullscreen();
1137
+ return;
1138
+ }
1139
+ if (wrap?.requestFullscreen) {
1140
+ wrap.requestFullscreen().catch(() => v?.webkitEnterFullscreen?.());
1141
+ return;
1142
+ }
1143
+ v?.webkitEnterFullscreen?.();
1144
+ }, []);
1145
+ const onKey = (e) => {
1146
+ const t = e.target;
1147
+ if (e.altKey || e.ctrlKey || e.metaKey || t.isContentEditable || t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) return;
1148
+ if (e.key === "c" || e.key === "C") {
1149
+ e.preventDefault();
1150
+ toggleCaptions();
1151
+ }
1152
+ if (e.key === "f" || e.key === "F") {
1153
+ e.preventDefault();
1154
+ toggleFullscreen();
1155
+ }
1156
+ };
1157
+ const controlsUp = paused || active || focusWithin;
1158
+ const showLayer = on && width > 0 && !nativeView && (track !== null || cue !== null || failed);
1159
+ const layerTrack = track ?? { ...FALLBACK_FRAME, lang };
1160
+ const frame = layerTrack.frame;
1161
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1162
+ "div",
1163
+ {
1164
+ ref: wrapRef,
1165
+ className,
1166
+ onPointerMove: wake,
1167
+ onFocus: () => {
1168
+ setFocusWithin(true);
1169
+ wake();
1170
+ },
1171
+ onBlur: (e) => {
1172
+ if (!e.currentTarget.contains(e.relatedTarget)) setFocusWithin(false);
1173
+ },
1174
+ onKeyDown: onKey,
1175
+ style: {
1176
+ position: "relative",
1177
+ display: full ? "flex" : "block",
1178
+ alignItems: "center",
1179
+ justifyContent: "center",
1180
+ background: full ? "var(--color-palette-ink-900)" : void 0,
1181
+ ...style
1182
+ },
1183
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { ref: stageRef, style: { position: "relative", width: full ? `min(100vw, calc(100vh * ${frame.width / frame.height}))` : "100%", aspectRatio: `${frame.width} / ${frame.height}` }, children: [
1184
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1185
+ "video",
1186
+ {
1187
+ ref: videoRef,
1188
+ src,
1189
+ poster,
1190
+ controls: true,
1191
+ controlsList: "nofullscreen",
1192
+ "data-dataimago-captioned-video": "",
1193
+ playsInline: true,
1194
+ preload: "metadata",
1195
+ "aria-label": title,
1196
+ style: { display: "block", width: "100%", height: "100%" },
1197
+ children: [
1198
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("track", { ref: trackRef, kind: "captions", src: fallbackCaptions, srcLang: lang, label: "Captions", default: true }),
1199
+ chapters ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("track", { ref: chaptersRef, kind: "chapters", src: chapters, srcLang: lang, label: "Chapters" }) : null
1200
+ ]
1201
+ }
1202
+ ),
1203
+ showLayer ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1204
+ CaptionLayer,
1205
+ {
1206
+ track: layerTrack,
1207
+ time,
1208
+ width,
1209
+ reducedMotion,
1210
+ clearance: controlsUp ? CONTROLS_HEIGHT : 0,
1211
+ page: track ? void 0 : cue
1212
+ }
1213
+ ) : null,
1214
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: {
1215
+ position: "absolute",
1216
+ top: 8,
1217
+ right: 8,
1218
+ display: "flex",
1219
+ gap: 6,
1220
+ pointerEvents: "none",
1221
+ // hidden once faded: an invisible button must not take a click or a Tab stop
1222
+ opacity: controlsUp ? 1 : 0,
1223
+ visibility: controlsUp ? "visible" : "hidden",
1224
+ transition: reducedMotion ? void 0 : controlsUp ? "opacity 200ms" : "opacity 200ms, visibility 0s linear 200ms"
1225
+ }, children: [
1226
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", style: buttonStyle, "aria-pressed": on, "aria-label": "CC (captions)", title: "Captions (c)", onClick: toggleCaptions, onFocus: wake, children: "CC" }),
1227
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", style: buttonStyle, "aria-pressed": full, "aria-label": full ? "Exit full screen" : "Full screen", title: "Full screen (f)", onClick: toggleFullscreen, onFocus: wake, children: full ? "Exit" : "Full" })
1228
+ ] })
1229
+ ] })
1230
+ }
1231
+ );
1232
+ };
1233
+ var FALLBACK_FRAME = {
1234
+ schema: CAPTIONS_SCHEMA,
1235
+ work: "",
1236
+ version: "",
1237
+ lang: "en",
1238
+ pages: [],
1239
+ style: { profile: "dataimago-v1", treatment: "frosted", weight: 500, toneSeconds: 0, lineHeight: 1.28 },
1240
+ frame: { width: 1920, height: 1080, fontSize: 44, safe: { top: 72, right: 96, bottom: 72, left: 96 } }
1241
+ };
664
1242
  // Annotate the CommonJS export names for ESM import in node:
665
1243
  0 && (module.exports = {
666
1244
  BrandAnimation,
667
1245
  Button,
1246
+ CAPTIONS_SCHEMA,
1247
+ CAPTIONS_STORAGE_KEY,
1248
+ CaptionLayer,
1249
+ CaptionedVideo,
668
1250
  CopulaLockup,
669
1251
  Logo,
670
1252
  LogoMonogram,
@@ -675,12 +1257,19 @@ function ThemeScript({ nonce }) {
675
1257
  THEME_STORAGE_KEY,
676
1258
  ThemeScript,
677
1259
  ThemeToggle,
1260
+ captionLook,
1261
+ isCaptionTrack,
678
1262
  nextThemePreference,
1263
+ pageAt,
1264
+ paletteVar,
679
1265
  readThemePreference,
680
1266
  resolveTheme,
681
1267
  setThemePreference,
1268
+ splitMath,
682
1269
  themeInitScript,
1270
+ toneColor,
683
1271
  useResolvedTheme,
684
- useThemePreference
1272
+ useThemePreference,
1273
+ wordTone
685
1274
  });
686
1275
  //# sourceMappingURL=index.cjs.map