@editframe/react 0.37.3-beta → 0.38.1

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 (42) hide show
  1. package/dist/components/TimelineRoot.d.ts +19 -12
  2. package/dist/components/TimelineRoot.js +34 -34
  3. package/dist/components/TimelineRoot.js.map +1 -1
  4. package/dist/gui/OverlayItem.js.map +1 -1
  5. package/dist/gui/Scrubber.d.ts +2 -0
  6. package/dist/gui/Scrubber.js.map +1 -1
  7. package/dist/{elements → gui}/ThumbnailStrip.d.ts +1 -1
  8. package/dist/{elements → gui}/ThumbnailStrip.js +2 -4
  9. package/dist/gui/ThumbnailStrip.js.map +1 -0
  10. package/dist/gui/TimelineRuler.js.map +1 -1
  11. package/dist/gui/TrimHandles.d.ts +11 -0
  12. package/dist/gui/TrimHandles.js +18 -0
  13. package/dist/gui/TrimHandles.js.map +1 -0
  14. package/dist/hooks/create-element.d.ts +10 -3
  15. package/dist/hooks/create-element.js +4 -1
  16. package/dist/hooks/create-element.js.map +1 -1
  17. package/dist/hooks/useMediaInfo.d.ts +15 -0
  18. package/dist/hooks/useMediaInfo.js +59 -0
  19. package/dist/hooks/useMediaInfo.js.map +1 -0
  20. package/dist/hooks/usePanZoomTransform.js.map +1 -1
  21. package/dist/hooks/useTimingInfo.d.ts +2 -2
  22. package/dist/hooks/useTimingInfo.js +12 -11
  23. package/dist/hooks/useTimingInfo.js.map +1 -1
  24. package/dist/index.d.ts +5 -3
  25. package/dist/index.js +4 -2
  26. package/dist/r3f/CompositionCanvas.d.ts +32 -0
  27. package/dist/r3f/CompositionCanvas.js +94 -0
  28. package/dist/r3f/CompositionCanvas.js.map +1 -0
  29. package/dist/r3f/OffscreenCompositionCanvas.d.ts +28 -0
  30. package/dist/r3f/OffscreenCompositionCanvas.js +119 -0
  31. package/dist/r3f/OffscreenCompositionCanvas.js.map +1 -0
  32. package/dist/r3f/index.d.ts +5 -0
  33. package/dist/r3f/index.js +5 -0
  34. package/dist/r3f/renderOffscreen.d.ts +27 -0
  35. package/dist/r3f/renderOffscreen.js +291 -0
  36. package/dist/r3f/renderOffscreen.js.map +1 -0
  37. package/dist/r3f/worker-protocol.d.ts +39 -0
  38. package/dist/server.d.ts +11 -0
  39. package/dist/server.js +11 -0
  40. package/package.json +45 -11
  41. package/tsdown.config.ts +1 -0
  42. package/dist/elements/ThumbnailStrip.js.map +0 -1
@@ -2,10 +2,15 @@ import * as React$1 from "react";
2
2
 
3
3
  //#region src/components/TimelineRoot.d.ts
4
4
  interface TimelineRootProps {
5
- /** Unique identifier for the root timegroup */
5
+ /**
6
+ * Unique identifier used for playback control targeting (Preview, Scrubber, TogglePlay).
7
+ * Also passed as `id` prop to the component for clone rendering.
8
+ */
6
9
  id: string;
7
- /** React component that renders the timeline content (must include a Timegroup at root) */
8
- component: React$1.ComponentType;
10
+ /**
11
+ * React component that renders the timeline content (must include a Timegroup at root).
12
+ */
13
+ component: React$1.ComponentType<Record<string, unknown>>;
9
14
  /** Optional CSS class name for the container */
10
15
  className?: string;
11
16
  /** Optional inline styles for the container */
@@ -16,23 +21,25 @@ interface TimelineRootProps {
16
21
  /**
17
22
  * TimelineRoot - Factory wrapper for React-based timelines.
18
23
  *
19
- * This component enables proper clone rendering by providing a factory pattern
20
- * for creating fully functional timeline instances. When render clones are created
21
- * (for exports, thumbnails, etc.), the initializer re-renders the React component
22
- * tree to ensure all JavaScript state and React lifecycle methods work correctly.
24
+ * This component enables proper clone rendering by registering a clone factory
25
+ * for the managed ef-timegroup element. When render clones are needed
26
+ * (for exports, thumbnails, etc.), the factory mounts a fresh React component
27
+ * tree producing a fully functional second instance with all hooks, state,
28
+ * and effects running.
29
+ *
30
+ * This is necessary because React DOM cannot be cloned via cloneNode() —
31
+ * cloned elements are dead HTML without React's fiber tree behind them.
32
+ * The factory pattern ensures each clone is a real React mount.
23
33
  *
24
34
  * @example
25
35
  * ```tsx
26
- * const MyTimelineContent = () => (
36
+ * const MyTimeline = () => (
27
37
  * <Timegroup mode="sequence">
28
38
  * <MyScenes />
29
39
  * </Timegroup>
30
40
  * );
31
41
  *
32
- * // Wrap with Configuration if needed
33
- * <Configuration apiHost="...">
34
- * <TimelineRoot id="root" component={MyTimelineContent} />
35
- * </Configuration>
42
+ * <TimelineRoot id="root" component={MyTimeline} />
36
43
  * ```
37
44
  */
38
45
  declare const TimelineRoot: React$1.FC<TimelineRootProps>;
@@ -1,3 +1,4 @@
1
+ import { registerCloneFactory, unregisterCloneFactory } from "@editframe/elements";
1
2
  import { useEffect, useRef } from "react";
2
3
  import * as ReactDOM from "react-dom/client";
3
4
  import { flushSync } from "react-dom";
@@ -6,59 +7,58 @@ import { jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/components/TimelineRoot.tsx
7
8
  /**
8
9
  * TimelineRoot - Factory wrapper for React-based timelines.
9
- *
10
- * This component enables proper clone rendering by providing a factory pattern
11
- * for creating fully functional timeline instances. When render clones are created
12
- * (for exports, thumbnails, etc.), the initializer re-renders the React component
13
- * tree to ensure all JavaScript state and React lifecycle methods work correctly.
14
- *
10
+ *
11
+ * This component enables proper clone rendering by registering a clone factory
12
+ * for the managed ef-timegroup element. When render clones are needed
13
+ * (for exports, thumbnails, etc.), the factory mounts a fresh React component
14
+ * tree producing a fully functional second instance with all hooks, state,
15
+ * and effects running.
16
+ *
17
+ * This is necessary because React DOM cannot be cloned via cloneNode() —
18
+ * cloned elements are dead HTML without React's fiber tree behind them.
19
+ * The factory pattern ensures each clone is a real React mount.
20
+ *
15
21
  * @example
16
22
  * ```tsx
17
- * const MyTimelineContent = () => (
23
+ * const MyTimeline = () => (
18
24
  * <Timegroup mode="sequence">
19
25
  * <MyScenes />
20
26
  * </Timegroup>
21
27
  * );
22
- *
23
- * // Wrap with Configuration if needed
24
- * <Configuration apiHost="...">
25
- * <TimelineRoot id="root" component={MyTimelineContent} />
26
- * </Configuration>
28
+ *
29
+ * <TimelineRoot id="root" component={MyTimeline} />
27
30
  * ```
28
31
  */
29
32
  const TimelineRoot = ({ id, component: Component, className, style, children }) => {
30
33
  const containerRef = useRef(null);
34
+ const componentRef = useRef(Component);
35
+ componentRef.current = Component;
31
36
  useEffect(() => {
32
37
  const container = containerRef.current;
33
38
  if (!container) return;
34
39
  const timegroup = container.querySelector("ef-timegroup");
35
- if (!timegroup) {
36
- console.warn("[TimelineRoot] No ef-timegroup found in component. Ensure your component renders a Timegroup.");
37
- return;
38
- }
39
- timegroup.id = id;
40
- timegroup.initializer = (cloneEl) => {
41
- const cloneContainer = cloneEl.parentElement;
42
- if (!cloneContainer) {
43
- console.error("[TimelineRoot] No parent container for clone");
44
- return;
45
- }
46
- cloneEl.remove();
40
+ if (!timegroup) throw new Error("[TimelineRoot] No ef-timegroup found in component. Ensure your component renders a Timegroup.");
41
+ registerCloneFactory(timegroup, (cloneContainer) => {
47
42
  const root = ReactDOM.createRoot(cloneContainer);
43
+ const Comp = componentRef.current;
48
44
  flushSync(() => {
49
- root.render(/* @__PURE__ */ jsx(Component, {}));
45
+ root.render(/* @__PURE__ */ jsx(Comp, { id }));
50
46
  });
51
47
  const newTimegroup = cloneContainer.querySelector("ef-timegroup");
52
- if (newTimegroup) newTimegroup._reactRoot = root;
53
- else {
54
- cloneContainer._reactRoot = root;
55
- console.error("[TimelineRoot] No ef-timegroup found after React render");
56
- }
57
- };
48
+ if (!newTimegroup) throw new Error("[TimelineRoot] Clone factory did not produce an ef-timegroup. Ensure your component renders a Timegroup.");
49
+ return {
50
+ timegroup: newTimegroup,
51
+ cleanup: () => {
52
+ queueMicrotask(() => {
53
+ root.unmount();
54
+ });
55
+ }
56
+ };
57
+ });
58
58
  return () => {
59
- timegroup.initializer = void 0;
59
+ unregisterCloneFactory(timegroup);
60
60
  };
61
- }, [id, Component]);
61
+ }, [id]);
62
62
  return /* @__PURE__ */ jsxs("div", {
63
63
  ref: containerRef,
64
64
  className,
@@ -66,7 +66,7 @@ const TimelineRoot = ({ id, component: Component, className, style, children })
66
66
  display: "contents",
67
67
  ...style
68
68
  },
69
- children: [children, /* @__PURE__ */ jsx(Component, {})]
69
+ children: [children, /* @__PURE__ */ jsx(Component, { id })]
70
70
  });
71
71
  };
72
72
 
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineRoot.js","names":["TimelineRoot: React.FC<TimelineRootProps>"],"sources":["../../src/components/TimelineRoot.tsx"],"sourcesContent":["import * as React from 'react';\nimport * as ReactDOM from 'react-dom/client';\nimport { flushSync } from 'react-dom';\nimport { useEffect, useRef } from 'react';\nimport type { EFTimegroup } from '@editframe/elements';\n\ninterface TimelineRootProps {\n /** Unique identifier for the root timegroup */\n id: string;\n /** React component that renders the timeline content (must include a Timegroup at root) */\n component: React.ComponentType;\n /** Optional CSS class name for the container */\n className?: string;\n /** Optional inline styles for the container */\n style?: React.CSSProperties;\n /** Optional children to render alongside the component (e.g., Configuration wrapper) */\n children?: React.ReactNode;\n}\n\n/**\n * TimelineRoot - Factory wrapper for React-based timelines.\n * \n * This component enables proper clone rendering by providing a factory pattern\n * for creating fully functional timeline instances. When render clones are created\n * (for exports, thumbnails, etc.), the initializer re-renders the React component\n * tree to ensure all JavaScript state and React lifecycle methods work correctly.\n * \n * @example\n * ```tsx\n * const MyTimelineContent = () => (\n * <Timegroup mode=\"sequence\">\n * <MyScenes />\n * </Timegroup>\n * );\n * \n * // Wrap with Configuration if needed\n * <Configuration apiHost=\"...\">\n * <TimelineRoot id=\"root\" component={MyTimelineContent} />\n * </Configuration>\n * ```\n */\nexport const TimelineRoot: React.FC<TimelineRootProps> = ({\n id,\n component: Component,\n className,\n style,\n children,\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n \n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n \n // Find the root timegroup rendered by Component\n const timegroup = container.querySelector('ef-timegroup') as EFTimegroup;\n if (!timegroup) {\n console.warn('[TimelineRoot] No ef-timegroup found in component. Ensure your component renders a Timegroup.');\n return;\n }\n \n // Ensure the timegroup has the specified ID\n timegroup.id = id;\n \n // Register factory initializer - MUST be synchronous\n // Uses flushSync to force React to render synchronously\n timegroup.initializer = (cloneEl: EFTimegroup) => {\n const cloneContainer = cloneEl.parentElement;\n if (!cloneContainer) {\n console.error('[TimelineRoot] No parent container for clone');\n return;\n }\n \n // Remove the cloned DOM - React will render a fresh component tree\n // The cloned DOM doesn't have React bindings, so we need to replace it\n cloneEl.remove();\n \n // Create React root for the clone container\n const root = ReactDOM.createRoot(cloneContainer);\n \n // Use flushSync to render synchronously (required by initializer contract)\n // This ensures the component tree is fully rendered before initializer returns\n flushSync(() => {\n root.render(<Component />);\n });\n \n // Find the new timegroup rendered by React and store the React root on it\n // This is needed for cleanup in createRenderClone\n const newTimegroup = cloneContainer.querySelector('ef-timegroup');\n if (newTimegroup) {\n (newTimegroup as any)._reactRoot = root;\n } else {\n // Store root on container so we can still clean up\n (cloneContainer as any)._reactRoot = root;\n console.error('[TimelineRoot] No ef-timegroup found after React render');\n }\n };\n \n // Cleanup: remove initializer when component unmounts\n return () => {\n timegroup.initializer = undefined;\n };\n }, [id, Component]);\n \n return (\n <div \n ref={containerRef} \n className={className} \n style={{ display: 'contents', ...style }}\n >\n {children}\n <Component />\n </div>\n );\n};\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,MAAaA,gBAA6C,EACxD,IACA,WAAW,WACX,WACA,OACA,eACI;CACJ,MAAM,eAAe,OAAuB,KAAK;AAEjD,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAGhB,MAAM,YAAY,UAAU,cAAc,eAAe;AACzD,MAAI,CAAC,WAAW;AACd,WAAQ,KAAK,gGAAgG;AAC7G;;AAIF,YAAU,KAAK;AAIf,YAAU,eAAe,YAAyB;GAChD,MAAM,iBAAiB,QAAQ;AAC/B,OAAI,CAAC,gBAAgB;AACnB,YAAQ,MAAM,+CAA+C;AAC7D;;AAKF,WAAQ,QAAQ;GAGhB,MAAM,OAAO,SAAS,WAAW,eAAe;AAIhD,mBAAgB;AACd,SAAK,OAAO,oBAAC,cAAY,CAAC;KAC1B;GAIF,MAAM,eAAe,eAAe,cAAc,eAAe;AACjE,OAAI,aACF,CAAC,aAAqB,aAAa;QAC9B;AAEL,IAAC,eAAuB,aAAa;AACrC,YAAQ,MAAM,0DAA0D;;;AAK5E,eAAa;AACX,aAAU,cAAc;;IAEzB,CAAC,IAAI,UAAU,CAAC;AAEnB,QACE,qBAAC;EACC,KAAK;EACM;EACX,OAAO;GAAE,SAAS;GAAY,GAAG;GAAO;aAEvC,UACD,oBAAC,cAAY;GACT"}
1
+ {"version":3,"file":"TimelineRoot.js","names":["TimelineRoot: React.FC<TimelineRootProps>"],"sources":["../../src/components/TimelineRoot.tsx"],"sourcesContent":["import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { flushSync } from \"react-dom\";\nimport { useEffect, useRef } from \"react\";\nimport {\n registerCloneFactory,\n unregisterCloneFactory,\n type EFTimegroup,\n} from \"@editframe/elements\";\n\ninterface TimelineRootProps {\n /**\n * Unique identifier used for playback control targeting (Preview, Scrubber, TogglePlay).\n * Also passed as `id` prop to the component for clone rendering.\n */\n id: string;\n /**\n * React component that renders the timeline content (must include a Timegroup at root).\n */\n component: React.ComponentType<Record<string, unknown>>;\n /** Optional CSS class name for the container */\n className?: string;\n /** Optional inline styles for the container */\n style?: React.CSSProperties;\n /** Optional children to render alongside the component (e.g., Configuration wrapper) */\n children?: React.ReactNode;\n}\n\n/**\n * TimelineRoot - Factory wrapper for React-based timelines.\n *\n * This component enables proper clone rendering by registering a clone factory\n * for the managed ef-timegroup element. When render clones are needed\n * (for exports, thumbnails, etc.), the factory mounts a fresh React component\n * tree producing a fully functional second instance with all hooks, state,\n * and effects running.\n *\n * This is necessary because React DOM cannot be cloned via cloneNode() —\n * cloned elements are dead HTML without React's fiber tree behind them.\n * The factory pattern ensures each clone is a real React mount.\n *\n * @example\n * ```tsx\n * const MyTimeline = () => (\n * <Timegroup mode=\"sequence\">\n * <MyScenes />\n * </Timegroup>\n * );\n *\n * <TimelineRoot id=\"root\" component={MyTimeline} />\n * ```\n */\nexport const TimelineRoot: React.FC<TimelineRootProps> = ({\n id,\n component: Component,\n className,\n style,\n children,\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const componentRef = useRef(Component);\n componentRef.current = Component;\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Find the root timegroup rendered by Component\n const timegroup = container.querySelector(\"ef-timegroup\") as EFTimegroup;\n if (!timegroup) {\n throw new Error(\n \"[TimelineRoot] No ef-timegroup found in component. \" +\n \"Ensure your component renders a Timegroup.\",\n );\n }\n\n // Register a clone factory for this element.\n // When createRenderClone is called, it will use this factory\n // to mount a fresh React tree instead of cloning dead DOM.\n registerCloneFactory(timegroup, (cloneContainer: HTMLElement) => {\n const root = ReactDOM.createRoot(cloneContainer);\n const Comp = componentRef.current;\n flushSync(() => {\n root.render(<Comp id={id} />);\n });\n\n const newTimegroup = cloneContainer.querySelector(\n \"ef-timegroup\",\n ) as EFTimegroup | null;\n if (!newTimegroup) {\n throw new Error(\n \"[TimelineRoot] Clone factory did not produce an ef-timegroup. \" +\n \"Ensure your component renders a Timegroup.\",\n );\n }\n\n return {\n timegroup: newTimegroup,\n cleanup: () => {\n queueMicrotask(() => {\n root.unmount();\n });\n },\n };\n });\n\n return () => {\n unregisterCloneFactory(timegroup);\n };\n }, [id]);\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{ display: \"contents\", ...style }}\n >\n {children}\n <Component id={id} />\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAaA,gBAA6C,EACxD,IACA,WAAW,WACX,WACA,OACA,eACI;CACJ,MAAM,eAAe,OAAuB,KAAK;CACjD,MAAM,eAAe,OAAO,UAAU;AACtC,cAAa,UAAU;AAEvB,iBAAgB;EACd,MAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;EAGhB,MAAM,YAAY,UAAU,cAAc,eAAe;AACzD,MAAI,CAAC,UACH,OAAM,IAAI,MACR,gGAED;AAMH,uBAAqB,YAAY,mBAAgC;GAC/D,MAAM,OAAO,SAAS,WAAW,eAAe;GAChD,MAAM,OAAO,aAAa;AAC1B,mBAAgB;AACd,SAAK,OAAO,oBAAC,QAAS,KAAM,CAAC;KAC7B;GAEF,MAAM,eAAe,eAAe,cAClC,eACD;AACD,OAAI,CAAC,aACH,OAAM,IAAI,MACR,2GAED;AAGH,UAAO;IACL,WAAW;IACX,eAAe;AACb,0BAAqB;AACnB,WAAK,SAAS;OACd;;IAEL;IACD;AAEF,eAAa;AACX,0BAAuB,UAAU;;IAElC,CAAC,GAAG,CAAC;AAER,QACE,qBAAC;EACC,KAAK;EACM;EACX,OAAO;GAAE,SAAS;GAAY,GAAG;GAAO;aAEvC,UACD,oBAAC,aAAc,KAAM;GACjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"OverlayItem.js","names":["EFOverlayItemElement"],"sources":["../../src/gui/OverlayItem.tsx"],"sourcesContent":["import {\n EFOverlayItem as EFOverlayItemElement,\n} from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface OverlayItemProps {\n /** Element ID - automatically resolves to [data-element-id] or [data-timegroup-id] selector */\n elementId?: string;\n /** Target element or selector - use for custom targeting when elementId doesn't work */\n target?: HTMLElement | string;\n /** Called when position changes. Receives CustomEvent with OverlayItemPosition as detail. */\n onPositionChanged?: (e: Event) => void;\n}\n\nexport const OverlayItem = createComponent<\n EFOverlayItemElement,\n { onPositionChanged: \"position-changed\" }\n>({\n tagName: \"ef-overlay-item\",\n elementClass: EFOverlayItemElement,\n react: React,\n displayName: \"OverlayItem\",\n events: {\n onPositionChanged: \"position-changed\",\n },\n}) as React.ForwardRefExoticComponent<OverlayItemProps & React.RefAttributes<EFOverlayItemElement>>;\n"],"mappings":";;;;;AAeA,MAAa,cAAc,gBAGzB;CACA,SAAS;CACT,cAAcA;CACd,OAAO;CACP,aAAa;CACb,QAAQ,EACN,mBAAmB,oBACpB;CACF,CAAC"}
1
+ {"version":3,"file":"OverlayItem.js","names":["EFOverlayItemElement"],"sources":["../../src/gui/OverlayItem.tsx"],"sourcesContent":["import { EFOverlayItem as EFOverlayItemElement } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface OverlayItemProps {\n /** Element ID - automatically resolves to [data-element-id] or [data-timegroup-id] selector */\n elementId?: string;\n /** Target element or selector - use for custom targeting when elementId doesn't work */\n target?: HTMLElement | string;\n /** Called when position changes. Receives CustomEvent with OverlayItemPosition as detail. */\n onPositionChanged?: (e: Event) => void;\n}\n\nexport const OverlayItem = createComponent<\n EFOverlayItemElement,\n { onPositionChanged: \"position-changed\" }\n>({\n tagName: \"ef-overlay-item\",\n elementClass: EFOverlayItemElement,\n react: React,\n displayName: \"OverlayItem\",\n events: {\n onPositionChanged: \"position-changed\",\n },\n}) as React.ForwardRefExoticComponent<\n OverlayItemProps & React.RefAttributes<EFOverlayItemElement>\n>;\n"],"mappings":";;;;;AAaA,MAAa,cAAc,gBAGzB;CACA,SAAS;CACT,cAAcA;CACd,OAAO;CACP,aAAa;CACb,QAAQ,EACN,mBAAmB,oBACpB;CACF,CAAC"}
@@ -13,6 +13,8 @@ interface ScrubberProps {
13
13
  scrollContainerRef?: React.RefObject<HTMLElement>;
14
14
  isScrubbingRef?: React.MutableRefObject<boolean>;
15
15
  onSeek?: (time: number) => void;
16
+ className?: string;
17
+ style?: React.CSSProperties;
16
18
  }
17
19
  declare const Scrubber: React.ForwardRefExoticComponent<ScrubberProps & React.RefAttributes<EFScrubber>>;
18
20
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"Scrubber.js","names":[],"sources":["../../src/gui/Scrubber.tsx"],"sourcesContent":["import { EFScrubber } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface ScrubberProps {\n orientation?: \"horizontal\" | \"vertical\";\n currentTimeMs?: number;\n durationMs?: number;\n zoomScale?: number;\n containerWidth?: number;\n fps?: number;\n rawScrubTimeMs?: number | null;\n scrollContainerRef?: React.RefObject<HTMLElement>;\n isScrubbingRef?: React.MutableRefObject<boolean>;\n onSeek?: (time: number) => void;\n}\n\nconst BaseScrubber = createComponent<\n EFScrubber,\n { onSeek: \"seek\" }\n>({\n tagName: \"ef-scrubber\",\n elementClass: EFScrubber,\n react: React,\n displayName: \"Scrubber\",\n events: {\n onSeek: \"seek\",\n },\n}) as React.ForwardRefExoticComponent<ScrubberProps & React.RefAttributes<EFScrubber>>;\n\nexport const Scrubber = React.forwardRef<EFScrubber, ScrubberProps>(\n (props, ref) => {\n const { scrollContainerRef, isScrubbingRef, ...restProps } = props;\n const elementRef = React.useRef<EFScrubber | null>(null);\n\n React.useLayoutEffect(() => {\n if (elementRef.current) {\n if (scrollContainerRef?.current) {\n elementRef.current.scrollContainerRef = scrollContainerRef;\n } else {\n elementRef.current.scrollContainerRef = undefined;\n }\n if (isScrubbingRef) {\n elementRef.current.isScrubbingRef = isScrubbingRef;\n } else {\n elementRef.current.isScrubbingRef = undefined;\n }\n }\n }, [scrollContainerRef?.current, isScrubbingRef]);\n\n return (\n <BaseScrubber\n {...restProps}\n ref={(node) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n />\n );\n },\n);\n\nScrubber.displayName = \"Scrubber\";\n"],"mappings":";;;;;;AAiBA,MAAM,eAAe,gBAGnB;CACA,SAAS;CACT,cAAc;CACd,OAAO;CACP,aAAa;CACb,QAAQ,EACN,QAAQ,QACT;CACF,CAAC;AAEF,MAAa,WAAW,MAAM,YAC3B,OAAO,QAAQ;CACd,MAAM,EAAE,oBAAoB,eAAgB,GAAG,cAAc;CAC7D,MAAM,aAAa,MAAM,OAA0B,KAAK;AAExD,OAAM,sBAAsB;AAC1B,MAAI,WAAW,SAAS;AACtB,OAAI,oBAAoB,QACtB,YAAW,QAAQ,qBAAqB;OAExC,YAAW,QAAQ,qBAAqB;AAE1C,OAAI,eACF,YAAW,QAAQ,iBAAiB;OAEpC,YAAW,QAAQ,iBAAiB;;IAGvC,CAAC,oBAAoB,SAAS,eAAe,CAAC;AAEjD,QACE,oBAAC;EACC,GAAI;EACJ,MAAM,SAAS;AACb,cAAW,UAAU;AACrB,OAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;YAC/B,IAAK,KAAI,UAAU;;GAE9B;EAGP;AAED,SAAS,cAAc"}
1
+ {"version":3,"file":"Scrubber.js","names":[],"sources":["../../src/gui/Scrubber.tsx"],"sourcesContent":["import { EFScrubber } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface ScrubberProps {\n orientation?: \"horizontal\" | \"vertical\";\n currentTimeMs?: number;\n durationMs?: number;\n zoomScale?: number;\n containerWidth?: number;\n fps?: number;\n rawScrubTimeMs?: number | null;\n scrollContainerRef?: React.RefObject<HTMLElement>;\n isScrubbingRef?: React.MutableRefObject<boolean>;\n onSeek?: (time: number) => void;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst BaseScrubber = createComponent<EFScrubber, { onSeek: \"seek\" }>({\n tagName: \"ef-scrubber\",\n elementClass: EFScrubber,\n react: React,\n displayName: \"Scrubber\",\n events: {\n onSeek: \"seek\",\n },\n}) as React.ForwardRefExoticComponent<\n ScrubberProps & React.RefAttributes<EFScrubber>\n>;\n\nexport const Scrubber = React.forwardRef<EFScrubber, ScrubberProps>(\n (props, ref) => {\n const { scrollContainerRef, isScrubbingRef, ...restProps } = props;\n const elementRef = React.useRef<EFScrubber | null>(null);\n\n React.useLayoutEffect(() => {\n if (elementRef.current) {\n if (scrollContainerRef?.current) {\n elementRef.current.scrollContainerRef = scrollContainerRef;\n } else {\n elementRef.current.scrollContainerRef = undefined;\n }\n if (isScrubbingRef) {\n elementRef.current.isScrubbingRef = isScrubbingRef;\n } else {\n elementRef.current.isScrubbingRef = undefined;\n }\n }\n }, [scrollContainerRef?.current, isScrubbingRef]);\n\n return (\n <BaseScrubber\n {...restProps}\n ref={(node) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n />\n );\n },\n);\n\nScrubber.displayName = \"Scrubber\";\n"],"mappings":";;;;;;AAmBA,MAAM,eAAe,gBAAgD;CACnE,SAAS;CACT,cAAc;CACd,OAAO;CACP,aAAa;CACb,QAAQ,EACN,QAAQ,QACT;CACF,CAAC;AAIF,MAAa,WAAW,MAAM,YAC3B,OAAO,QAAQ;CACd,MAAM,EAAE,oBAAoB,eAAgB,GAAG,cAAc;CAC7D,MAAM,aAAa,MAAM,OAA0B,KAAK;AAExD,OAAM,sBAAsB;AAC1B,MAAI,WAAW,SAAS;AACtB,OAAI,oBAAoB,QACtB,YAAW,QAAQ,qBAAqB;OAExC,YAAW,QAAQ,qBAAqB;AAE1C,OAAI,eACF,YAAW,QAAQ,iBAAiB;OAEpC,YAAW,QAAQ,iBAAiB;;IAGvC,CAAC,oBAAoB,SAAS,eAAe,CAAC;AAEjD,QACE,oBAAC;EACC,GAAI;EACJ,MAAM,SAAS;AACb,cAAW,UAAU;AACrB,OAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;YAC/B,IAAK,KAAI,UAAU;;GAE9B;EAGP;AAED,SAAS,cAAc"}
@@ -1,7 +1,7 @@
1
1
  import { ReactWebComponent } from "../hooks/create-element.js";
2
2
  import { EFThumbnailStrip } from "@editframe/elements";
3
3
 
4
- //#region src/elements/ThumbnailStrip.d.ts
4
+ //#region src/gui/ThumbnailStrip.d.ts
5
5
  declare const ThumbnailStrip: ReactWebComponent<EFThumbnailStrip, {}>;
6
6
  //#endregion
7
7
  export { ThumbnailStrip };
@@ -2,13 +2,11 @@ import { createComponent } from "../hooks/create-element.js";
2
2
  import { EFThumbnailStrip } from "@editframe/elements";
3
3
  import React from "react";
4
4
 
5
- //#region src/elements/ThumbnailStrip.ts
5
+ //#region src/gui/ThumbnailStrip.tsx
6
6
  const ThumbnailStrip = createComponent({
7
7
  tagName: "ef-thumbnail-strip",
8
8
  elementClass: EFThumbnailStrip,
9
- react: React,
10
- displayName: "ThumbnailStrip",
11
- events: {}
9
+ react: React
12
10
  });
13
11
 
14
12
  //#endregion
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThumbnailStrip.js","names":["EFThumbnailStripElement"],"sources":["../../src/gui/ThumbnailStrip.tsx"],"sourcesContent":["import { EFThumbnailStrip as EFThumbnailStripElement } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport const ThumbnailStrip = createComponent({\n tagName: \"ef-thumbnail-strip\",\n elementClass: EFThumbnailStripElement,\n react: React,\n});\n"],"mappings":";;;;;AAIA,MAAa,iBAAiB,gBAAgB;CAC5C,SAAS;CACT,cAAcA;CACd,OAAO;CACR,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineRuler.js","names":[],"sources":["../../src/gui/TimelineRuler.tsx"],"sourcesContent":["import { EFTimelineRuler } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface TimelineRulerProps {\n durationMs?: number;\n zoomScale?: number;\n containerWidth?: number;\n fps?: number;\n scrollContainerSelector?: string;\n scrollContainerRef?: React.RefObject<HTMLElement>;\n}\n\nconst BaseTimelineRuler = createComponent<\n EFTimelineRuler,\n {}\n>({\n tagName: \"ef-timeline-ruler\",\n elementClass: EFTimelineRuler,\n react: React,\n displayName: \"TimelineRuler\",\n}) as React.ForwardRefExoticComponent<TimelineRulerProps & React.RefAttributes<EFTimelineRuler>>;\n\nexport const TimelineRuler = React.forwardRef<\n EFTimelineRuler,\n TimelineRulerProps\n>((props, ref) => {\n const { scrollContainerRef, ...restProps } = props;\n const elementRef = React.useRef<EFTimelineRuler | null>(null);\n\n React.useLayoutEffect(() => {\n if (elementRef.current && scrollContainerRef?.current) {\n (elementRef.current as any).scrollContainerElement = scrollContainerRef.current;\n } else if (elementRef.current) {\n (elementRef.current as any).scrollContainerElement = null;\n }\n }, [scrollContainerRef?.current]);\n\n return (\n <BaseTimelineRuler\n {...(restProps as any)}\n ref={(node) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n />\n );\n});\n\nTimelineRuler.displayName = \"TimelineRuler\";\n"],"mappings":";;;;;;AAaA,MAAM,oBAAoB,gBAGxB;CACA,SAAS;CACT,cAAc;CACd,OAAO;CACP,aAAa;CACd,CAAC;AAEF,MAAa,gBAAgB,MAAM,YAGhC,OAAO,QAAQ;CAChB,MAAM,EAAE,mBAAoB,GAAG,cAAc;CAC7C,MAAM,aAAa,MAAM,OAA+B,KAAK;AAE7D,OAAM,sBAAsB;AAC1B,MAAI,WAAW,WAAW,oBAAoB,QAC5C,CAAC,WAAW,QAAgB,yBAAyB,mBAAmB;WAC/D,WAAW,QACpB,CAAC,WAAW,QAAgB,yBAAyB;IAEtD,CAAC,oBAAoB,QAAQ,CAAC;AAEjC,QACE,oBAAC;EACC,GAAK;EACL,MAAM,SAAS;AACb,cAAW,UAAU;AACrB,OAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;YAC/B,IAAK,KAAI,UAAU;;GAE9B;EAEJ;AAEF,cAAc,cAAc"}
1
+ {"version":3,"file":"TimelineRuler.js","names":[],"sources":["../../src/gui/TimelineRuler.tsx"],"sourcesContent":["import { EFTimelineRuler } from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent } from \"../hooks/create-element\";\n\nexport interface TimelineRulerProps {\n durationMs?: number;\n zoomScale?: number;\n containerWidth?: number;\n fps?: number;\n scrollContainerSelector?: string;\n scrollContainerRef?: React.RefObject<HTMLElement>;\n}\n\nconst BaseTimelineRuler = createComponent<EFTimelineRuler, {}>({\n tagName: \"ef-timeline-ruler\",\n elementClass: EFTimelineRuler,\n react: React,\n displayName: \"TimelineRuler\",\n}) as React.ForwardRefExoticComponent<\n TimelineRulerProps & React.RefAttributes<EFTimelineRuler>\n>;\n\nexport const TimelineRuler = React.forwardRef<\n EFTimelineRuler,\n TimelineRulerProps\n>((props, ref) => {\n const { scrollContainerRef, ...restProps } = props;\n const elementRef = React.useRef<EFTimelineRuler | null>(null);\n\n React.useLayoutEffect(() => {\n if (elementRef.current && scrollContainerRef?.current) {\n (elementRef.current as any).scrollContainerElement =\n scrollContainerRef.current;\n } else if (elementRef.current) {\n (elementRef.current as any).scrollContainerElement = null;\n }\n }, [scrollContainerRef?.current]);\n\n return (\n <BaseTimelineRuler\n {...(restProps as any)}\n ref={(node) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }}\n />\n );\n});\n\nTimelineRuler.displayName = \"TimelineRuler\";\n"],"mappings":";;;;;;AAaA,MAAM,oBAAoB,gBAAqC;CAC7D,SAAS;CACT,cAAc;CACd,OAAO;CACP,aAAa;CACd,CAAC;AAIF,MAAa,gBAAgB,MAAM,YAGhC,OAAO,QAAQ;CAChB,MAAM,EAAE,mBAAoB,GAAG,cAAc;CAC7C,MAAM,aAAa,MAAM,OAA+B,KAAK;AAE7D,OAAM,sBAAsB;AAC1B,MAAI,WAAW,WAAW,oBAAoB,QAC5C,CAAC,WAAW,QAAgB,yBAC1B,mBAAmB;WACZ,WAAW,QACpB,CAAC,WAAW,QAAgB,yBAAyB;IAEtD,CAAC,oBAAoB,QAAQ,CAAC;AAEjC,QACE,oBAAC;EACC,GAAK;EACL,MAAM,SAAS;AACb,cAAW,UAAU;AACrB,OAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;YAC/B,IAAK,KAAI,UAAU;;GAE9B;EAEJ;AAEF,cAAc,cAAc"}
@@ -0,0 +1,11 @@
1
+ import { EventName, ReactWebComponent } from "../hooks/create-element.js";
2
+ import { EFTrimHandles, TrimChangeDetail } from "@editframe/elements";
3
+
4
+ //#region src/gui/TrimHandles.d.ts
5
+ declare const TrimHandles: ReactWebComponent<EFTrimHandles, {
6
+ onTrimChange: EventName<CustomEvent<TrimChangeDetail>>;
7
+ onTrimChangeEnd: EventName<CustomEvent<TrimChangeDetail>>;
8
+ }>;
9
+ //#endregion
10
+ export { TrimHandles };
11
+ //# sourceMappingURL=TrimHandles.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { createComponent } from "../hooks/create-element.js";
2
+ import { EFTrimHandles } from "@editframe/elements";
3
+ import React from "react";
4
+
5
+ //#region src/gui/TrimHandles.ts
6
+ const TrimHandles = createComponent({
7
+ tagName: "ef-trim-handles",
8
+ elementClass: EFTrimHandles,
9
+ react: React,
10
+ events: {
11
+ onTrimChange: "trim-change",
12
+ onTrimChangeEnd: "trim-change-end"
13
+ }
14
+ });
15
+
16
+ //#endregion
17
+ export { TrimHandles };
18
+ //# sourceMappingURL=TrimHandles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TrimHandles.js","names":["EFTrimHandlesElement"],"sources":["../../src/gui/TrimHandles.ts"],"sourcesContent":["import {\n EFTrimHandles as EFTrimHandlesElement,\n type TrimChangeDetail,\n} from \"@editframe/elements\";\nimport React from \"react\";\nimport { createComponent, type EventName } from \"../hooks/create-element\";\n\nexport const TrimHandles = createComponent({\n tagName: \"ef-trim-handles\",\n elementClass: EFTrimHandlesElement,\n react: React,\n events: {\n onTrimChange: \"trim-change\" as EventName<CustomEvent<TrimChangeDetail>>,\n onTrimChangeEnd: \"trim-change-end\" as EventName<\n CustomEvent<TrimChangeDetail>\n >,\n },\n});\n"],"mappings":";;;;;AAOA,MAAa,cAAc,gBAAgB;CACzC,SAAS;CACT,cAAcA;CACd,OAAO;CACP,QAAQ;EACN,cAAc;EACd,iBAAiB;EAGlB;CACF,CAAC"}
@@ -2,11 +2,18 @@ import React from "react";
2
2
 
3
3
  //#region src/hooks/create-element.d.ts
4
4
 
5
- type EventNames = Record<string, string>;
6
- type EventListeners<E extends EventNames> = { [K in keyof E]?: (e: Event) => void };
5
+ /**
6
+ * Branded string that carries the DOM event type at the type level.
7
+ * At runtime it's just a string (the DOM event name).
8
+ */
9
+ type EventName<T$1 extends Event = Event> = string & {
10
+ __eventType?: T$1;
11
+ };
12
+ type EventNames = Record<string, EventName>;
13
+ type EventListeners<E extends EventNames> = { [K in keyof E]?: (e: E[K] extends EventName<infer T> ? T : Event) => void };
7
14
  type ElementProps<I> = Partial<Omit<I, keyof HTMLElement>>;
8
15
  type ComponentProps<I, E extends EventNames = {}> = Omit<React.HTMLAttributes<I>, keyof E | keyof ElementProps<I>> & EventListeners<E> & ElementProps<I>;
9
16
  type ReactWebComponent<I extends HTMLElement, E extends EventNames = {}> = React.ForwardRefExoticComponent<ComponentProps<I, E> & React.RefAttributes<I>>;
10
17
  //#endregion
11
- export { ReactWebComponent };
18
+ export { EventName, ReactWebComponent };
12
19
  //# sourceMappingURL=create-element.d.ts.map
@@ -48,7 +48,10 @@ function createComponent({ react: React$1, tagName, elementClass, events, displa
48
48
  reactProps[k === "className" ? "class" : k] = v;
49
49
  continue;
50
50
  }
51
- if (eventProps.has(k) || k in elementClass.prototype) elementProps[k] = v;
51
+ if (eventProps.has(k) || k in elementClass.prototype) {
52
+ elementProps[k] = v;
53
+ continue;
54
+ }
52
55
  reactProps[k] = v;
53
56
  }
54
57
  isomorphicEffect(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"create-element.js","names":["React","reactProps: Record<string, unknown>","elementProps: Record<string, unknown>"],"sources":["../../src/hooks/create-element.ts"],"sourcesContent":["import React from \"react\";\n\nlet isomorphicEffect =\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nexport function setIsomorphicEffect(\n effect: typeof React.useLayoutEffect | typeof React.useEffect,\n) {\n isomorphicEffect = effect;\n}\n\nconst reservedReactProperties = new Set([\n \"children\",\n \"localName\",\n \"ref\",\n \"style\",\n \"className\",\n]);\nconst listenedEvents = new WeakMap<Element, Map<string, EventListenerObject>>();\n\ntype Constructor<T> = { new (): T };\ntype EventNames = Record<string, string>;\n\ntype EventListeners<E extends EventNames> = {\n [K in keyof E]?: (e: Event) => void;\n};\n\ntype ElementProps<I> = Partial<Omit<I, keyof HTMLElement>>;\ntype ComponentProps<I, E extends EventNames = {}> = Omit<\n React.HTMLAttributes<I>,\n keyof E | keyof ElementProps<I>\n> &\n EventListeners<E> &\n ElementProps<I>;\n\nexport type ReactWebComponent<\n I extends HTMLElement,\n E extends EventNames = {},\n> = React.ForwardRefExoticComponent<\n ComponentProps<I, E> & React.RefAttributes<I>\n>;\n\nexport interface Options<I extends HTMLElement, E extends EventNames = {}> {\n react: typeof React;\n tagName: string;\n elementClass: Constructor<I>;\n events?: E;\n displayName?: string;\n}\n\nfunction addOrUpdateEventListener(\n node: Element,\n event: string,\n listener?: (e?: Event) => void,\n) {\n let events = listenedEvents.get(node);\n if (!events) {\n events = new Map();\n listenedEvents.set(node, events);\n }\n let handler = events.get(event);\n\n if (listener) {\n if (!handler) {\n handler = { handleEvent: listener };\n events.set(event, handler);\n node.addEventListener(event, handler);\n } else {\n handler.handleEvent = listener;\n }\n } else if (handler) {\n events.delete(event);\n node.removeEventListener(event, handler);\n }\n}\n\nfunction setProperty<E extends Element>(\n node: E,\n name: string,\n value: unknown,\n old: unknown,\n events?: EventNames,\n) {\n const event = events?.[name];\n if (event) {\n if (value !== old)\n addOrUpdateEventListener(node, event, value as (e?: Event) => void);\n return;\n }\n node[name as keyof E] = value as E[keyof E];\n if (\n (value === undefined || value === null) &&\n name in HTMLElement.prototype\n ) {\n node.removeAttribute(name);\n }\n}\n\nexport function createComponent<\n I extends HTMLElement,\n E extends EventNames = {},\n>({\n react: React,\n tagName,\n elementClass,\n events,\n displayName,\n}: Options<I, E>): ReactWebComponent<I, E> {\n const eventProps = new Set(Object.keys(events ?? {}));\n\n const ReactComponent = React.forwardRef<I, ComponentProps<I, E>>(\n (props, ref) => {\n const elementRef = React.useRef<I | null>(null);\n const prevPropsRef = React.useRef(new Map<string, unknown>());\n\n const reactProps: Record<string, unknown> = {\n suppressHydrationWarning: true,\n };\n const elementProps: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(props)) {\n if (reservedReactProperties.has(k)) {\n reactProps[k === \"className\" ? \"class\" : k] = v;\n continue;\n }\n if (eventProps.has(k) || k in elementClass.prototype)\n elementProps[k] = v;\n reactProps[k] = v;\n }\n\n isomorphicEffect(() => {\n if (!elementRef.current) return;\n const newProps = new Map<string, unknown>();\n for (const key in elementProps) {\n setProperty(\n elementRef.current,\n key,\n props[key as keyof typeof props],\n prevPropsRef.current.get(key),\n events,\n );\n prevPropsRef.current.delete(key);\n newProps.set(key, props[key as keyof typeof props]);\n }\n for (const [key, value] of prevPropsRef.current) {\n setProperty(elementRef.current, key, undefined, value, events);\n }\n prevPropsRef.current = newProps;\n\n // Remove defer-hydration if present\n elementRef.current.removeAttribute(\"defer-hydration\");\n }, [props]);\n\n return React.createElement(tagName, {\n ...reactProps,\n ref: (node: I) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n },\n });\n },\n );\n\n ReactComponent.displayName = displayName ?? elementClass.name;\n return ReactComponent as ReactWebComponent<I, E>;\n}\n"],"mappings":";;;AAEA,IAAI,mBACF,OAAO,WAAW,cAAc,MAAM,kBAAkB,MAAM;AAQhE,MAAM,0BAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACD,CAAC;AACF,MAAM,iCAAiB,IAAI,SAAoD;AAgC/E,SAAS,yBACP,MACA,OACA,UACA;CACA,IAAI,SAAS,eAAe,IAAI,KAAK;AACrC,KAAI,CAAC,QAAQ;AACX,2BAAS,IAAI,KAAK;AAClB,iBAAe,IAAI,MAAM,OAAO;;CAElC,IAAI,UAAU,OAAO,IAAI,MAAM;AAE/B,KAAI,SACF,KAAI,CAAC,SAAS;AACZ,YAAU,EAAE,aAAa,UAAU;AACnC,SAAO,IAAI,OAAO,QAAQ;AAC1B,OAAK,iBAAiB,OAAO,QAAQ;OAErC,SAAQ,cAAc;UAEf,SAAS;AAClB,SAAO,OAAO,MAAM;AACpB,OAAK,oBAAoB,OAAO,QAAQ;;;AAI5C,SAAS,YACP,MACA,MACA,OACA,KACA,QACA;CACA,MAAM,QAAQ,SAAS;AACvB,KAAI,OAAO;AACT,MAAI,UAAU,IACZ,0BAAyB,MAAM,OAAO,MAA6B;AACrE;;AAEF,MAAK,QAAmB;AACxB,MACG,UAAU,UAAa,UAAU,SAClC,QAAQ,YAAY,UAEpB,MAAK,gBAAgB,KAAK;;AAI9B,SAAgB,gBAGd,EACA,OAAOA,SACP,SACA,cACA,QACA,eACyC;CACzC,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC,CAAC;CAErD,MAAM,iBAAiBA,QAAM,YAC1B,OAAO,QAAQ;EACd,MAAM,aAAaA,QAAM,OAAiB,KAAK;EAC/C,MAAM,eAAeA,QAAM,uBAAO,IAAI,KAAsB,CAAC;EAE7D,MAAMC,aAAsC,EAC1C,0BAA0B,MAC3B;EACD,MAAMC,eAAwC,EAAE;AAEhD,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,EAAE;AAC1C,OAAI,wBAAwB,IAAI,EAAE,EAAE;AAClC,eAAW,MAAM,cAAc,UAAU,KAAK;AAC9C;;AAEF,OAAI,WAAW,IAAI,EAAE,IAAI,KAAK,aAAa,UACzC,cAAa,KAAK;AACpB,cAAW,KAAK;;AAGlB,yBAAuB;AACrB,OAAI,CAAC,WAAW,QAAS;GACzB,MAAM,2BAAW,IAAI,KAAsB;AAC3C,QAAK,MAAM,OAAO,cAAc;AAC9B,gBACE,WAAW,SACX,KACA,MAAM,MACN,aAAa,QAAQ,IAAI,IAAI,EAC7B,OACD;AACD,iBAAa,QAAQ,OAAO,IAAI;AAChC,aAAS,IAAI,KAAK,MAAM,KAA2B;;AAErD,QAAK,MAAM,CAAC,KAAK,UAAU,aAAa,QACtC,aAAY,WAAW,SAAS,KAAK,QAAW,OAAO,OAAO;AAEhE,gBAAa,UAAU;AAGvB,cAAW,QAAQ,gBAAgB,kBAAkB;KACpD,CAAC,MAAM,CAAC;AAEX,SAAOF,QAAM,cAAc,SAAS;GAClC,GAAG;GACH,MAAM,SAAY;AAChB,eAAW,UAAU;AACrB,QAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;aAC/B,IAAK,KAAI,UAAU;;GAE/B,CAAC;GAEL;AAED,gBAAe,cAAc,eAAe,aAAa;AACzD,QAAO"}
1
+ {"version":3,"file":"create-element.js","names":["React","reactProps: Record<string, unknown>","elementProps: Record<string, unknown>"],"sources":["../../src/hooks/create-element.ts"],"sourcesContent":["import React from \"react\";\n\nlet isomorphicEffect =\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nexport function setIsomorphicEffect(\n effect: typeof React.useLayoutEffect | typeof React.useEffect,\n) {\n isomorphicEffect = effect;\n}\n\nconst reservedReactProperties = new Set([\n \"children\",\n \"localName\",\n \"ref\",\n \"style\",\n \"className\",\n]);\nconst listenedEvents = new WeakMap<Element, Map<string, EventListenerObject>>();\n\ntype Constructor<T> = { new (): T };\n\n/**\n * Branded string that carries the DOM event type at the type level.\n * At runtime it's just a string (the DOM event name).\n */\nexport type EventName<T extends Event = Event> = string & { __eventType?: T };\n\ntype EventNames = Record<string, EventName>;\n\ntype EventListeners<E extends EventNames> = {\n [K in keyof E]?: (e: E[K] extends EventName<infer T> ? T : Event) => void;\n};\n\ntype ElementProps<I> = Partial<Omit<I, keyof HTMLElement>>;\ntype ComponentProps<I, E extends EventNames = {}> = Omit<\n React.HTMLAttributes<I>,\n keyof E | keyof ElementProps<I>\n> &\n EventListeners<E> &\n ElementProps<I>;\n\nexport type ReactWebComponent<\n I extends HTMLElement,\n E extends EventNames = {},\n> = React.ForwardRefExoticComponent<\n ComponentProps<I, E> & React.RefAttributes<I>\n>;\n\nexport interface Options<I extends HTMLElement, E extends EventNames = {}> {\n react: typeof React;\n tagName: string;\n elementClass: Constructor<I>;\n events?: E;\n displayName?: string;\n}\n\nfunction addOrUpdateEventListener(\n node: Element,\n event: string,\n listener?: (e?: Event) => void,\n) {\n let events = listenedEvents.get(node);\n if (!events) {\n events = new Map();\n listenedEvents.set(node, events);\n }\n let handler = events.get(event);\n\n if (listener) {\n if (!handler) {\n handler = { handleEvent: listener };\n events.set(event, handler);\n node.addEventListener(event, handler);\n } else {\n handler.handleEvent = listener;\n }\n } else if (handler) {\n events.delete(event);\n node.removeEventListener(event, handler);\n }\n}\n\nfunction setProperty<E extends Element>(\n node: E,\n name: string,\n value: unknown,\n old: unknown,\n events?: EventNames,\n) {\n const event = events?.[name];\n if (event) {\n if (value !== old)\n addOrUpdateEventListener(node, event, value as (e?: Event) => void);\n return;\n }\n node[name as keyof E] = value as E[keyof E];\n if (\n (value === undefined || value === null) &&\n name in HTMLElement.prototype\n ) {\n node.removeAttribute(name);\n }\n}\n\nexport function createComponent<\n I extends HTMLElement,\n E extends EventNames = {},\n>({\n react: React,\n tagName,\n elementClass,\n events,\n displayName,\n}: Options<I, E>): ReactWebComponent<I, E> {\n const eventProps = new Set(Object.keys(events ?? {}));\n\n const ReactComponent = React.forwardRef<I, ComponentProps<I, E>>(\n (props, ref) => {\n const elementRef = React.useRef<I | null>(null);\n const prevPropsRef = React.useRef(new Map<string, unknown>());\n\n const reactProps: Record<string, unknown> = {\n suppressHydrationWarning: true,\n };\n const elementProps: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(props)) {\n if (reservedReactProperties.has(k)) {\n reactProps[k === \"className\" ? \"class\" : k] = v;\n continue;\n }\n if (eventProps.has(k) || k in elementClass.prototype) {\n elementProps[k] = v;\n continue;\n }\n reactProps[k] = v;\n }\n\n isomorphicEffect(() => {\n if (!elementRef.current) return;\n const newProps = new Map<string, unknown>();\n for (const key in elementProps) {\n setProperty(\n elementRef.current,\n key,\n props[key as keyof typeof props],\n prevPropsRef.current.get(key),\n events,\n );\n prevPropsRef.current.delete(key);\n newProps.set(key, props[key as keyof typeof props]);\n }\n for (const [key, value] of prevPropsRef.current) {\n setProperty(elementRef.current, key, undefined, value, events);\n }\n prevPropsRef.current = newProps;\n\n // Remove defer-hydration if present\n elementRef.current.removeAttribute(\"defer-hydration\");\n }, [props]);\n\n return React.createElement(tagName, {\n ...reactProps,\n ref: (node: I) => {\n elementRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n },\n });\n },\n );\n\n ReactComponent.displayName = displayName ?? elementClass.name;\n return ReactComponent as ReactWebComponent<I, E>;\n}\n"],"mappings":";;;AAEA,IAAI,mBACF,OAAO,WAAW,cAAc,MAAM,kBAAkB,MAAM;AAQhE,MAAM,0BAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACD,CAAC;AACF,MAAM,iCAAiB,IAAI,SAAoD;AAuC/E,SAAS,yBACP,MACA,OACA,UACA;CACA,IAAI,SAAS,eAAe,IAAI,KAAK;AACrC,KAAI,CAAC,QAAQ;AACX,2BAAS,IAAI,KAAK;AAClB,iBAAe,IAAI,MAAM,OAAO;;CAElC,IAAI,UAAU,OAAO,IAAI,MAAM;AAE/B,KAAI,SACF,KAAI,CAAC,SAAS;AACZ,YAAU,EAAE,aAAa,UAAU;AACnC,SAAO,IAAI,OAAO,QAAQ;AAC1B,OAAK,iBAAiB,OAAO,QAAQ;OAErC,SAAQ,cAAc;UAEf,SAAS;AAClB,SAAO,OAAO,MAAM;AACpB,OAAK,oBAAoB,OAAO,QAAQ;;;AAI5C,SAAS,YACP,MACA,MACA,OACA,KACA,QACA;CACA,MAAM,QAAQ,SAAS;AACvB,KAAI,OAAO;AACT,MAAI,UAAU,IACZ,0BAAyB,MAAM,OAAO,MAA6B;AACrE;;AAEF,MAAK,QAAmB;AACxB,MACG,UAAU,UAAa,UAAU,SAClC,QAAQ,YAAY,UAEpB,MAAK,gBAAgB,KAAK;;AAI9B,SAAgB,gBAGd,EACA,OAAOA,SACP,SACA,cACA,QACA,eACyC;CACzC,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC,CAAC;CAErD,MAAM,iBAAiBA,QAAM,YAC1B,OAAO,QAAQ;EACd,MAAM,aAAaA,QAAM,OAAiB,KAAK;EAC/C,MAAM,eAAeA,QAAM,uBAAO,IAAI,KAAsB,CAAC;EAE7D,MAAMC,aAAsC,EAC1C,0BAA0B,MAC3B;EACD,MAAMC,eAAwC,EAAE;AAEhD,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,EAAE;AAC1C,OAAI,wBAAwB,IAAI,EAAE,EAAE;AAClC,eAAW,MAAM,cAAc,UAAU,KAAK;AAC9C;;AAEF,OAAI,WAAW,IAAI,EAAE,IAAI,KAAK,aAAa,WAAW;AACpD,iBAAa,KAAK;AAClB;;AAEF,cAAW,KAAK;;AAGlB,yBAAuB;AACrB,OAAI,CAAC,WAAW,QAAS;GACzB,MAAM,2BAAW,IAAI,KAAsB;AAC3C,QAAK,MAAM,OAAO,cAAc;AAC9B,gBACE,WAAW,SACX,KACA,MAAM,MACN,aAAa,QAAQ,IAAI,IAAI,EAC7B,OACD;AACD,iBAAa,QAAQ,OAAO,IAAI;AAChC,aAAS,IAAI,KAAK,MAAM,KAA2B;;AAErD,QAAK,MAAM,CAAC,KAAK,UAAU,aAAa,QACtC,aAAY,WAAW,SAAS,KAAK,QAAW,OAAO,OAAO;AAEhE,gBAAa,UAAU;AAGvB,cAAW,QAAQ,gBAAgB,kBAAkB;KACpD,CAAC,MAAM,CAAC;AAEX,SAAOF,QAAM,cAAc,SAAS;GAClC,GAAG;GACH,MAAM,SAAY;AAChB,eAAW,UAAU;AACrB,QAAI,OAAO,QAAQ,WAAY,KAAI,KAAK;aAC/B,IAAK,KAAI,UAAU;;GAE/B,CAAC;GAEL;AAED,gBAAe,cAAc,eAAe,aAAa;AACzD,QAAO"}
@@ -0,0 +1,15 @@
1
+ import { ReactiveControllerHost } from "lit";
2
+
3
+ //#region src/hooks/useMediaInfo.d.ts
4
+ interface MediaInfo {
5
+ intrinsicDurationMs: number | undefined;
6
+ loading: boolean;
7
+ }
8
+ type MediaElement = ReactiveControllerHost & {
9
+ intrinsicDurationMs: number | undefined;
10
+ isConnected: boolean;
11
+ };
12
+ declare const useMediaInfo: (mediaRef: React.RefObject<MediaElement | null>) => MediaInfo;
13
+ //#endregion
14
+ export { MediaInfo, useMediaInfo };
15
+ //# sourceMappingURL=useMediaInfo.d.ts.map
@@ -0,0 +1,59 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ //#region src/hooks/useMediaInfo.ts
4
+ var MediaInfoController = class {
5
+ #isConnected = false;
6
+ #lastIntrinsicDurationMs = void 0;
7
+ constructor(host, setMediaInfo) {
8
+ this.host = host;
9
+ this.setMediaInfo = setMediaInfo;
10
+ this.host.addController(this);
11
+ }
12
+ hostConnected() {
13
+ this.#isConnected = true;
14
+ }
15
+ hostDisconnected() {
16
+ this.#isConnected = false;
17
+ this.host.removeController(this);
18
+ }
19
+ hostUpdated() {
20
+ if (!this.#isConnected) return;
21
+ const intrinsicDurationMs = this.host.intrinsicDurationMs;
22
+ if (intrinsicDurationMs !== this.#lastIntrinsicDurationMs) {
23
+ this.#lastIntrinsicDurationMs = intrinsicDurationMs;
24
+ this.#updateReactState();
25
+ }
26
+ }
27
+ #updateReactState() {
28
+ const intrinsicDurationMs = this.host.intrinsicDurationMs;
29
+ this.setMediaInfo({
30
+ intrinsicDurationMs,
31
+ loading: intrinsicDurationMs === void 0
32
+ });
33
+ }
34
+ syncNow() {
35
+ this.#updateReactState();
36
+ }
37
+ };
38
+ const useMediaInfo = (mediaRef) => {
39
+ const [mediaInfo, setMediaInfo] = useState({
40
+ intrinsicDurationMs: void 0,
41
+ loading: true
42
+ });
43
+ useEffect(() => {
44
+ if (!mediaRef.current) return;
45
+ const controller = new MediaInfoController(mediaRef.current, setMediaInfo);
46
+ if (mediaRef.current.isConnected) {
47
+ controller.hostConnected();
48
+ controller.syncNow();
49
+ }
50
+ return () => {
51
+ controller.hostDisconnected();
52
+ };
53
+ }, [mediaRef.current]);
54
+ return mediaInfo;
55
+ };
56
+
57
+ //#endregion
58
+ export { useMediaInfo };
59
+ //# sourceMappingURL=useMediaInfo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMediaInfo.js","names":["host: MediaElement","setMediaInfo: React.Dispatch<React.SetStateAction<MediaInfo>>","#isConnected","#lastIntrinsicDurationMs","#updateReactState"],"sources":["../../src/hooks/useMediaInfo.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { useEffect, useState } from \"react\";\n\nexport interface MediaInfo {\n intrinsicDurationMs: number | undefined;\n loading: boolean;\n}\n\ntype MediaElement = ReactiveControllerHost & {\n intrinsicDurationMs: number | undefined;\n isConnected: boolean;\n};\n\nexport class MediaInfoController implements ReactiveController {\n #isConnected = false;\n #lastIntrinsicDurationMs: number | undefined = undefined;\n\n constructor(\n private host: MediaElement,\n private setMediaInfo: React.Dispatch<React.SetStateAction<MediaInfo>>,\n ) {\n this.host.addController(this);\n }\n\n hostConnected(): void {\n this.#isConnected = true;\n }\n\n hostDisconnected(): void {\n this.#isConnected = false;\n this.host.removeController(this);\n }\n\n hostUpdated(): void {\n if (!this.#isConnected) return;\n\n const intrinsicDurationMs = this.host.intrinsicDurationMs;\n\n if (intrinsicDurationMs !== this.#lastIntrinsicDurationMs) {\n this.#lastIntrinsicDurationMs = intrinsicDurationMs;\n this.#updateReactState();\n }\n }\n\n #updateReactState(): void {\n const intrinsicDurationMs = this.host.intrinsicDurationMs;\n this.setMediaInfo({\n intrinsicDurationMs,\n loading: intrinsicDurationMs === undefined,\n });\n }\n\n syncNow(): void {\n this.#updateReactState();\n }\n}\n\nexport const useMediaInfo = (\n mediaRef: React.RefObject<MediaElement | null>,\n) => {\n const [mediaInfo, setMediaInfo] = useState<MediaInfo>({\n intrinsicDurationMs: undefined,\n loading: true,\n });\n\n useEffect(() => {\n if (!mediaRef.current) return;\n\n const controller = new MediaInfoController(mediaRef.current, setMediaInfo);\n\n if (mediaRef.current.isConnected) {\n controller.hostConnected();\n controller.syncNow();\n }\n\n return () => {\n controller.hostDisconnected();\n };\n }, [mediaRef.current]);\n\n return mediaInfo;\n};\n"],"mappings":";;;AAaA,IAAa,sBAAb,MAA+D;CAC7D,eAAe;CACf,2BAA+C;CAE/C,YACE,AAAQA,MACR,AAAQC,cACR;EAFQ;EACA;AAER,OAAK,KAAK,cAAc,KAAK;;CAG/B,gBAAsB;AACpB,QAAKC,cAAe;;CAGtB,mBAAyB;AACvB,QAAKA,cAAe;AACpB,OAAK,KAAK,iBAAiB,KAAK;;CAGlC,cAAoB;AAClB,MAAI,CAAC,MAAKA,YAAc;EAExB,MAAM,sBAAsB,KAAK,KAAK;AAEtC,MAAI,wBAAwB,MAAKC,yBAA0B;AACzD,SAAKA,0BAA2B;AAChC,SAAKC,kBAAmB;;;CAI5B,oBAA0B;EACxB,MAAM,sBAAsB,KAAK,KAAK;AACtC,OAAK,aAAa;GAChB;GACA,SAAS,wBAAwB;GAClC,CAAC;;CAGJ,UAAgB;AACd,QAAKA,kBAAmB;;;AAI5B,MAAa,gBACX,aACG;CACH,MAAM,CAAC,WAAW,gBAAgB,SAAoB;EACpD,qBAAqB;EACrB,SAAS;EACV,CAAC;AAEF,iBAAgB;AACd,MAAI,CAAC,SAAS,QAAS;EAEvB,MAAM,aAAa,IAAI,oBAAoB,SAAS,SAAS,aAAa;AAE1E,MAAI,SAAS,QAAQ,aAAa;AAChC,cAAW,eAAe;AAC1B,cAAW,SAAS;;AAGtB,eAAa;AACX,cAAW,kBAAkB;;IAE9B,CAAC,SAAS,QAAQ,CAAC;AAEtB,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"usePanZoomTransform.js","names":[],"sources":["../../src/hooks/usePanZoomTransform.ts"],"sourcesContent":["import { type PanZoomTransform } from \"@editframe/elements\";\nimport React, { useEffect, useState } from \"react\";\n\n/**\n * Hook to get PanZoom transform values from a PanZoom element ref.\n * Listens to transform-changed events to keep transform values in sync.\n *\n * @param panZoomRef - Ref to the PanZoom element\n * @returns Current transform values (scale, x, y)\n *\n * @example\n * const panZoomRef = useRef<EFPanZoom | null>(null);\n * const transform = usePanZoomTransform(panZoomRef);\n * // transform.scale, transform.x, transform.y\n */\nexport function usePanZoomTransform(\n panZoomRef: React.RefObject<(EventTarget & { scale: number; x: number; y: number }) | null>,\n): PanZoomTransform {\n const [transform, setTransform] = useState<PanZoomTransform>({\n scale: 1,\n x: 0,\n y: 0,\n });\n\n useEffect(() => {\n const panZoom = panZoomRef.current;\n if (!panZoom) {\n return;\n }\n\n // Initialize with current values\n setTransform({\n scale: panZoom.scale ?? 1,\n x: panZoom.x ?? 0,\n y: panZoom.y ?? 0,\n });\n\n // Listen for transform changes\n const handleTransformChanged = (e: Event) => {\n const customEvent = e as CustomEvent<PanZoomTransform>;\n if (customEvent.detail) {\n setTransform(customEvent.detail);\n } else {\n // Fallback to reading from element if detail is not available\n setTransform({\n scale: panZoom.scale ?? 1,\n x: panZoom.x ?? 0,\n y: panZoom.y ?? 0,\n });\n }\n };\n\n panZoom.addEventListener(\"transform-changed\", handleTransformChanged);\n\n return () => {\n panZoom.removeEventListener(\"transform-changed\", handleTransformChanged);\n };\n }, [panZoomRef]);\n\n return transform;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,oBACd,YACkB;CAClB,MAAM,CAAC,WAAW,gBAAgB,SAA2B;EAC3D,OAAO;EACP,GAAG;EACH,GAAG;EACJ,CAAC;AAEF,iBAAgB;EACd,MAAM,UAAU,WAAW;AAC3B,MAAI,CAAC,QACH;AAIF,eAAa;GACX,OAAO,QAAQ,SAAS;GACxB,GAAG,QAAQ,KAAK;GAChB,GAAG,QAAQ,KAAK;GACjB,CAAC;EAGF,MAAM,0BAA0B,MAAa;GAC3C,MAAM,cAAc;AACpB,OAAI,YAAY,OACd,cAAa,YAAY,OAAO;OAGhC,cAAa;IACX,OAAO,QAAQ,SAAS;IACxB,GAAG,QAAQ,KAAK;IAChB,GAAG,QAAQ,KAAK;IACjB,CAAC;;AAIN,UAAQ,iBAAiB,qBAAqB,uBAAuB;AAErE,eAAa;AACX,WAAQ,oBAAoB,qBAAqB,uBAAuB;;IAEzE,CAAC,WAAW,CAAC;AAEhB,QAAO"}
1
+ {"version":3,"file":"usePanZoomTransform.js","names":[],"sources":["../../src/hooks/usePanZoomTransform.ts"],"sourcesContent":["import { type PanZoomTransform } from \"@editframe/elements\";\nimport React, { useEffect, useState } from \"react\";\n\n/**\n * Hook to get PanZoom transform values from a PanZoom element ref.\n * Listens to transform-changed events to keep transform values in sync.\n *\n * @param panZoomRef - Ref to the PanZoom element\n * @returns Current transform values (scale, x, y)\n *\n * @example\n * const panZoomRef = useRef<EFPanZoom | null>(null);\n * const transform = usePanZoomTransform(panZoomRef);\n * // transform.scale, transform.x, transform.y\n */\nexport function usePanZoomTransform(\n panZoomRef: React.RefObject<\n (EventTarget & { scale: number; x: number; y: number }) | null\n >,\n): PanZoomTransform {\n const [transform, setTransform] = useState<PanZoomTransform>({\n scale: 1,\n x: 0,\n y: 0,\n });\n\n useEffect(() => {\n const panZoom = panZoomRef.current;\n if (!panZoom) {\n return;\n }\n\n // Initialize with current values\n setTransform({\n scale: panZoom.scale ?? 1,\n x: panZoom.x ?? 0,\n y: panZoom.y ?? 0,\n });\n\n // Listen for transform changes\n const handleTransformChanged = (e: Event) => {\n const customEvent = e as CustomEvent<PanZoomTransform>;\n if (customEvent.detail) {\n setTransform(customEvent.detail);\n } else {\n // Fallback to reading from element if detail is not available\n setTransform({\n scale: panZoom.scale ?? 1,\n x: panZoom.x ?? 0,\n y: panZoom.y ?? 0,\n });\n }\n };\n\n panZoom.addEventListener(\"transform-changed\", handleTransformChanged);\n\n return () => {\n panZoom.removeEventListener(\"transform-changed\", handleTransformChanged);\n };\n }, [panZoomRef]);\n\n return transform;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,oBACd,YAGkB;CAClB,MAAM,CAAC,WAAW,gBAAgB,SAA2B;EAC3D,OAAO;EACP,GAAG;EACH,GAAG;EACJ,CAAC;AAEF,iBAAgB;EACd,MAAM,UAAU,WAAW;AAC3B,MAAI,CAAC,QACH;AAIF,eAAa;GACX,OAAO,QAAQ,SAAS;GACxB,GAAG,QAAQ,KAAK;GAChB,GAAG,QAAQ,KAAK;GACjB,CAAC;EAGF,MAAM,0BAA0B,MAAa;GAC3C,MAAM,cAAc;AACpB,OAAI,YAAY,OACd,cAAa,YAAY,OAAO;OAGhC,cAAa;IACX,OAAO,QAAQ,SAAS;IACxB,GAAG,QAAQ,KAAK;IAChB,GAAG,QAAQ,KAAK;IACjB,CAAC;;AAIN,UAAQ,iBAAiB,qBAAqB,uBAAuB;AAErE,eAAa;AACX,WAAQ,oBAAoB,qBAAqB,uBAAuB;;IAEzE,CAAC,WAAW,CAAC;AAEhB,QAAO"}
@@ -2,8 +2,8 @@ import { EFTimegroup } from "@editframe/elements";
2
2
  import * as react0 from "react";
3
3
 
4
4
  //#region src/hooks/useTimingInfo.d.ts
5
- declare const useTimingInfo: (timegroupRef?: React.RefObject<EFTimegroup>) => {
6
- ref: react0.RefObject<EFTimegroup>;
5
+ declare const useTimingInfo: (timegroupRef?: React.RefObject<EFTimegroup | null>) => {
6
+ ref: react0.RefObject<EFTimegroup | null>;
7
7
  ownCurrentTimeMs: number;
8
8
  durationMs: number;
9
9
  percentComplete: number;
@@ -2,8 +2,9 @@ import { useEffect, useRef, useState } from "react";
2
2
 
3
3
  //#region src/hooks/useTimingInfo.ts
4
4
  var CurrentTimeController = class {
5
- #lastTaskPromise = null;
6
5
  #isConnected = false;
6
+ #lastOwnCurrentTimeMs = NaN;
7
+ #lastDurationMs = NaN;
7
8
  constructor(host, setCurrentTime) {
8
9
  this.host = host;
9
10
  this.setCurrentTime = setCurrentTime;
@@ -14,23 +15,23 @@ var CurrentTimeController = class {
14
15
  }
15
16
  hostDisconnected() {
16
17
  this.#isConnected = false;
17
- this.#lastTaskPromise = null;
18
18
  this.host.removeController(this);
19
19
  }
20
20
  hostUpdated() {
21
- const currentTaskPromise = this.host.frameTask.taskComplete;
22
- if (currentTaskPromise !== this.#lastTaskPromise) {
23
- this.#lastTaskPromise = currentTaskPromise;
24
- currentTaskPromise.then(() => {
25
- if (this.#isConnected) this.#updateReactState();
26
- }).catch(() => {});
21
+ if (!this.#isConnected) return;
22
+ const { ownCurrentTimeMs, durationMs } = this.host;
23
+ if (ownCurrentTimeMs !== this.#lastOwnCurrentTimeMs || durationMs !== this.#lastDurationMs) {
24
+ this.#lastOwnCurrentTimeMs = ownCurrentTimeMs;
25
+ this.#lastDurationMs = durationMs;
26
+ this.#updateReactState();
27
27
  }
28
28
  }
29
29
  #updateReactState() {
30
+ const { ownCurrentTimeMs, durationMs } = this.host;
30
31
  this.setCurrentTime({
31
- ownCurrentTimeMs: this.host.ownCurrentTimeMs,
32
- durationMs: this.host.durationMs,
33
- percentComplete: this.host.ownCurrentTimeMs / this.host.durationMs
32
+ ownCurrentTimeMs,
33
+ durationMs,
34
+ percentComplete: durationMs > 0 ? ownCurrentTimeMs / durationMs : 0
34
35
  });
35
36
  }
36
37
  syncNow() {
@@ -1 +1 @@
1
- {"version":3,"file":"useTimingInfo.js","names":["host: {\n ownCurrentTimeMs: number;\n durationMs: number;\n frameTask: Task<readonly unknown[], unknown>;\n } & ReactiveControllerHost","setCurrentTime: React.Dispatch<React.SetStateAction<TimeInfo>>","#isConnected","#lastTaskPromise","#updateReactState"],"sources":["../../src/hooks/useTimingInfo.ts"],"sourcesContent":["import type { EFTimegroup } from \"@editframe/elements\";\nimport type { Task } from \"@lit/task\";\nimport type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { useEffect, useRef, useState } from \"react\";\n\ninterface TimeInfo {\n ownCurrentTimeMs: number;\n durationMs: number;\n percentComplete: number;\n}\n\nclass CurrentTimeController implements ReactiveController {\n #lastTaskPromise: Promise<unknown> | null = null;\n #isConnected = false;\n\n constructor(\n private host: {\n ownCurrentTimeMs: number;\n durationMs: number;\n frameTask: Task<readonly unknown[], unknown>;\n } & ReactiveControllerHost,\n private setCurrentTime: React.Dispatch<React.SetStateAction<TimeInfo>>,\n ) {\n this.host.addController(this);\n }\n\n hostConnected(): void {\n this.#isConnected = true;\n }\n\n hostDisconnected(): void {\n this.#isConnected = false;\n this.#lastTaskPromise = null;\n this.host.removeController(this);\n }\n\n hostUpdated(): void {\n const currentTaskPromise = this.host.frameTask.taskComplete;\n\n // Detect if a new frame task has started (promise reference changed)\n if (currentTaskPromise !== this.#lastTaskPromise) {\n this.#lastTaskPromise = currentTaskPromise;\n\n // Wait for this specific task to complete, then update React\n // This is async so it doesn't block the update cycle\n currentTaskPromise\n .then(() => {\n // Only update if still connected\n if (this.#isConnected) {\n this.#updateReactState();\n }\n })\n .catch(() => {\n // Ignore task errors - we'll continue observing\n });\n }\n }\n\n #updateReactState(): void {\n // Always update to ensure React has the latest state\n this.setCurrentTime({\n ownCurrentTimeMs: this.host.ownCurrentTimeMs,\n durationMs: this.host.durationMs,\n percentComplete: this.host.ownCurrentTimeMs / this.host.durationMs,\n });\n }\n\n // Public method to manually trigger sync (for initialization)\n syncNow(): void {\n this.#updateReactState();\n }\n}\n\nexport const useTimingInfo = (\n timegroupRef: React.RefObject<EFTimegroup> = useRef<EFTimegroup>(null),\n) => {\n const [timeInfo, setTimeInfo] = useState<TimeInfo>({\n ownCurrentTimeMs: 0,\n durationMs: 0,\n percentComplete: 0,\n });\n\n useEffect(() => {\n if (!timegroupRef.current) {\n throw new Error(\"Timegroup ref not set\");\n }\n\n const controller = new CurrentTimeController(\n timegroupRef.current,\n setTimeInfo,\n );\n\n // Trigger initial update if the timegroup is already connected\n if (timegroupRef.current.isConnected) {\n controller.hostConnected();\n // Sync initial state immediately\n controller.syncNow();\n }\n\n // Cleanup function\n return () => {\n controller.hostDisconnected();\n };\n }, [timegroupRef.current]);\n\n return { ...timeInfo, ref: timegroupRef };\n};\n"],"mappings":";;;AAWA,IAAM,wBAAN,MAA0D;CACxD,mBAA4C;CAC5C,eAAe;CAEf,YACE,AAAQA,MAKR,AAAQC,gBACR;EANQ;EAKA;AAER,OAAK,KAAK,cAAc,KAAK;;CAG/B,gBAAsB;AACpB,QAAKC,cAAe;;CAGtB,mBAAyB;AACvB,QAAKA,cAAe;AACpB,QAAKC,kBAAmB;AACxB,OAAK,KAAK,iBAAiB,KAAK;;CAGlC,cAAoB;EAClB,MAAM,qBAAqB,KAAK,KAAK,UAAU;AAG/C,MAAI,uBAAuB,MAAKA,iBAAkB;AAChD,SAAKA,kBAAmB;AAIxB,sBACG,WAAW;AAEV,QAAI,MAAKD,YACP,OAAKE,kBAAmB;KAE1B,CACD,YAAY,GAEX;;;CAIR,oBAA0B;AAExB,OAAK,eAAe;GAClB,kBAAkB,KAAK,KAAK;GAC5B,YAAY,KAAK,KAAK;GACtB,iBAAiB,KAAK,KAAK,mBAAmB,KAAK,KAAK;GACzD,CAAC;;CAIJ,UAAgB;AACd,QAAKA,kBAAmB;;;AAI5B,MAAa,iBACX,eAA6C,OAAoB,KAAK,KACnE;CACH,MAAM,CAAC,UAAU,eAAe,SAAmB;EACjD,kBAAkB;EAClB,YAAY;EACZ,iBAAiB;EAClB,CAAC;AAEF,iBAAgB;AACd,MAAI,CAAC,aAAa,QAChB,OAAM,IAAI,MAAM,wBAAwB;EAG1C,MAAM,aAAa,IAAI,sBACrB,aAAa,SACb,YACD;AAGD,MAAI,aAAa,QAAQ,aAAa;AACpC,cAAW,eAAe;AAE1B,cAAW,SAAS;;AAItB,eAAa;AACX,cAAW,kBAAkB;;IAE9B,CAAC,aAAa,QAAQ,CAAC;AAE1B,QAAO;EAAE,GAAG;EAAU,KAAK;EAAc"}
1
+ {"version":3,"file":"useTimingInfo.js","names":["host: {\n ownCurrentTimeMs: number;\n durationMs: number;\n } & ReactiveControllerHost","setCurrentTime: React.Dispatch<React.SetStateAction<TimeInfo>>","#isConnected","#lastOwnCurrentTimeMs","#lastDurationMs","#updateReactState"],"sources":["../../src/hooks/useTimingInfo.ts"],"sourcesContent":["import type { EFTimegroup } from \"@editframe/elements\";\nimport type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { useEffect, useRef, useState } from \"react\";\n\ninterface TimeInfo {\n ownCurrentTimeMs: number;\n durationMs: number;\n percentComplete: number;\n}\n\nclass CurrentTimeController implements ReactiveController {\n #isConnected = false;\n #lastOwnCurrentTimeMs = Number.NaN;\n #lastDurationMs = Number.NaN;\n\n constructor(\n private host: {\n ownCurrentTimeMs: number;\n durationMs: number;\n } & ReactiveControllerHost,\n private setCurrentTime: React.Dispatch<React.SetStateAction<TimeInfo>>,\n ) {\n this.host.addController(this);\n }\n\n hostConnected(): void {\n this.#isConnected = true;\n }\n\n hostDisconnected(): void {\n this.#isConnected = false;\n this.host.removeController(this);\n }\n\n hostUpdated(): void {\n if (!this.#isConnected) return;\n\n const { ownCurrentTimeMs, durationMs } = this.host;\n\n if (\n ownCurrentTimeMs !== this.#lastOwnCurrentTimeMs ||\n durationMs !== this.#lastDurationMs\n ) {\n this.#lastOwnCurrentTimeMs = ownCurrentTimeMs;\n this.#lastDurationMs = durationMs;\n this.#updateReactState();\n }\n }\n\n #updateReactState(): void {\n const { ownCurrentTimeMs, durationMs } = this.host;\n this.setCurrentTime({\n ownCurrentTimeMs,\n durationMs,\n percentComplete: durationMs > 0 ? ownCurrentTimeMs / durationMs : 0,\n });\n }\n\n syncNow(): void {\n this.#updateReactState();\n }\n}\n\nexport const useTimingInfo = (\n timegroupRef: React.RefObject<EFTimegroup | null> = useRef<EFTimegroup>(null),\n) => {\n const [timeInfo, setTimeInfo] = useState<TimeInfo>({\n ownCurrentTimeMs: 0,\n durationMs: 0,\n percentComplete: 0,\n });\n\n useEffect(() => {\n if (!timegroupRef.current) {\n throw new Error(\"Timegroup ref not set\");\n }\n\n const controller = new CurrentTimeController(\n timegroupRef.current,\n setTimeInfo,\n );\n\n if (timegroupRef.current.isConnected) {\n controller.hostConnected();\n controller.syncNow();\n }\n\n return () => {\n controller.hostDisconnected();\n };\n }, [timegroupRef.current]);\n\n return { ...timeInfo, ref: timegroupRef };\n};\n"],"mappings":";;;AAUA,IAAM,wBAAN,MAA0D;CACxD,eAAe;CACf,wBAAwB;CACxB,kBAAkB;CAElB,YACE,AAAQA,MAIR,AAAQC,gBACR;EALQ;EAIA;AAER,OAAK,KAAK,cAAc,KAAK;;CAG/B,gBAAsB;AACpB,QAAKC,cAAe;;CAGtB,mBAAyB;AACvB,QAAKA,cAAe;AACpB,OAAK,KAAK,iBAAiB,KAAK;;CAGlC,cAAoB;AAClB,MAAI,CAAC,MAAKA,YAAc;EAExB,MAAM,EAAE,kBAAkB,eAAe,KAAK;AAE9C,MACE,qBAAqB,MAAKC,wBAC1B,eAAe,MAAKC,gBACpB;AACA,SAAKD,uBAAwB;AAC7B,SAAKC,iBAAkB;AACvB,SAAKC,kBAAmB;;;CAI5B,oBAA0B;EACxB,MAAM,EAAE,kBAAkB,eAAe,KAAK;AAC9C,OAAK,eAAe;GAClB;GACA;GACA,iBAAiB,aAAa,IAAI,mBAAmB,aAAa;GACnE,CAAC;;CAGJ,UAAgB;AACd,QAAKA,kBAAmB;;;AAI5B,MAAa,iBACX,eAAoD,OAAoB,KAAK,KAC1E;CACH,MAAM,CAAC,UAAU,eAAe,SAAmB;EACjD,kBAAkB;EAClB,YAAY;EACZ,iBAAiB;EAClB,CAAC;AAEF,iBAAgB;AACd,MAAI,CAAC,aAAa,QAChB,OAAM,IAAI,MAAM,wBAAwB;EAG1C,MAAM,aAAa,IAAI,sBACrB,aAAa,SACb,YACD;AAED,MAAI,aAAa,QAAQ,aAAa;AACpC,cAAW,eAAe;AAC1B,cAAW,SAAS;;AAGtB,eAAa;AACX,cAAW,kBAAkB;;IAE9B,CAAC,aAAa,QAAQ,CAAC;AAE1B,QAAO;EAAE,GAAG;EAAU,KAAK;EAAc"}
package/dist/index.d.ts CHANGED
@@ -4,7 +4,6 @@ import { Captions, CaptionsActiveWord, CaptionsAfterActiveWord, CaptionsBeforeAc
4
4
  import { Text, TextSegment } from "./elements/Text.js";
5
5
  import { Image } from "./elements/Image.js";
6
6
  import { Surface } from "./elements/Surface.js";
7
- import { ThumbnailStrip } from "./elements/ThumbnailStrip.js";
8
7
  import { Timegroup } from "./elements/Timegroup.js";
9
8
  import { TimelineRoot } from "./components/TimelineRoot.js";
10
9
  import { Video } from "./elements/Video.js";
@@ -24,11 +23,14 @@ import { Pause } from "./gui/Pause.js";
24
23
  import { Play } from "./gui/Play.js";
25
24
  import { Preview } from "./gui/Preview.js";
26
25
  import { Scrubber, ScrubberProps } from "./gui/Scrubber.js";
26
+ import { ThumbnailStrip } from "./gui/ThumbnailStrip.js";
27
+ import { TrimHandles } from "./gui/TrimHandles.js";
27
28
  import { TimelineRuler } from "./gui/TimelineRuler.js";
28
29
  import { ToggleLoop } from "./gui/ToggleLoop.js";
29
30
  import { TogglePlay } from "./gui/TogglePlay.js";
30
31
  import { Workbench } from "./gui/Workbench.js";
31
32
  import { useTimingInfo } from "./hooks/useTimingInfo.js";
33
+ import { MediaInfo, useMediaInfo } from "./hooks/useMediaInfo.js";
32
34
  import { usePanZoomTransform } from "./hooks/usePanZoomTransform.js";
33
- import { elementNeedsFitScale, needsFitScale } from "@editframe/elements";
34
- export { Audio, Captions, CaptionsActiveWord, CaptionsAfterActiveWord, CaptionsBeforeActiveWord, CaptionsSegment, Configuration, Controls, Dial, Filmstrip, FitScale, FocusOverlay, Image, OverlayItem, type OverlayItemProps, OverlayLayer, PanZoom, Pause, Play, Preview, ResizableBox, Scrubber, type ScrubberProps, Surface, Text, TextSegment, ThumbnailStrip, TimeDisplay, Timegroup, TimelineRoot, TimelineRuler, ToggleLoop, TogglePlay, TransformHandles, Video, Waveform, Workbench, elementNeedsFitScale, needsFitScale, usePanZoomTransform, useTimingInfo };
35
+ import { TrimChangeDetail, TrimValue, elementNeedsFitScale, needsFitScale } from "@editframe/elements";
36
+ export { Audio, Captions, CaptionsActiveWord, CaptionsAfterActiveWord, CaptionsBeforeActiveWord, CaptionsSegment, Configuration, Controls, Dial, Filmstrip, FitScale, FocusOverlay, Image, type MediaInfo, OverlayItem, type OverlayItemProps, OverlayLayer, PanZoom, Pause, Play, Preview, ResizableBox, Scrubber, type ScrubberProps, Surface, Text, TextSegment, ThumbnailStrip, TimeDisplay, Timegroup, TimelineRoot, TimelineRuler, ToggleLoop, TogglePlay, TransformHandles, type TrimChangeDetail, TrimHandles, type TrimValue, Video, Waveform, Workbench, elementNeedsFitScale, needsFitScale, useMediaInfo, usePanZoomTransform, useTimingInfo };