@bycrux/editor 0.5.0 → 0.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bycrux/editor",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -388,7 +388,9 @@ function ReviewSurface<P extends Project>({
388
388
  }
389
389
 
390
390
  return (
391
- <div className="flex flex-1 overflow-hidden">
391
+ <div className="flex flex-col flex-1 overflow-hidden">
392
+ {/* Work area — editor body + version rail, side by side */}
393
+ <div className="flex flex-1 overflow-hidden min-h-0">
392
394
  {/* Main: preview + timeline */}
393
395
  <div className="flex flex-col flex-1 overflow-hidden">
394
396
  <div className="flex-1 flex items-center justify-center bg-black overflow-hidden p-2">
@@ -473,8 +475,8 @@ function ReviewSurface<P extends Project>({
473
475
  </div>
474
476
  </div>
475
477
 
476
- {/* Right sidebar — version history + run history slot + host-supplied assets panel */}
477
- {(adapter.listVersionHistory || slots?.assetsPanel || slots?.runHistory) && (
478
+ {/* Right rail — version history + run history slot */}
479
+ {(adapter.listVersionHistory || slots?.runHistory) && (
478
480
  <div className="w-48 shrink-0 border-l border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-950 flex flex-col overflow-hidden">
479
481
  {adapter.listVersionHistory && (
480
482
  <VersionPanel versions={versions} restoring={restoring} onRestore={handleRestoreVersion} />
@@ -483,7 +485,16 @@ function ReviewSurface<P extends Project>({
483
485
  RunSnapshot / project.history are host-only types — the package never
484
486
  reads them. When absent nothing is rendered. */}
485
487
  {slots?.runHistory}
486
- {slots?.assetsPanel}
488
+ </div>
489
+ )}
490
+ </div>
491
+
492
+ {/* Project media / assets — full-width region stacked BELOW the editor,
493
+ mirroring CarouselEditor's layout (was previously crammed into the
494
+ narrow right rail). The host's panel manages its own scroll. */}
495
+ {slots?.assetsPanel && (
496
+ <div className="shrink-0 border-t border-gray-200 dark:border-gray-800 w-full flex flex-col max-h-[45%] overflow-hidden">
497
+ {slots.assetsPanel}
487
498
  </div>
488
499
  )}
489
500
 
@@ -50,6 +50,10 @@ export function useVideoPlayback(
50
50
  const loopOffsetRef = useRef(0)
51
51
  const rafRef = useRef<number | null>(null)
52
52
  const rafLastMs = useRef<number | null>(null)
53
+ // rAF clock for VIDEO projects — drives clip-boundary detection at ~60Hz
54
+ // instead of the <video> element's coarse `timeupdate` event (~4Hz). See the
55
+ // effect below for why.
56
+ const videoRafRef = useRef<number | null>(null)
53
57
  const audioRefsMap = useRef<Map<string, HTMLAudioElement>>(new Map())
54
58
  const audioSrcMap = useRef<Map<string, string>>(new Map())
55
59
  // Web Audio API: GainNode per audio track allows volume > 1.0 (amplification).
@@ -693,6 +697,39 @@ export function useVideoPlayback(
693
697
  handleTimeUpdate()
694
698
  }, [handleTimeUpdate])
695
699
 
700
+ // ── Video boundary clock ─────────────────────────────────────────────────
701
+ // Drive clip-boundary detection from requestAnimationFrame (~60Hz) rather
702
+ // than relying on the <video> element's `timeupdate` event, which only fires
703
+ // ~every 250ms. Under timeupdate-gating the active clip plays up to a full
704
+ // ~250ms PAST its outPoint before the swap fires; on a silence-trimmed
705
+ // single-source timeline that overshoot is trimmed-out footage playing past
706
+ // the cut — the "underlying video keeps playing in the background" bug.
707
+ // Polling currentTime every frame tightens the boundary to ~16ms.
708
+ // handleTimeUpdate is idempotent (preload + swap are guarded), so the
709
+ // timeupdate event firing in addition to this is harmless. Canvas projects
710
+ // advance time via their own rAF above and are excluded here.
711
+ useEffect(() => {
712
+ if (isCanvasProject || !isPlaying) {
713
+ if (videoRafRef.current !== null) {
714
+ cancelAnimationFrame(videoRafRef.current)
715
+ videoRafRef.current = null
716
+ }
717
+ return
718
+ }
719
+ function pump() {
720
+ // The gap clock owns time during gaps; handleTimeUpdate no-ops then.
721
+ if (!inGapRef.current) handleTimeUpdate()
722
+ videoRafRef.current = requestAnimationFrame(pump)
723
+ }
724
+ videoRafRef.current = requestAnimationFrame(pump)
725
+ return () => {
726
+ if (videoRafRef.current !== null) {
727
+ cancelAnimationFrame(videoRafRef.current)
728
+ videoRafRef.current = null
729
+ }
730
+ }
731
+ }, [isPlaying, isCanvasProject, handleTimeUpdate])
732
+
696
733
  function togglePlay() {
697
734
  // GESTURE-ANCHORED: same rationale as the keydown handler — resume the
698
735
  // AudioContext synchronously inside this user-gesture call so a wired