@bycrux/editor 0.6.2 → 0.6.3

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.6.2",
3
+ "version": "0.6.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
package/src/types.ts CHANGED
@@ -491,13 +491,17 @@ export interface VideoEditorProps<P extends Project = Project> {
491
491
  onBackToSetup?: () => void
492
492
  /**
493
493
  * Where the host's `slots.assetsPanel` is placed in the review layout:
494
- * - `'right'` (default) a sidebar column to the right of the preview/timeline.
495
- * The historical Montaj-local layout; preferred when horizontal space is ample.
494
+ * - `'sidebar'` — stacked inside the right-hand version/run-history rail, below
495
+ * it, sharing one column. This is the historical Montaj-local OS layout
496
+ * (versions on top, assets right below) and what the desktop UI uses.
497
+ * - `'right'` — its own dedicated sidebar column to the LEFT of the version
498
+ * rail (two separate columns). Preferred only when horizontal space is ample
499
+ * and the host wants assets visually distinct from versions.
496
500
  * - `'bottom'` — a full-width region stacked below the editor. Used by hosts with
497
501
  * constrained width (e.g. the Hub editor) where vertical stacking reads better.
498
502
  * The host chooses per deployment; the package defaults to `'right'`.
499
503
  */
500
- assetsPlacement?: 'right' | 'bottom'
504
+ assetsPlacement?: 'sidebar' | 'right' | 'bottom'
501
505
 
502
506
  // ── Host-supplied Montaj-specific UI (render-prop seams) ──────────────────
503
507
  // The clip/audio inspector and the subcut-regeneration tool read host-only
@@ -561,17 +561,23 @@ function ReviewSurface<P extends Project>({
561
561
  </div>
562
562
  </div>
563
563
 
564
- {/* Assets — right sidebar column (assetsPlacement: 'right', the default /
565
- Montaj-local layout). The host's panel manages its own scroll. */}
564
+ {/* Assets — dedicated separate column to the LEFT of the version rail
565
+ (assetsPlacement: 'right', two distinct columns). The Montaj-local OS
566
+ layout uses 'sidebar' instead (stacked into the version rail below).
567
+ The host's panel manages its own scroll. */}
566
568
  {assetsPlacement === 'right' && slots?.assetsPanel && (
567
569
  <div className="w-72 shrink-0 border-l border-[var(--editor-border)] bg-[var(--editor-surface)] flex flex-col overflow-hidden">
568
570
  {slots.assetsPanel}
569
571
  </div>
570
572
  )}
571
573
 
572
- {/* Right rail — version history + run history slot */}
573
- {(adapter.listVersionHistory || slots?.runHistory) && (
574
- <div className="w-48 shrink-0 border-l border-[var(--editor-border)] bg-[var(--editor-surface)] flex flex-col overflow-hidden">
574
+ {/* Right rail — version history + run history slot, and (in 'sidebar'
575
+ placement) the assets panel stacked beneath them in the SAME column.
576
+ This is the historical Montaj-local OS layout: versions on top, assets
577
+ right below, one column — not a separate assets column. */}
578
+ {(adapter.listVersionHistory || slots?.runHistory ||
579
+ (assetsPlacement === 'sidebar' && slots?.assetsPanel)) && (
580
+ <div className={`${assetsPlacement === 'sidebar' ? 'w-64' : 'w-48'} shrink-0 border-l border-[var(--editor-border)] bg-[var(--editor-surface)] flex flex-col overflow-hidden`}>
575
581
  {adapter.listVersionHistory && (
576
582
  <VersionPanel versions={versions} restoring={restoring} onRestore={handleRestoreVersion} />
577
583
  )}
@@ -579,6 +585,14 @@ function ReviewSurface<P extends Project>({
579
585
  RunSnapshot / project.history are host-only types — the package never
580
586
  reads them. When absent nothing is rendered. */}
581
587
  {slots?.runHistory}
588
+ {/* Assets stacked below versions/runs (assetsPlacement: 'sidebar'). The
589
+ host's panel manages its own scroll; flex-1 lets it take the
590
+ remaining rail height. A top border separates it from the runs. */}
591
+ {assetsPlacement === 'sidebar' && slots?.assetsPanel && (
592
+ <div className="flex-1 min-h-0 overflow-hidden border-t border-[var(--editor-border)] flex flex-col">
593
+ {slots.assetsPanel}
594
+ </div>
595
+ )}
582
596
  </div>
583
597
  )}
584
598
  </div>
@@ -136,6 +136,8 @@ export default function PreviewPlayer({
136
136
  // and the Web Audio createMediaElementSource graph outputs silence. R2
137
137
  // sends Access-Control-Allow-Origin, so anonymous CORS keeps it audible.
138
138
  crossOrigin="anonymous"
139
+ // Fetch enough to render the seeked poster frame on load (before play).
140
+ preload="auto"
139
141
  onLoadedMetadata={(e) => { const v = e.currentTarget; if (v.videoWidth && v.videoHeight) setVideoDims({ w: v.videoWidth, h: v.videoHeight }) }}
140
142
  onTimeUpdate={() => { if (activeSlotRef.current === 0) handleTimeUpdate() }}
141
143
  onEnded={() => { if (activeSlotRef.current === 0) handleEnded() }}
@@ -150,6 +152,7 @@ export default function PreviewPlayer({
150
152
  // See slot 0: anonymous CORS so R2 cross-origin clips aren't tainted
151
153
  // (which would mute the Web Audio graph).
152
154
  crossOrigin="anonymous"
155
+ preload="auto"
153
156
  onLoadedMetadata={(e) => { const v = e.currentTarget; if (v.videoWidth && v.videoHeight) setVideoDims({ w: v.videoWidth, h: v.videoHeight }) }}
154
157
  onTimeUpdate={() => { if (activeSlotRef.current === 1) handleTimeUpdate() }}
155
158
  onEnded={() => { if (activeSlotRef.current === 1) handleEnded() }}
@@ -196,6 +196,12 @@ export function useVideoPlayback(
196
196
  * deferred play() is not autoplay-blocked.
197
197
  */
198
198
  function playFromGesture(video: HTMLVideoElement) {
199
+ // Wire this slot to Web Audio on the first play (not at load) so the paused
200
+ // poster frame can render. This also creates the shared AudioContext inside
201
+ // the gesture, so the resume() below is gesture-credited.
202
+ ensureVideoGain(activeSlotRef.current)
203
+ const cur = clips[activeIdxRef.current]
204
+ if (cur) applyClipVolume(cur)
199
205
  const w = window as Window & MontajWindow
200
206
  const ctx = w.__montajSharedCtx
201
207
  if (ctx && ctx.state === 'suspended') {
@@ -229,11 +235,21 @@ export function useVideoPlayback(
229
235
  return v.__montajGain ?? null
230
236
  }
231
237
 
232
- // Set video clip volume via GainNode (supports amplification > 1.0).
233
- // Muted clips get gain 0; unmuted clips get the clip's volume value.
238
+ // Existing gain for a slot WITHOUT wiring it. Wiring (createMediaElementSource)
239
+ // is deferred to the first play gesture so the paused poster frame can render —
240
+ // a <video> wired to a suspended AudioContext produces no frames at all.
241
+ function getVideoGain(slot: 0 | 1): GainNode | null {
242
+ const v = (slot === 0 ? video0Ref.current : video1Ref.current) as MontajVideoElement | null
243
+ return (v && v.__montajGain) ?? null
244
+ }
245
+
246
+ // Set video clip volume via GainNode (supports amplification > 1.0). Muted clips
247
+ // get gain 0; unmuted clips get the clip's volume value. No-op until the slot is
248
+ // wired (first play) — there's no audio to control on a paused poster, and
249
+ // wiring here would gate the poster frame on the suspended context.
234
250
  function applyClipVolume(clip: { muted?: boolean; volume?: number }) {
235
251
  const slot = activeSlotRef.current
236
- const gain = ensureVideoGain(slot)
252
+ const gain = getVideoGain(slot)
237
253
  if (gain) gain.gain.value = clip.muted ? 0 : (clip.volume ?? 1)
238
254
  }
239
255