ai-design-system 0.1.105 → 0.1.106

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.
@@ -94,6 +94,26 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
94
94
  return 0;
95
95
  });
96
96
 
97
+ // Dual-video buffer for gapless playback between scenes:
98
+ // One slot actively plays the current scene, while the other slot preloads the next scene in the background.
99
+ const [activeSlot, setActiveSlot] = React.useState<0 | 1>(0);
100
+ const [slot0Index, setSlot0Index] = React.useState<number>(() => {
101
+ if (initialSceneIndex >= 0 && initialSceneIndex < scenes.length) {
102
+ return initialSceneIndex;
103
+ }
104
+ return 0;
105
+ });
106
+ const [slot1Index, setSlot1Index] = React.useState<number>(() => {
107
+ if (scenes.length > 1) {
108
+ const init = initialSceneIndex >= 0 && initialSceneIndex < scenes.length ? initialSceneIndex : 0;
109
+ return (init + 1) % scenes.length;
110
+ }
111
+ return 0;
112
+ });
113
+
114
+ const videoRef0 = React.useRef<HTMLVideoElement | null>(null);
115
+ const videoRef1 = React.useRef<HTMLVideoElement | null>(null);
116
+
97
117
  const [isPlayingState, setIsPlayingState] = React.useState<boolean>(isPlayingProp ?? true);
98
118
  const isPlaying = isPlayingProp !== undefined ? isPlayingProp : isPlayingState;
99
119
 
@@ -111,10 +131,46 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
111
131
  }, [setIsPlaying]);
112
132
 
113
133
  const [isMuted, setIsMuted] = React.useState<boolean>(initialMuted);
114
- const videoRef = React.useRef<HTMLVideoElement | null>(null);
115
134
 
116
135
  const activeScene = scenes[currentSceneIndex] || scenes[0];
117
136
 
137
+ const goToScene = React.useCallback(
138
+ (targetIdx: number) => {
139
+ if (!scenes.length) return;
140
+ const clampedIdx = (targetIdx + scenes.length) % scenes.length;
141
+ onSceneChange?.(clampedIdx, scenes[clampedIdx]);
142
+ setCurrentSceneIndex(clampedIdx);
143
+
144
+ if (scenes.length <= 1) {
145
+ setSlot0Index(clampedIdx);
146
+ const v0 = videoRef0.current;
147
+ if (v0) {
148
+ v0.currentTime = 0;
149
+ if (isPlaying) v0.play().catch(() => {});
150
+ }
151
+ return;
152
+ }
153
+
154
+ const nextActiveSlot: 0 | 1 = activeSlot === 0 ? 1 : 0;
155
+ const targetVideo = nextActiveSlot === 0 ? videoRef0.current : videoRef1.current;
156
+ if (targetVideo) {
157
+ targetVideo.currentTime = 0;
158
+ }
159
+ const sceneAfterTarget = (clampedIdx + 1) % scenes.length;
160
+
161
+ if (nextActiveSlot === 0) {
162
+ setSlot0Index(clampedIdx);
163
+ setSlot1Index(sceneAfterTarget);
164
+ } else {
165
+ setSlot1Index(clampedIdx);
166
+ setSlot0Index(sceneAfterTarget);
167
+ }
168
+
169
+ setActiveSlot(nextActiveSlot);
170
+ },
171
+ [scenes, activeSlot, isPlaying, onSceneChange]
172
+ );
173
+
118
174
  const handlePromptSubmit = React.useCallback(
119
175
  (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => {
120
176
  if (onSubmit) {
@@ -130,30 +186,20 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
130
186
 
131
187
  const handlePrev = React.useCallback(() => {
132
188
  if (!scenes.length) return;
133
- setCurrentSceneIndex((prev) => {
134
- const nextIdx = prev > 0 ? prev - 1 : scenes.length - 1;
135
- onSceneChange?.(nextIdx, scenes[nextIdx]);
136
- return nextIdx;
137
- });
138
- }, [scenes, onSceneChange]);
189
+ const prevIdx = currentSceneIndex > 0 ? currentSceneIndex - 1 : scenes.length - 1;
190
+ goToScene(prevIdx);
191
+ }, [scenes.length, currentSceneIndex, goToScene]);
139
192
 
140
193
  const handleNext = React.useCallback(() => {
141
194
  if (!scenes.length) return;
142
- setCurrentSceneIndex((prev) => {
143
- const nextIdx = prev < scenes.length - 1 ? prev + 1 : 0;
144
- onSceneChange?.(nextIdx, scenes[nextIdx]);
145
- return nextIdx;
146
- });
147
- }, [scenes, onSceneChange]);
195
+ const nextIdx = currentSceneIndex < scenes.length - 1 ? currentSceneIndex + 1 : 0;
196
+ goToScene(nextIdx);
197
+ }, [scenes.length, currentSceneIndex, goToScene]);
148
198
 
149
199
  const handleSceneEnded = React.useCallback(() => {
150
200
  if (!scenes.length || !isPlaying) return;
151
- setCurrentSceneIndex((prev) => {
152
- const nextIdx = prev < scenes.length - 1 ? prev + 1 : 0;
153
- onSceneChange?.(nextIdx, scenes[nextIdx]);
154
- return nextIdx;
155
- });
156
- }, [scenes, onSceneChange, isPlaying]);
201
+ handleNext();
202
+ }, [scenes.length, isPlaying, handleNext]);
157
203
 
158
204
  const handleDownload = React.useCallback(async () => {
159
205
  if (onDownload) {
@@ -187,28 +233,48 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
187
233
  }
188
234
  }, [onDownload, downloadUrl, videoUrl, activeScene?.scene_clip_url]);
189
235
 
190
- // Playback & mute synchronization
236
+ // Playback & mute synchronization across dual slots
191
237
  React.useEffect(() => {
192
- const video = videoRef.current;
193
- if (!video) return;
238
+ const activeVideo = activeSlot === 0 ? videoRef0.current : videoRef1.current;
239
+ const idleVideo = activeSlot === 0 ? videoRef1.current : videoRef0.current;
194
240
 
195
- video.muted = isMuted;
241
+ if (idleVideo) {
242
+ idleVideo.pause();
243
+ idleVideo.muted = true;
244
+ }
245
+
246
+ if (!activeVideo) return;
247
+
248
+ activeVideo.muted = isMuted;
196
249
 
197
250
  if (isPlaying) {
198
- const playPromise = video.play();
251
+ if (activeVideo.ended) {
252
+ activeVideo.currentTime = 0;
253
+ }
254
+ const playPromise = activeVideo.play();
199
255
  if (playPromise !== undefined) {
200
256
  playPromise.catch(() => {
201
257
  if (!isMuted) {
202
- video.muted = true;
258
+ activeVideo.muted = true;
203
259
  setIsMuted(true);
204
- video.play().catch(() => {});
260
+ activeVideo.play().catch(() => {});
205
261
  }
206
262
  });
207
263
  }
208
264
  } else {
209
- video.pause();
265
+ activeVideo.pause();
210
266
  }
211
- }, [activeScene?.scene_clip_url, isMuted, isPlaying]);
267
+ }, [activeSlot, isPlaying, isMuted, slot0Index, slot1Index]);
268
+
269
+ React.useEffect(() => {
270
+ if (!scenes || scenes.length === 0) return;
271
+ if (currentSceneIndex >= scenes.length) {
272
+ setCurrentSceneIndex(0);
273
+ setSlot0Index(0);
274
+ setSlot1Index(scenes.length > 1 ? 1 : 0);
275
+ setActiveSlot(0);
276
+ }
277
+ }, [scenes, currentSceneIndex]);
212
278
 
213
279
  // Keyboard navigation & playback controls
214
280
  React.useEffect(() => {
@@ -367,20 +433,40 @@ export const ScenePlayer = React.forwardRef<HTMLDivElement, ScenePlayerProps>(
367
433
 
368
434
  {/* Main Scene Video Player Stage */}
369
435
  <div className="flex-1 relative flex items-center justify-center bg-black overflow-hidden min-h-0">
436
+ {/* Slot 0 Video */}
370
437
  <video
371
- ref={videoRef}
372
- key={activeScene?.scene_clip_url}
373
- src={activeScene?.scene_clip_url}
374
- autoPlay
438
+ ref={videoRef0}
439
+ src={scenes[slot0Index]?.scene_clip_url}
440
+ preload="auto"
375
441
  playsInline
376
- loop={false}
377
- muted={isMuted}
378
- onEnded={handleSceneEnded}
379
- className="w-full h-full object-contain bg-black"
442
+ muted={activeSlot === 0 ? isMuted : true}
443
+ onEnded={activeSlot === 0 ? handleSceneEnded : undefined}
444
+ data-testid="scene-video-0"
445
+ className={cn(
446
+ "w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-150",
447
+ activeSlot === 0 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
448
+ )}
380
449
  />
381
450
 
451
+ {/* Slot 1 Video (preloads next scene in background for gapless playback) */}
452
+ {scenes.length > 1 && (
453
+ <video
454
+ ref={videoRef1}
455
+ src={scenes[slot1Index]?.scene_clip_url}
456
+ preload="auto"
457
+ playsInline
458
+ muted={activeSlot === 1 ? isMuted : true}
459
+ onEnded={activeSlot === 1 ? handleSceneEnded : undefined}
460
+ data-testid="scene-video-1"
461
+ className={cn(
462
+ "w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-150",
463
+ activeSlot === 1 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
464
+ )}
465
+ />
466
+ )}
467
+
382
468
  {/* Invisible Interactive Tap Zones for Stepping Scenes */}
383
- <div className="absolute inset-x-0 top-[60px] bottom-[70px] flex z-10">
469
+ <div className="absolute inset-x-0 top-[60px] bottom-[70px] flex z-20">
384
470
  <Button
385
471
  variant="ghost"
386
472
  data-testid="btn-prev-scene"
package/dist/index.cjs CHANGED
@@ -14692,7 +14692,7 @@ var ScenePlayer = React3__namespace.forwardRef(
14692
14692
  "onSceneChange",
14693
14693
  "className"
14694
14694
  ]);
14695
- var _a2;
14695
+ var _a2, _b2, _c;
14696
14696
  const handleBack = onBack || onClose;
14697
14697
  const [currentSceneIndex, setCurrentSceneIndex] = React3__namespace.useState(() => {
14698
14698
  if (initialSceneIndex >= 0 && initialSceneIndex < scenes.length) {
@@ -14700,6 +14700,22 @@ var ScenePlayer = React3__namespace.forwardRef(
14700
14700
  }
14701
14701
  return 0;
14702
14702
  });
14703
+ const [activeSlot, setActiveSlot] = React3__namespace.useState(0);
14704
+ const [slot0Index, setSlot0Index] = React3__namespace.useState(() => {
14705
+ if (initialSceneIndex >= 0 && initialSceneIndex < scenes.length) {
14706
+ return initialSceneIndex;
14707
+ }
14708
+ return 0;
14709
+ });
14710
+ const [slot1Index, setSlot1Index] = React3__namespace.useState(() => {
14711
+ if (scenes.length > 1) {
14712
+ const init = initialSceneIndex >= 0 && initialSceneIndex < scenes.length ? initialSceneIndex : 0;
14713
+ return (init + 1) % scenes.length;
14714
+ }
14715
+ return 0;
14716
+ });
14717
+ const videoRef0 = React3__namespace.useRef(null);
14718
+ const videoRef1 = React3__namespace.useRef(null);
14703
14719
  const [isPlayingState, setIsPlayingState] = React3__namespace.useState(isPlayingProp != null ? isPlayingProp : true);
14704
14720
  const isPlaying = isPlayingProp !== void 0 ? isPlayingProp : isPlayingState;
14705
14721
  const setIsPlaying = React3__namespace.useCallback(
@@ -14714,8 +14730,40 @@ var ScenePlayer = React3__namespace.forwardRef(
14714
14730
  setIsPlaying((prev) => !prev);
14715
14731
  }, [setIsPlaying]);
14716
14732
  const [isMuted, setIsMuted] = React3__namespace.useState(initialMuted);
14717
- const videoRef = React3__namespace.useRef(null);
14718
14733
  const activeScene = scenes[currentSceneIndex] || scenes[0];
14734
+ const goToScene = React3__namespace.useCallback(
14735
+ (targetIdx) => {
14736
+ if (!scenes.length) return;
14737
+ const clampedIdx = (targetIdx + scenes.length) % scenes.length;
14738
+ onSceneChange == null ? void 0 : onSceneChange(clampedIdx, scenes[clampedIdx]);
14739
+ setCurrentSceneIndex(clampedIdx);
14740
+ if (scenes.length <= 1) {
14741
+ setSlot0Index(clampedIdx);
14742
+ const v0 = videoRef0.current;
14743
+ if (v0) {
14744
+ v0.currentTime = 0;
14745
+ if (isPlaying) v0.play().catch(() => {
14746
+ });
14747
+ }
14748
+ return;
14749
+ }
14750
+ const nextActiveSlot = activeSlot === 0 ? 1 : 0;
14751
+ const targetVideo = nextActiveSlot === 0 ? videoRef0.current : videoRef1.current;
14752
+ if (targetVideo) {
14753
+ targetVideo.currentTime = 0;
14754
+ }
14755
+ const sceneAfterTarget = (clampedIdx + 1) % scenes.length;
14756
+ if (nextActiveSlot === 0) {
14757
+ setSlot0Index(clampedIdx);
14758
+ setSlot1Index(sceneAfterTarget);
14759
+ } else {
14760
+ setSlot1Index(clampedIdx);
14761
+ setSlot0Index(sceneAfterTarget);
14762
+ }
14763
+ setActiveSlot(nextActiveSlot);
14764
+ },
14765
+ [scenes, activeSlot, isPlaying, onSceneChange]
14766
+ );
14719
14767
  const handlePromptSubmit = React3__namespace.useCallback(
14720
14768
  (message, event) => {
14721
14769
  if (onSubmit) {
@@ -14730,30 +14778,20 @@ var ScenePlayer = React3__namespace.forwardRef(
14730
14778
  );
14731
14779
  const handlePrev = React3__namespace.useCallback(() => {
14732
14780
  if (!scenes.length) return;
14733
- setCurrentSceneIndex((prev) => {
14734
- const nextIdx = prev > 0 ? prev - 1 : scenes.length - 1;
14735
- onSceneChange == null ? void 0 : onSceneChange(nextIdx, scenes[nextIdx]);
14736
- return nextIdx;
14737
- });
14738
- }, [scenes, onSceneChange]);
14781
+ const prevIdx = currentSceneIndex > 0 ? currentSceneIndex - 1 : scenes.length - 1;
14782
+ goToScene(prevIdx);
14783
+ }, [scenes.length, currentSceneIndex, goToScene]);
14739
14784
  const handleNext = React3__namespace.useCallback(() => {
14740
14785
  if (!scenes.length) return;
14741
- setCurrentSceneIndex((prev) => {
14742
- const nextIdx = prev < scenes.length - 1 ? prev + 1 : 0;
14743
- onSceneChange == null ? void 0 : onSceneChange(nextIdx, scenes[nextIdx]);
14744
- return nextIdx;
14745
- });
14746
- }, [scenes, onSceneChange]);
14786
+ const nextIdx = currentSceneIndex < scenes.length - 1 ? currentSceneIndex + 1 : 0;
14787
+ goToScene(nextIdx);
14788
+ }, [scenes.length, currentSceneIndex, goToScene]);
14747
14789
  const handleSceneEnded = React3__namespace.useCallback(() => {
14748
14790
  if (!scenes.length || !isPlaying) return;
14749
- setCurrentSceneIndex((prev) => {
14750
- const nextIdx = prev < scenes.length - 1 ? prev + 1 : 0;
14751
- onSceneChange == null ? void 0 : onSceneChange(nextIdx, scenes[nextIdx]);
14752
- return nextIdx;
14753
- });
14754
- }, [scenes, onSceneChange, isPlaying]);
14791
+ handleNext();
14792
+ }, [scenes.length, isPlaying, handleNext]);
14755
14793
  const handleDownload = React3__namespace.useCallback(async () => {
14756
- var _a3, _b2;
14794
+ var _a3, _b3;
14757
14795
  if (onDownload) {
14758
14796
  onDownload();
14759
14797
  return;
@@ -14775,7 +14813,7 @@ var ScenePlayer = React3__namespace.forwardRef(
14775
14813
  } catch (e) {
14776
14814
  const link = document.createElement("a");
14777
14815
  link.href = targetUrl;
14778
- link.download = ((_b2 = targetUrl.split("/").pop()) == null ? void 0 : _b2.split("?")[0]) || "video.mp4";
14816
+ link.download = ((_b3 = targetUrl.split("/").pop()) == null ? void 0 : _b3.split("?")[0]) || "video.mp4";
14779
14817
  link.target = "_blank";
14780
14818
  link.rel = "noopener noreferrer";
14781
14819
  document.body.appendChild(link);
@@ -14784,25 +14822,42 @@ var ScenePlayer = React3__namespace.forwardRef(
14784
14822
  }
14785
14823
  }, [onDownload, downloadUrl, videoUrl, activeScene == null ? void 0 : activeScene.scene_clip_url]);
14786
14824
  React3__namespace.useEffect(() => {
14787
- const video = videoRef.current;
14788
- if (!video) return;
14789
- video.muted = isMuted;
14825
+ const activeVideo = activeSlot === 0 ? videoRef0.current : videoRef1.current;
14826
+ const idleVideo = activeSlot === 0 ? videoRef1.current : videoRef0.current;
14827
+ if (idleVideo) {
14828
+ idleVideo.pause();
14829
+ idleVideo.muted = true;
14830
+ }
14831
+ if (!activeVideo) return;
14832
+ activeVideo.muted = isMuted;
14790
14833
  if (isPlaying) {
14791
- const playPromise = video.play();
14834
+ if (activeVideo.ended) {
14835
+ activeVideo.currentTime = 0;
14836
+ }
14837
+ const playPromise = activeVideo.play();
14792
14838
  if (playPromise !== void 0) {
14793
14839
  playPromise.catch(() => {
14794
14840
  if (!isMuted) {
14795
- video.muted = true;
14841
+ activeVideo.muted = true;
14796
14842
  setIsMuted(true);
14797
- video.play().catch(() => {
14843
+ activeVideo.play().catch(() => {
14798
14844
  });
14799
14845
  }
14800
14846
  });
14801
14847
  }
14802
14848
  } else {
14803
- video.pause();
14849
+ activeVideo.pause();
14804
14850
  }
14805
- }, [activeScene == null ? void 0 : activeScene.scene_clip_url, isMuted, isPlaying]);
14851
+ }, [activeSlot, isPlaying, isMuted, slot0Index, slot1Index]);
14852
+ React3__namespace.useEffect(() => {
14853
+ if (!scenes || scenes.length === 0) return;
14854
+ if (currentSceneIndex >= scenes.length) {
14855
+ setCurrentSceneIndex(0);
14856
+ setSlot0Index(0);
14857
+ setSlot1Index(scenes.length > 1 ? 1 : 0);
14858
+ setActiveSlot(0);
14859
+ }
14860
+ }, [scenes, currentSceneIndex]);
14806
14861
  React3__namespace.useEffect(() => {
14807
14862
  const handleKeyDown = (e) => {
14808
14863
  const target = e.target;
@@ -14951,18 +15006,36 @@ var ScenePlayer = React3__namespace.forwardRef(
14951
15006
  /* @__PURE__ */ jsxRuntime.jsx(
14952
15007
  "video",
14953
15008
  {
14954
- ref: videoRef,
14955
- src: activeScene == null ? void 0 : activeScene.scene_clip_url,
14956
- autoPlay: true,
15009
+ ref: videoRef0,
15010
+ src: (_b2 = scenes[slot0Index]) == null ? void 0 : _b2.scene_clip_url,
15011
+ preload: "auto",
14957
15012
  playsInline: true,
14958
- loop: false,
14959
- muted: isMuted,
14960
- onEnded: handleSceneEnded,
14961
- className: "w-full h-full object-contain bg-black"
14962
- },
14963
- activeScene == null ? void 0 : activeScene.scene_clip_url
15013
+ muted: activeSlot === 0 ? isMuted : true,
15014
+ onEnded: activeSlot === 0 ? handleSceneEnded : void 0,
15015
+ "data-testid": "scene-video-0",
15016
+ className: cn(
15017
+ "w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-150",
15018
+ activeSlot === 0 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
15019
+ )
15020
+ }
15021
+ ),
15022
+ scenes.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
15023
+ "video",
15024
+ {
15025
+ ref: videoRef1,
15026
+ src: (_c = scenes[slot1Index]) == null ? void 0 : _c.scene_clip_url,
15027
+ preload: "auto",
15028
+ playsInline: true,
15029
+ muted: activeSlot === 1 ? isMuted : true,
15030
+ onEnded: activeSlot === 1 ? handleSceneEnded : void 0,
15031
+ "data-testid": "scene-video-1",
15032
+ className: cn(
15033
+ "w-full h-full object-contain bg-black absolute inset-0 transition-opacity duration-150",
15034
+ activeSlot === 1 ? "opacity-100 z-10" : "opacity-0 pointer-events-none z-0"
15035
+ )
15036
+ }
14964
15037
  ),
14965
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-x-0 top-[60px] bottom-[70px] flex z-10", children: [
15038
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-x-0 top-[60px] bottom-[70px] flex z-20", children: [
14966
15039
  /* @__PURE__ */ jsxRuntime.jsx(
14967
15040
  Button2,
14968
15041
  {