@twick/studio 0.15.18 → 0.15.20

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/dist/index.mjs CHANGED
@@ -2,9 +2,9 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
5
- import { forwardRef, createElement, useState, useEffect, useRef, createContext, useContext, useCallback, useMemo } from "react";
6
- import { useTimelineContext, TrackElement, Track, ImageElement, AudioElement, VideoElement, TextElement, RectElement, CircleElement, CAPTION_STYLE, CaptionElement, ElementTextEffect, ElementAnimation, PLAYER_STATE } from "@twick/timeline";
7
- import { AudioElement as AudioElement2, CAPTION_COLOR, CAPTION_FONT, CAPTION_STYLE as CAPTION_STYLE2, CAPTION_STYLE_OPTIONS, CaptionElement as CaptionElement2, CircleElement as CircleElement2, ElementAdder, ElementAnimation as ElementAnimation2, ElementCloner, ElementDeserializer, ElementFrameEffect, ElementRemover, ElementSerializer, ElementSplitter, ElementTextEffect as ElementTextEffect2, ElementUpdater, ElementValidator, INITIAL_TIMELINE_DATA, IconElement, ImageElement as ImageElement2, PROCESS_STATE, RectElement as RectElement2, TIMELINE_ACTION, TIMELINE_ELEMENT_TYPE, TextElement as TextElement2, TimelineEditor, TimelineProvider, Track as Track2, TrackElement as TrackElement2, VideoElement as VideoElement2, WORDS_PER_PHRASE, generateShortUuid, getCurrentElements, getTotalDuration, isElementId, isTrackId, useTimelineContext as useTimelineContext2 } from "@twick/timeline";
5
+ import { forwardRef, createElement, useState, useEffect, useRef, useCallback, createContext, useContext, useMemo } from "react";
6
+ import { useTimelineContext, TrackElement, Track, ImageElement, AudioElement, VideoElement, TextElement, RectElement, CircleElement, CAPTION_STYLE, CaptionElement, ElementTextEffect, ElementAnimation, CAPTION_STYLE_OPTIONS, PLAYER_STATE } from "@twick/timeline";
7
+ import { AudioElement as AudioElement2, CAPTION_COLOR, CAPTION_FONT, CAPTION_STYLE as CAPTION_STYLE2, CAPTION_STYLE_OPTIONS as CAPTION_STYLE_OPTIONS2, CaptionElement as CaptionElement2, CircleElement as CircleElement2, ElementAdder, ElementAnimation as ElementAnimation2, ElementCloner, ElementDeserializer, ElementFrameEffect, ElementRemover, ElementSerializer, ElementSplitter, ElementTextEffect as ElementTextEffect2, ElementUpdater, ElementValidator, INITIAL_TIMELINE_DATA, IconElement, ImageElement as ImageElement2, PROCESS_STATE, RectElement as RectElement2, TIMELINE_ACTION, TIMELINE_ELEMENT_TYPE, TextElement as TextElement2, TimelineEditor, TimelineProvider, Track as Track2, TrackElement as TrackElement2, VideoElement as VideoElement2, WORDS_PER_PHRASE, generateShortUuid, getCurrentElements, getTotalDuration, isElementId, isTrackId, useTimelineContext as useTimelineContext2 } from "@twick/timeline";
8
8
  import VideoEditor, { useEditorManager, BrowserMediaManager, TIMELINE_DROP_MEDIA_TYPE, AVAILABLE_TEXT_FONTS, TEXT_EFFECTS, ANIMATIONS } from "@twick/video-editor";
9
9
  import { ANIMATIONS as ANIMATIONS2, BaseMediaManager, BrowserMediaManager as BrowserMediaManager2, PlayerControls, TEXT_EFFECTS as TEXT_EFFECTS2, TimelineManager, default as default2, animationGifs, getAnimationGif, setElementColors, useEditorManager as useEditorManager2, usePlayerControl, useTimelineControl } from "@twick/video-editor";
10
10
  import { useLivePlayerContext } from "@twick/live-player";
@@ -561,7 +561,8 @@ const toolCategories = [
561
561
  { id: "text", name: "Text", icon: "Type", description: "Add text elements" },
562
562
  { id: "circle", name: "Circle", icon: "Circle", description: "Add a circle element" },
563
563
  { id: "rect", name: "Rect", icon: "Rect", description: "Add a rectangle element" },
564
- { id: "caption", name: "Caption", icon: "MessageSquare", description: "Manage captions" }
564
+ { id: "caption", name: "Caption", icon: "MessageSquare", description: "Manage captions" },
565
+ { id: "generate-media", name: "Generate", icon: "Wand2", description: "Generate image or video with AI" }
565
566
  ];
566
567
  const getIcon = (iconName) => {
567
568
  switch (iconName) {
@@ -585,6 +586,8 @@ const getIcon = (iconName) => {
585
586
  return Square;
586
587
  case "MessageSquare":
587
588
  return MessageSquare;
589
+ case "Wand2":
590
+ return WandSparkles;
588
591
  default:
589
592
  return Plus;
590
593
  }
@@ -752,6 +755,182 @@ const useStudioManager = () => {
752
755
  updateElement
753
756
  };
754
757
  };
758
+ const putFileWithProgress = (uploadUrl, file, onProgress) => {
759
+ return new Promise((resolve, reject) => {
760
+ const xhr = new XMLHttpRequest();
761
+ xhr.upload.addEventListener("progress", (e) => {
762
+ if (e.lengthComputable) {
763
+ onProgress(e.loaded / e.total * 100);
764
+ }
765
+ });
766
+ xhr.addEventListener("load", () => {
767
+ if (xhr.status >= 200 && xhr.status < 300) {
768
+ onProgress(100);
769
+ resolve();
770
+ } else {
771
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
772
+ }
773
+ });
774
+ xhr.addEventListener("error", () => reject(new Error("Upload failed")));
775
+ xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
776
+ xhr.open("PUT", uploadUrl);
777
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
778
+ xhr.send(file);
779
+ });
780
+ };
781
+ const useCloudMediaUpload = (config) => {
782
+ const { uploadApiUrl, provider } = config;
783
+ const [isUploading, setIsUploading] = useState(false);
784
+ const [progress, setProgress] = useState(0);
785
+ const [error, setError] = useState(null);
786
+ const resetError = useCallback(() => {
787
+ setError(null);
788
+ }, []);
789
+ const uploadFile = useCallback(
790
+ async (file) => {
791
+ setIsUploading(true);
792
+ setProgress(0);
793
+ setError(null);
794
+ try {
795
+ if (provider === "s3") {
796
+ const presignRes = await fetch(uploadApiUrl, {
797
+ method: "POST",
798
+ headers: { "Content-Type": "application/json" },
799
+ body: JSON.stringify({
800
+ filename: file.name,
801
+ contentType: file.type || "application/octet-stream"
802
+ })
803
+ });
804
+ if (!presignRes.ok) {
805
+ const errBody = await presignRes.json().catch(() => ({}));
806
+ throw new Error(
807
+ errBody.error ?? `Failed to get upload URL: ${presignRes.statusText}`
808
+ );
809
+ }
810
+ const presignData = await presignRes.json();
811
+ const uploadUrl = presignData.uploadUrl;
812
+ await putFileWithProgress(uploadUrl, file, setProgress);
813
+ const publicUrl = uploadUrl.split("?")[0];
814
+ return { url: publicUrl };
815
+ }
816
+ if (provider === "gcs") {
817
+ setProgress(10);
818
+ const formData = new FormData();
819
+ formData.append("file", file);
820
+ const uploadRes = await fetch(uploadApiUrl, {
821
+ method: "POST",
822
+ body: formData
823
+ });
824
+ if (!uploadRes.ok) {
825
+ const errBody = await uploadRes.json().catch(() => ({}));
826
+ throw new Error(
827
+ errBody.error ?? `Upload failed: ${uploadRes.statusText}`
828
+ );
829
+ }
830
+ setProgress(100);
831
+ const data = await uploadRes.json();
832
+ if (!data.url) {
833
+ throw new Error("Upload response missing url");
834
+ }
835
+ return { url: data.url };
836
+ }
837
+ throw new Error(`Unknown provider: ${provider}`);
838
+ } catch (err) {
839
+ const message = err instanceof Error ? err.message : "Upload failed";
840
+ setError(message);
841
+ throw err;
842
+ } finally {
843
+ setIsUploading(false);
844
+ setProgress(0);
845
+ }
846
+ },
847
+ [uploadApiUrl, provider]
848
+ );
849
+ return {
850
+ uploadFile,
851
+ isUploading,
852
+ progress,
853
+ error,
854
+ resetError
855
+ };
856
+ };
857
+ const CloudMediaUpload = ({
858
+ onSuccess,
859
+ onError,
860
+ accept,
861
+ uploadApiUrl,
862
+ provider,
863
+ buttonText = "Upload to cloud",
864
+ className,
865
+ disabled = false,
866
+ id: providedId,
867
+ icon
868
+ }) => {
869
+ const id = providedId ?? `cloud-media-upload-${Math.random().toString(36).slice(2, 9)}`;
870
+ const inputRef = useRef(null);
871
+ const {
872
+ uploadFile,
873
+ isUploading,
874
+ progress,
875
+ error,
876
+ resetError
877
+ } = useCloudMediaUpload({ uploadApiUrl, provider });
878
+ const handleFileChange = async (e) => {
879
+ var _a;
880
+ const file = (_a = e.target.files) == null ? void 0 : _a[0];
881
+ if (!file) return;
882
+ try {
883
+ const { url } = await uploadFile(file);
884
+ onSuccess(url, file);
885
+ if (inputRef.current) {
886
+ inputRef.current.value = "";
887
+ }
888
+ } catch (err) {
889
+ const message = err instanceof Error ? err.message : "Upload failed";
890
+ onError == null ? void 0 : onError(message);
891
+ }
892
+ };
893
+ const handleLabelClick = () => {
894
+ if (disabled || isUploading) return;
895
+ resetError();
896
+ };
897
+ return /* @__PURE__ */ jsxs("div", { className: "file-input-container cloud-media-upload-container", children: [
898
+ /* @__PURE__ */ jsx(
899
+ "input",
900
+ {
901
+ ref: inputRef,
902
+ type: "file",
903
+ accept,
904
+ className: "file-input-hidden",
905
+ id,
906
+ onChange: handleFileChange,
907
+ disabled: disabled || isUploading,
908
+ "aria-label": buttonText
909
+ }
910
+ ),
911
+ /* @__PURE__ */ jsxs(
912
+ "label",
913
+ {
914
+ htmlFor: id,
915
+ className: className ?? "btn-primary file-input-label",
916
+ onClick: handleLabelClick,
917
+ style: { pointerEvents: disabled || isUploading ? "none" : void 0 },
918
+ children: [
919
+ icon ?? /* @__PURE__ */ jsx(Upload, { className: "icon-sm" }),
920
+ isUploading ? `${Math.round(progress)}%` : buttonText
921
+ ]
922
+ }
923
+ ),
924
+ isUploading && /* @__PURE__ */ jsx("div", { className: "cloud-media-upload-progress", role: "progressbar", "aria-valuenow": progress, "aria-valuemin": 0, "aria-valuemax": 100, children: /* @__PURE__ */ jsx(
925
+ "div",
926
+ {
927
+ className: "cloud-media-upload-progress-fill",
928
+ style: { width: `${progress}%` }
929
+ }
930
+ ) }),
931
+ error && /* @__PURE__ */ jsx("div", { className: "cloud-media-upload-error", role: "alert", children: error })
932
+ ] });
933
+ };
755
934
  const _MediaManagerSingleton = class _MediaManagerSingleton {
756
935
  constructor() {
757
936
  }
@@ -1309,19 +1488,42 @@ const AudioPanelContainer = (props) => {
1309
1488
  });
1310
1489
  addItem(newItem);
1311
1490
  };
1312
- return /* @__PURE__ */ jsx(
1313
- AudioPanel,
1314
- {
1315
- items,
1316
- searchQuery,
1317
- onSearchChange: setSearchQuery,
1318
- onItemSelect: handleSelection,
1319
- onFileUpload: handleFileUpload,
1320
- isLoading,
1321
- acceptFileTypes,
1322
- onUrlAdd
1323
- }
1324
- );
1491
+ const onCloudUploadSuccess = async (url, file) => {
1492
+ var _a;
1493
+ const newItem = await mediaManager.addItem({
1494
+ name: file.name,
1495
+ url,
1496
+ type: "audio",
1497
+ metadata: { source: ((_a = props.uploadConfig) == null ? void 0 : _a.provider) ?? "s3" }
1498
+ });
1499
+ addItem(newItem);
1500
+ };
1501
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1502
+ props.uploadConfig && /* @__PURE__ */ jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsx(
1503
+ CloudMediaUpload,
1504
+ {
1505
+ uploadApiUrl: props.uploadConfig.uploadApiUrl,
1506
+ provider: props.uploadConfig.provider,
1507
+ accept: "audio/*",
1508
+ onSuccess: onCloudUploadSuccess,
1509
+ buttonText: "Upload audio",
1510
+ className: "btn-ghost w-full"
1511
+ }
1512
+ ) }),
1513
+ /* @__PURE__ */ jsx(
1514
+ AudioPanel,
1515
+ {
1516
+ items,
1517
+ searchQuery,
1518
+ onSearchChange: setSearchQuery,
1519
+ onItemSelect: handleSelection,
1520
+ onFileUpload: handleFileUpload,
1521
+ isLoading,
1522
+ acceptFileTypes,
1523
+ onUrlAdd
1524
+ }
1525
+ )
1526
+ ] });
1325
1527
  };
1326
1528
  function ImagePanel({
1327
1529
  items,
@@ -1407,19 +1609,42 @@ function ImagePanelContainer(props) {
1407
1609
  });
1408
1610
  addItem(newItem);
1409
1611
  };
1410
- return /* @__PURE__ */ jsx(
1411
- ImagePanel,
1412
- {
1413
- items,
1414
- searchQuery,
1415
- onSearchChange: setSearchQuery,
1416
- onItemSelect: handleSelection,
1417
- onFileUpload: handleFileUpload,
1418
- isLoading,
1419
- acceptFileTypes,
1420
- onUrlAdd
1421
- }
1422
- );
1612
+ const onCloudUploadSuccess = async (url, file) => {
1613
+ var _a;
1614
+ const newItem = await mediaManager.addItem({
1615
+ name: file.name,
1616
+ url,
1617
+ type: "image",
1618
+ metadata: { source: ((_a = props.uploadConfig) == null ? void 0 : _a.provider) ?? "s3" }
1619
+ });
1620
+ addItem(newItem);
1621
+ };
1622
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1623
+ props.uploadConfig && /* @__PURE__ */ jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsx(
1624
+ CloudMediaUpload,
1625
+ {
1626
+ uploadApiUrl: props.uploadConfig.uploadApiUrl,
1627
+ provider: props.uploadConfig.provider,
1628
+ accept: "image/*",
1629
+ onSuccess: onCloudUploadSuccess,
1630
+ buttonText: "Upload image",
1631
+ className: "btn-ghost w-full"
1632
+ }
1633
+ ) }),
1634
+ /* @__PURE__ */ jsx(
1635
+ ImagePanel,
1636
+ {
1637
+ items,
1638
+ searchQuery,
1639
+ onSearchChange: setSearchQuery,
1640
+ onItemSelect: handleSelection,
1641
+ onFileUpload: handleFileUpload,
1642
+ isLoading,
1643
+ acceptFileTypes,
1644
+ onUrlAdd
1645
+ }
1646
+ )
1647
+ ] });
1423
1648
  }
1424
1649
  const useVideoPreview = () => {
1425
1650
  const [playingVideo, setPlayingVideo] = useState(null);
@@ -1573,17 +1798,40 @@ function VideoPanelContainer(props) {
1573
1798
  });
1574
1799
  addItem(newItem);
1575
1800
  };
1576
- return /* @__PURE__ */ jsx(
1577
- VideoPanel,
1578
- {
1579
- items,
1580
- onItemSelect: handleSelection,
1581
- onFileUpload: handleFileUpload,
1582
- isLoading,
1583
- acceptFileTypes,
1584
- onUrlAdd
1585
- }
1586
- );
1801
+ const onCloudUploadSuccess = async (url, file) => {
1802
+ var _a;
1803
+ const newItem = await mediaManager.addItem({
1804
+ name: file.name,
1805
+ url,
1806
+ type: "video",
1807
+ metadata: { source: ((_a = props.uploadConfig) == null ? void 0 : _a.provider) ?? "s3" }
1808
+ });
1809
+ addItem(newItem);
1810
+ };
1811
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1812
+ props.uploadConfig && /* @__PURE__ */ jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsx(
1813
+ CloudMediaUpload,
1814
+ {
1815
+ uploadApiUrl: props.uploadConfig.uploadApiUrl,
1816
+ provider: props.uploadConfig.provider,
1817
+ accept: "video/*",
1818
+ onSuccess: onCloudUploadSuccess,
1819
+ buttonText: "Upload video",
1820
+ className: "btn-ghost w-full"
1821
+ }
1822
+ ) }),
1823
+ /* @__PURE__ */ jsx(
1824
+ VideoPanel,
1825
+ {
1826
+ items,
1827
+ onItemSelect: handleSelection,
1828
+ onFileUpload: handleFileUpload,
1829
+ isLoading,
1830
+ acceptFileTypes,
1831
+ onUrlAdd
1832
+ }
1833
+ )
1834
+ ] });
1587
1835
  }
1588
1836
  function TextPanel({
1589
1837
  textContent,
@@ -2584,6 +2832,42 @@ const CAPTION_PROPS = {
2584
2832
  shadowOffset: [-2, 2],
2585
2833
  shadowColor: "#000000",
2586
2834
  shadowBlur: 5
2835
+ },
2836
+ [CAPTION_STYLE.OUTLINE_ONLY]: {
2837
+ font: {
2838
+ size: 42,
2839
+ weight: 600,
2840
+ family: "Arial"
2841
+ },
2842
+ colors: {
2843
+ text: "#ffffff",
2844
+ highlight: "#ff4081",
2845
+ bgColor: "#000000"
2846
+ },
2847
+ lineWidth: 0.5,
2848
+ stroke: "#000000",
2849
+ fontWeight: 600,
2850
+ shadowOffset: [0, 0],
2851
+ shadowColor: "#000000",
2852
+ shadowBlur: 0
2853
+ },
2854
+ [CAPTION_STYLE.SOFT_BOX]: {
2855
+ font: {
2856
+ size: 40,
2857
+ weight: 600,
2858
+ family: "Montserrat"
2859
+ },
2860
+ colors: {
2861
+ text: "#ffffff",
2862
+ highlight: "#ff4081",
2863
+ bgColor: "#333333"
2864
+ },
2865
+ lineWidth: 0.2,
2866
+ stroke: "#000000",
2867
+ fontWeight: 600,
2868
+ shadowOffset: [-1, 1],
2869
+ shadowColor: "rgba(0,0,0,0.3)",
2870
+ shadowBlur: 3
2587
2871
  }
2588
2872
  };
2589
2873
  const useCaptionsPanel = () => {
@@ -2673,12 +2957,324 @@ function CaptionsPanelContainer() {
2673
2957
  const captionsPanelProps = useCaptionsPanel();
2674
2958
  return /* @__PURE__ */ jsx(CaptionsPanel, { ...captionsPanelProps });
2675
2959
  }
2960
+ const FAL_IMAGE_ENDPOINTS = [
2961
+ {
2962
+ provider: "fal",
2963
+ endpointId: "fal-ai/flux-pro/kontext",
2964
+ label: "FLUX.1 Kontext [pro]",
2965
+ description: "Professional image generation with context-aware editing",
2966
+ popularity: 5,
2967
+ category: "image",
2968
+ inputAsset: ["image"],
2969
+ availableDimensions: [
2970
+ { width: 1024, height: 1024, label: "1024x1024 (1:1)" },
2971
+ { width: 1024, height: 576, label: "1024x576 (16:9)" },
2972
+ { width: 576, height: 1024, label: "576x1024 (9:16)" }
2973
+ ]
2974
+ },
2975
+ {
2976
+ provider: "fal",
2977
+ endpointId: "fal-ai/flux/dev",
2978
+ label: "FLUX.1 [dev]",
2979
+ description: "High-quality image generation",
2980
+ popularity: 5,
2981
+ category: "image",
2982
+ minSteps: 1,
2983
+ maxSteps: 50,
2984
+ defaultSteps: 28,
2985
+ minGuidanceScale: 1,
2986
+ maxGuidanceScale: 20,
2987
+ defaultGuidanceScale: 3.5,
2988
+ hasSeed: true
2989
+ },
2990
+ {
2991
+ provider: "fal",
2992
+ endpointId: "fal-ai/flux/schnell",
2993
+ label: "FLUX.1 [schnell]",
2994
+ description: "Ultra-fast image generation",
2995
+ popularity: 4,
2996
+ category: "image",
2997
+ defaultSteps: 4,
2998
+ availableDimensions: [
2999
+ { width: 1024, height: 1024, label: "1024x1024 (1:1)" },
3000
+ { width: 1024, height: 576, label: "1024x576 (16:9)" },
3001
+ { width: 576, height: 1024, label: "576x1024 (9:16)" }
3002
+ ]
3003
+ },
3004
+ {
3005
+ provider: "fal",
3006
+ endpointId: "fal-ai/gemini-25-flash-image",
3007
+ label: "Gemini 2.5 Flash Image",
3008
+ description: "Rapid text-to-image generation",
3009
+ popularity: 5,
3010
+ category: "image",
3011
+ availableDimensions: [
3012
+ { width: 1024, height: 1024, label: "1024x1024 (1:1)" },
3013
+ { width: 1024, height: 768, label: "1024x768 (4:3)" },
3014
+ { width: 768, height: 1024, label: "768x1024 (3:4)" },
3015
+ { width: 1024, height: 576, label: "1024x576 (16:9)" },
3016
+ { width: 576, height: 1024, label: "576x1024 (9:16)" }
3017
+ ]
3018
+ },
3019
+ {
3020
+ provider: "fal",
3021
+ endpointId: "fal-ai/ideogram/v3",
3022
+ label: "Ideogram V3",
3023
+ description: "Advanced text-to-image with superior text rendering",
3024
+ popularity: 5,
3025
+ category: "image",
3026
+ hasSeed: true,
3027
+ hasNegativePrompt: true
3028
+ }
3029
+ ];
3030
+ const FAL_VIDEO_ENDPOINTS = [
3031
+ {
3032
+ provider: "fal",
3033
+ endpointId: "fal-ai/veo3",
3034
+ label: "Veo 3",
3035
+ description: "Google Veo 3 text-to-video",
3036
+ popularity: 5,
3037
+ category: "video",
3038
+ availableDurations: [4, 6, 8],
3039
+ defaultDuration: 8,
3040
+ availableDimensions: [
3041
+ { width: 576, height: 1024, label: "576x1024 (9:16)" },
3042
+ { width: 1024, height: 576, label: "1024x576 (16:9)" },
3043
+ { width: 1024, height: 1024, label: "1024x1024 (1:1)" }
3044
+ ]
3045
+ },
3046
+ {
3047
+ provider: "fal",
3048
+ endpointId: "fal-ai/veo3/fast",
3049
+ label: "Veo 3 Fast",
3050
+ description: "Accelerated Veo 3 text-to-video",
3051
+ popularity: 5,
3052
+ category: "video",
3053
+ availableDurations: [4, 6, 8],
3054
+ defaultDuration: 8,
3055
+ availableDimensions: [
3056
+ { width: 576, height: 1024, label: "576x1024 (9:16)" },
3057
+ { width: 1024, height: 576, label: "1024x576 (16:9)" },
3058
+ { width: 1024, height: 1024, label: "1024x1024 (1:1)" }
3059
+ ]
3060
+ },
3061
+ {
3062
+ provider: "fal",
3063
+ endpointId: "fal-ai/veo3/image-to-video",
3064
+ label: "Veo 3 Image-to-Video",
3065
+ description: "Animate images with Veo 3",
3066
+ popularity: 5,
3067
+ category: "video",
3068
+ inputAsset: ["image"],
3069
+ availableDurations: [8],
3070
+ defaultDuration: 8
3071
+ },
3072
+ {
3073
+ provider: "fal",
3074
+ endpointId: "fal-ai/kling-video/v2.5-turbo/pro/text-to-video",
3075
+ label: "Kling 2.5 Turbo Pro",
3076
+ description: "Text-to-video with fluid motion",
3077
+ popularity: 5,
3078
+ category: "video",
3079
+ availableDurations: [5, 10],
3080
+ defaultDuration: 5,
3081
+ availableDimensions: [
3082
+ { width: 1024, height: 576, label: "1024x576 (16:9)" },
3083
+ { width: 576, height: 1024, label: "576x1024 (9:16)" },
3084
+ { width: 1024, height: 1024, label: "1024x1024 (1:1)" }
3085
+ ]
3086
+ }
3087
+ ];
3088
+ const DEFAULT_IMAGE_DURATION = 5;
3089
+ function GenerateMediaPanelContainer({
3090
+ videoResolution,
3091
+ addElement,
3092
+ studioConfig
3093
+ }) {
3094
+ var _a;
3095
+ const { getCurrentTime } = useLivePlayerContext();
3096
+ const [tab, setTab] = useState("image");
3097
+ const [prompt2, setPrompt] = useState("");
3098
+ const [selectedEndpointId, setSelectedEndpointId] = useState("");
3099
+ const [isGenerating, setIsGenerating] = useState(false);
3100
+ const [error, setError] = useState(null);
3101
+ const [status, setStatus] = useState(null);
3102
+ const imageService = studioConfig == null ? void 0 : studioConfig.imageGenerationService;
3103
+ const videoService = studioConfig == null ? void 0 : studioConfig.videoGenerationService;
3104
+ const hasAnyService = !!imageService || !!videoService;
3105
+ const endpoints = tab === "image" ? FAL_IMAGE_ENDPOINTS : FAL_VIDEO_ENDPOINTS;
3106
+ const defaultEndpointId = ((_a = endpoints[0]) == null ? void 0 : _a.endpointId) ?? "";
3107
+ useEffect(() => {
3108
+ if (!selectedEndpointId && defaultEndpointId) {
3109
+ setSelectedEndpointId(defaultEndpointId);
3110
+ }
3111
+ }, [tab, defaultEndpointId, selectedEndpointId]);
3112
+ const pollStatus = useCallback(
3113
+ async (requestId) => {
3114
+ const service = tab === "image" ? imageService : videoService;
3115
+ if (!service) return;
3116
+ const interval = setInterval(async () => {
3117
+ try {
3118
+ const result = await service.getRequestStatus(requestId);
3119
+ if (result.status === "completed" && result.url) {
3120
+ clearInterval(interval);
3121
+ setIsGenerating(false);
3122
+ setStatus(null);
3123
+ setError(null);
3124
+ const currentTime = getCurrentTime();
3125
+ const duration = result.duration ?? DEFAULT_IMAGE_DURATION;
3126
+ if (tab === "image") {
3127
+ const element = new ImageElement(result.url, videoResolution);
3128
+ element.setStart(currentTime);
3129
+ element.setEnd(currentTime + duration);
3130
+ addElement(element);
3131
+ } else {
3132
+ const element = new VideoElement(result.url, videoResolution);
3133
+ element.setStart(currentTime);
3134
+ element.setEnd(currentTime + duration);
3135
+ addElement(element);
3136
+ }
3137
+ } else if (result.status === "failed") {
3138
+ clearInterval(interval);
3139
+ setIsGenerating(false);
3140
+ setStatus(null);
3141
+ setError(result.error ?? "Generation failed");
3142
+ }
3143
+ } catch {
3144
+ }
3145
+ }, 3e3);
3146
+ return () => clearInterval(interval);
3147
+ },
3148
+ [tab, imageService, videoService, getCurrentTime, videoResolution, addElement]
3149
+ );
3150
+ const handleGenerate = useCallback(async () => {
3151
+ if (!prompt2.trim()) {
3152
+ setError("Enter a prompt");
3153
+ return;
3154
+ }
3155
+ if (tab === "image" && !imageService) {
3156
+ setError("Image generation not configured");
3157
+ return;
3158
+ }
3159
+ if (tab === "video" && !videoService) {
3160
+ setError("Video generation not configured");
3161
+ return;
3162
+ }
3163
+ setIsGenerating(true);
3164
+ setError(null);
3165
+ setStatus("Starting...");
3166
+ try {
3167
+ const endpointId = selectedEndpointId || defaultEndpointId;
3168
+ if (tab === "image" && imageService) {
3169
+ const requestId = await imageService.generateImage({
3170
+ provider: "fal",
3171
+ endpointId,
3172
+ prompt: prompt2.trim()
3173
+ });
3174
+ if (requestId) {
3175
+ setStatus("Generating image...");
3176
+ pollStatus(requestId);
3177
+ }
3178
+ } else if (tab === "video" && videoService) {
3179
+ const requestId = await videoService.generateVideo({
3180
+ provider: "fal",
3181
+ endpointId,
3182
+ prompt: prompt2.trim()
3183
+ });
3184
+ if (requestId) {
3185
+ setStatus("Generating video (this may take several minutes)...");
3186
+ pollStatus(requestId);
3187
+ }
3188
+ }
3189
+ } catch (err) {
3190
+ const msg = err instanceof Error ? err.message : "Generation failed";
3191
+ setError(msg);
3192
+ setIsGenerating(false);
3193
+ setStatus(null);
3194
+ }
3195
+ }, [
3196
+ tab,
3197
+ prompt2,
3198
+ selectedEndpointId,
3199
+ defaultEndpointId,
3200
+ imageService,
3201
+ videoService,
3202
+ pollStatus
3203
+ ]);
3204
+ if (!hasAnyService) {
3205
+ return /* @__PURE__ */ jsx("div", { className: "panel-container", children: /* @__PURE__ */ jsx("p", { className: "empty-state-text", children: "Image and video generation require configuration. Add imageGenerationService and videoGenerationService to StudioConfig." }) });
3206
+ }
3207
+ return /* @__PURE__ */ jsx("div", { className: "panel-container", children: /* @__PURE__ */ jsxs("div", { className: "panel-section", children: [
3208
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2 mb-2", children: [
3209
+ /* @__PURE__ */ jsx(
3210
+ "button",
3211
+ {
3212
+ type: "button",
3213
+ className: `btn-ghost ${tab === "image" ? "active" : ""}`,
3214
+ onClick: () => setTab("image"),
3215
+ disabled: !imageService,
3216
+ children: "Image"
3217
+ }
3218
+ ),
3219
+ /* @__PURE__ */ jsx(
3220
+ "button",
3221
+ {
3222
+ type: "button",
3223
+ className: `btn-ghost ${tab === "video" ? "active" : ""}`,
3224
+ onClick: () => setTab("video"),
3225
+ disabled: !videoService,
3226
+ children: "Video"
3227
+ }
3228
+ )
3229
+ ] }),
3230
+ /* @__PURE__ */ jsxs("div", { className: "mb-2", children: [
3231
+ /* @__PURE__ */ jsx("label", { className: "block text-sm mb-1", children: "Model" }),
3232
+ /* @__PURE__ */ jsx(
3233
+ "select",
3234
+ {
3235
+ className: "w-full p-2 border rounded",
3236
+ value: selectedEndpointId,
3237
+ onChange: (e) => setSelectedEndpointId(e.target.value),
3238
+ disabled: isGenerating,
3239
+ children: endpoints.map((ep) => /* @__PURE__ */ jsx("option", { value: ep.endpointId, children: ep.label }, ep.endpointId))
3240
+ }
3241
+ )
3242
+ ] }),
3243
+ /* @__PURE__ */ jsxs("div", { className: "mb-2", children: [
3244
+ /* @__PURE__ */ jsx("label", { className: "block text-sm mb-1", children: "Prompt" }),
3245
+ /* @__PURE__ */ jsx(
3246
+ "textarea",
3247
+ {
3248
+ className: "w-full p-2 border rounded min-h-[80px]",
3249
+ value: prompt2,
3250
+ onChange: (e) => setPrompt(e.target.value),
3251
+ placeholder: "Describe the image or video you want...",
3252
+ disabled: isGenerating
3253
+ }
3254
+ )
3255
+ ] }),
3256
+ error && /* @__PURE__ */ jsx("div", { className: "mb-2 text-red-600 text-sm", children: error }),
3257
+ status && /* @__PURE__ */ jsx("div", { className: "mb-2 text-sm text-gray-600", children: status }),
3258
+ /* @__PURE__ */ jsx(
3259
+ "button",
3260
+ {
3261
+ type: "button",
3262
+ className: "btn-primary w-full",
3263
+ onClick: handleGenerate,
3264
+ disabled: isGenerating || !prompt2.trim(),
3265
+ children: isGenerating ? "Generating..." : `Generate ${tab}`
3266
+ }
3267
+ )
3268
+ ] }) });
3269
+ }
2676
3270
  const ElementPanelContainer = ({
2677
3271
  selectedTool,
2678
3272
  videoResolution,
2679
3273
  selectedElement,
2680
3274
  addElement,
2681
- updateElement
3275
+ updateElement,
3276
+ uploadConfig,
3277
+ studioConfig
2682
3278
  }) => {
2683
3279
  const addNewElement = async (element) => {
2684
3280
  await addElement(element);
@@ -2692,7 +3288,8 @@ const ElementPanelContainer = ({
2692
3288
  videoResolution,
2693
3289
  selectedElement,
2694
3290
  addElement: addNewElement,
2695
- updateElement
3291
+ updateElement,
3292
+ uploadConfig
2696
3293
  }
2697
3294
  );
2698
3295
  case "audio":
@@ -2702,7 +3299,8 @@ const ElementPanelContainer = ({
2702
3299
  videoResolution,
2703
3300
  selectedElement,
2704
3301
  addElement: addNewElement,
2705
- updateElement
3302
+ updateElement,
3303
+ uploadConfig
2706
3304
  }
2707
3305
  );
2708
3306
  case "video":
@@ -2712,7 +3310,8 @@ const ElementPanelContainer = ({
2712
3310
  videoResolution,
2713
3311
  selectedElement,
2714
3312
  addElement: addNewElement,
2715
- updateElement
3313
+ updateElement,
3314
+ uploadConfig
2716
3315
  }
2717
3316
  );
2718
3317
  case "text":
@@ -2746,6 +3345,17 @@ const ElementPanelContainer = ({
2746
3345
  );
2747
3346
  case "caption":
2748
3347
  return /* @__PURE__ */ jsx(CaptionsPanelContainer, {});
3348
+ case "generate-media":
3349
+ return /* @__PURE__ */ jsx(
3350
+ GenerateMediaPanelContainer,
3351
+ {
3352
+ videoResolution,
3353
+ selectedElement,
3354
+ addElement: addNewElement,
3355
+ updateElement,
3356
+ studioConfig
3357
+ }
3358
+ );
2749
3359
  default:
2750
3360
  return /* @__PURE__ */ jsx("div", { className: "panel-container", children: /* @__PURE__ */ jsx("div", { className: "empty-state", children: /* @__PURE__ */ jsxs("div", { className: "empty-state-content", children: [
2751
3361
  /* @__PURE__ */ jsx(WandSparkles, { className: "empty-state-icon" }),
@@ -3220,6 +3830,258 @@ function Animation({
3220
3830
  })() })
3221
3831
  ] });
3222
3832
  }
3833
+ const CAPTION_FONT2 = {
3834
+ size: 40,
3835
+ family: "Bangers"
3836
+ };
3837
+ const CAPTION_COLOR2 = {
3838
+ text: "#ffffff",
3839
+ highlight: "#ff4081",
3840
+ bgColor: "#8C52FF",
3841
+ outlineColor: "#000000"
3842
+ };
3843
+ function CaptionPropPanel({
3844
+ selectedElement,
3845
+ updateElement
3846
+ }) {
3847
+ const { editor, changeLog } = useTimelineContext();
3848
+ const captionRef = useRef(null);
3849
+ const [capStyle, setCapStyle] = useState(
3850
+ CAPTION_STYLE_OPTIONS[CAPTION_STYLE.WORD_BG_HIGHLIGHT]
3851
+ );
3852
+ const [fontSize, setFontSize] = useState(CAPTION_FONT2.size);
3853
+ const [fontFamily, setFontFamily] = useState(CAPTION_FONT2.family);
3854
+ const [colors, setColors] = useState({
3855
+ text: CAPTION_COLOR2.text,
3856
+ highlight: CAPTION_COLOR2.highlight,
3857
+ bgColor: CAPTION_COLOR2.bgColor,
3858
+ outlineColor: CAPTION_COLOR2.outlineColor
3859
+ });
3860
+ const track = selectedElement instanceof CaptionElement ? editor.getTrackById(selectedElement.getTrackId()) : null;
3861
+ const trackProps = (track == null ? void 0 : track.getProps()) ?? {};
3862
+ const applyToAll = (trackProps == null ? void 0 : trackProps.applyToAll) ?? false;
3863
+ const handleUpdateCaption = (updates) => {
3864
+ const captionElement = selectedElement;
3865
+ if (!captionElement) return;
3866
+ if (applyToAll && track) {
3867
+ const nextFont = {
3868
+ size: updates.fontSize ?? fontSize,
3869
+ family: updates.fontFamily ?? fontFamily
3870
+ };
3871
+ const nextColors = updates.colors ?? colors;
3872
+ const nextCapStyle = updates.style ?? (capStyle == null ? void 0 : capStyle.value);
3873
+ track.setProps({
3874
+ ...trackProps,
3875
+ capStyle: nextCapStyle,
3876
+ font: { ...(trackProps == null ? void 0 : trackProps.font) ?? {}, ...nextFont },
3877
+ colors: nextColors
3878
+ });
3879
+ editor.refresh();
3880
+ } else {
3881
+ const elementProps = captionElement.getProps() ?? {};
3882
+ captionElement.setProps({
3883
+ ...elementProps,
3884
+ capStyle: updates.style ?? (capStyle == null ? void 0 : capStyle.value),
3885
+ font: {
3886
+ size: updates.fontSize ?? fontSize,
3887
+ family: updates.fontFamily ?? fontFamily
3888
+ },
3889
+ colors: updates.colors ?? colors
3890
+ });
3891
+ updateElement == null ? void 0 : updateElement(captionElement);
3892
+ }
3893
+ };
3894
+ useEffect(() => {
3895
+ var _a, _b;
3896
+ const captionElement = selectedElement;
3897
+ if (captionElement) {
3898
+ if (captionRef.current) {
3899
+ captionRef.current.value = captionElement == null ? void 0 : captionElement.getText();
3900
+ }
3901
+ const props = applyToAll ? trackProps : captionElement.getProps() ?? {};
3902
+ const _capStyle = props == null ? void 0 : props.capStyle;
3903
+ if (_capStyle && _capStyle in CAPTION_STYLE_OPTIONS) {
3904
+ setCapStyle(CAPTION_STYLE_OPTIONS[_capStyle]);
3905
+ }
3906
+ setFontSize(((_a = props == null ? void 0 : props.font) == null ? void 0 : _a.size) ?? CAPTION_FONT2.size);
3907
+ setFontFamily(((_b = props == null ? void 0 : props.font) == null ? void 0 : _b.family) ?? CAPTION_FONT2.family);
3908
+ const c = props == null ? void 0 : props.colors;
3909
+ setColors({
3910
+ text: (c == null ? void 0 : c.text) ?? CAPTION_COLOR2.text,
3911
+ highlight: (c == null ? void 0 : c.highlight) ?? CAPTION_COLOR2.highlight,
3912
+ bgColor: (c == null ? void 0 : c.bgColor) ?? CAPTION_COLOR2.bgColor,
3913
+ outlineColor: (c == null ? void 0 : c.outlineColor) ?? CAPTION_COLOR2.outlineColor
3914
+ });
3915
+ }
3916
+ }, [selectedElement, applyToAll, changeLog]);
3917
+ if (!(selectedElement instanceof CaptionElement)) {
3918
+ return null;
3919
+ }
3920
+ return /* @__PURE__ */ jsxs("div", { className: "panel-container", children: [
3921
+ /* @__PURE__ */ jsxs("div", { className: "panel-section", children: [
3922
+ /* @__PURE__ */ jsx("label", { className: "label-dark", children: "Caption Style" }),
3923
+ /* @__PURE__ */ jsx(
3924
+ "select",
3925
+ {
3926
+ value: capStyle.value,
3927
+ onChange: (e) => {
3928
+ const val = e.target.value;
3929
+ if (val in CAPTION_STYLE_OPTIONS) {
3930
+ setCapStyle(CAPTION_STYLE_OPTIONS[val]);
3931
+ }
3932
+ handleUpdateCaption({ style: e.target.value });
3933
+ },
3934
+ className: "select-dark w-full",
3935
+ children: Object.values(CAPTION_STYLE_OPTIONS).map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
3936
+ }
3937
+ )
3938
+ ] }),
3939
+ /* @__PURE__ */ jsxs("div", { className: "panel-section", children: [
3940
+ /* @__PURE__ */ jsx("label", { className: "label-dark", children: "Font Size" }),
3941
+ /* @__PURE__ */ jsxs("div", { className: "slider-container", children: [
3942
+ /* @__PURE__ */ jsx(
3943
+ "input",
3944
+ {
3945
+ type: "range",
3946
+ min: "8",
3947
+ max: "72",
3948
+ step: "1",
3949
+ value: fontSize,
3950
+ onChange: (e) => {
3951
+ const value = Number(e.target.value);
3952
+ setFontSize(value);
3953
+ handleUpdateCaption({ fontSize: value });
3954
+ },
3955
+ className: "slider-purple"
3956
+ }
3957
+ ),
3958
+ /* @__PURE__ */ jsxs("span", { className: "slider-value", children: [
3959
+ fontSize,
3960
+ "px"
3961
+ ] })
3962
+ ] })
3963
+ ] }),
3964
+ /* @__PURE__ */ jsxs("div", { className: "panel-section", children: [
3965
+ /* @__PURE__ */ jsx("label", { className: "label-dark", children: "Font" }),
3966
+ /* @__PURE__ */ jsxs(
3967
+ "select",
3968
+ {
3969
+ value: fontFamily,
3970
+ onChange: (e) => {
3971
+ const value = e.target.value;
3972
+ setFontFamily(value);
3973
+ handleUpdateCaption({ fontFamily: value });
3974
+ },
3975
+ className: "select-dark w-full",
3976
+ children: [
3977
+ /* @__PURE__ */ jsx("option", { value: "Bangers", children: "Bangers" }),
3978
+ /* @__PURE__ */ jsx("option", { value: "Arial", children: "Arial" }),
3979
+ /* @__PURE__ */ jsx("option", { value: "Helvetica", children: "Helvetica" }),
3980
+ /* @__PURE__ */ jsx("option", { value: "Times New Roman", children: "Times New Roman" })
3981
+ ]
3982
+ }
3983
+ )
3984
+ ] }),
3985
+ /* @__PURE__ */ jsxs("div", { className: "panel-section", children: [
3986
+ /* @__PURE__ */ jsx("label", { className: "label-dark", children: "Colors" }),
3987
+ /* @__PURE__ */ jsxs("div", { className: "color-section", children: [
3988
+ /* @__PURE__ */ jsxs("div", { className: "color-control", children: [
3989
+ /* @__PURE__ */ jsx("label", { className: "label-small", children: "Text Color" }),
3990
+ /* @__PURE__ */ jsxs("div", { className: "color-inputs", children: [
3991
+ /* @__PURE__ */ jsx(
3992
+ "input",
3993
+ {
3994
+ type: "color",
3995
+ value: colors.text,
3996
+ onChange: (e) => {
3997
+ const newColors = { ...colors, text: e.target.value };
3998
+ setColors(newColors);
3999
+ handleUpdateCaption({ colors: newColors });
4000
+ },
4001
+ className: "color-picker"
4002
+ }
4003
+ ),
4004
+ /* @__PURE__ */ jsx(
4005
+ "input",
4006
+ {
4007
+ type: "text",
4008
+ value: colors.text,
4009
+ onChange: (e) => {
4010
+ const newColors = { ...colors, text: e.target.value };
4011
+ setColors(newColors);
4012
+ handleUpdateCaption({ colors: newColors });
4013
+ },
4014
+ className: "color-text"
4015
+ }
4016
+ )
4017
+ ] })
4018
+ ] }),
4019
+ /* @__PURE__ */ jsxs("div", { className: "color-control", children: [
4020
+ /* @__PURE__ */ jsx("label", { className: "label-small", children: "Background Color" }),
4021
+ /* @__PURE__ */ jsxs("div", { className: "color-inputs", children: [
4022
+ /* @__PURE__ */ jsx(
4023
+ "input",
4024
+ {
4025
+ type: "color",
4026
+ value: colors.bgColor,
4027
+ onChange: (e) => {
4028
+ const newColors = { ...colors, bgColor: e.target.value };
4029
+ setColors(newColors);
4030
+ handleUpdateCaption({ colors: newColors });
4031
+ },
4032
+ className: "color-picker"
4033
+ }
4034
+ ),
4035
+ /* @__PURE__ */ jsx(
4036
+ "input",
4037
+ {
4038
+ type: "text",
4039
+ value: colors.bgColor,
4040
+ onChange: (e) => {
4041
+ const newColors = { ...colors, bgColor: e.target.value };
4042
+ setColors(newColors);
4043
+ handleUpdateCaption({ colors: newColors });
4044
+ },
4045
+ className: "color-text"
4046
+ }
4047
+ )
4048
+ ] })
4049
+ ] }),
4050
+ /* @__PURE__ */ jsxs("div", { className: "color-control", children: [
4051
+ /* @__PURE__ */ jsx("label", { className: "label-small", children: "Outline Color" }),
4052
+ /* @__PURE__ */ jsxs("div", { className: "color-inputs", children: [
4053
+ /* @__PURE__ */ jsx(
4054
+ "input",
4055
+ {
4056
+ type: "color",
4057
+ value: colors.outlineColor,
4058
+ onChange: (e) => {
4059
+ const newColors = { ...colors, outlineColor: e.target.value };
4060
+ setColors(newColors);
4061
+ handleUpdateCaption({ colors: newColors });
4062
+ },
4063
+ className: "color-picker"
4064
+ }
4065
+ ),
4066
+ /* @__PURE__ */ jsx(
4067
+ "input",
4068
+ {
4069
+ type: "text",
4070
+ value: colors.outlineColor,
4071
+ onChange: (e) => {
4072
+ const newColors = { ...colors, outlineColor: e.target.value };
4073
+ setColors(newColors);
4074
+ handleUpdateCaption({ colors: newColors });
4075
+ },
4076
+ className: "color-text"
4077
+ }
4078
+ )
4079
+ ] })
4080
+ ] })
4081
+ ] })
4082
+ ] })
4083
+ ] });
4084
+ }
3223
4085
  const MIN_DB = -60;
3224
4086
  const MAX_DB = 6;
3225
4087
  function linearToDb(linear) {
@@ -3667,6 +4529,7 @@ function TextPropsPanel({
3667
4529
  )
3668
4530
  ] });
3669
4531
  }
4532
+ const DEFAULT_CANVAS_BACKGROUND = "#000000";
3670
4533
  function PropertiesPanelContainer({
3671
4534
  selectedElement,
3672
4535
  updateElement,
@@ -3676,26 +4539,66 @@ function PropertiesPanelContainer({
3676
4539
  pollingIntervalMs,
3677
4540
  videoResolution
3678
4541
  }) {
4542
+ const { editor, present } = useTimelineContext();
4543
+ const backgroundColor = (present == null ? void 0 : present.backgroundColor) ?? editor.getBackgroundColor() ?? DEFAULT_CANVAS_BACKGROUND;
4544
+ const handleBackgroundColorChange = useCallback(
4545
+ (value) => {
4546
+ editor.setBackgroundColor(value);
4547
+ },
4548
+ [editor]
4549
+ );
3679
4550
  const title = selectedElement instanceof TextElement ? selectedElement.getText() : (selectedElement == null ? void 0 : selectedElement.getName()) || (selectedElement == null ? void 0 : selectedElement.getType()) || "Element";
3680
4551
  return /* @__PURE__ */ jsxs("aside", { className: "properties-panel", "aria-label": "Element properties inspector", children: [
3681
4552
  /* @__PURE__ */ jsxs("div", { className: "properties-header", children: [
3682
4553
  !selectedElement && /* @__PURE__ */ jsx("h3", { className: "properties-title", children: "Composition" }),
3683
- selectedElement && selectedElement.getType() === "caption" && /* @__PURE__ */ jsx("h3", { className: "properties-title", children: "Edit from the captions panel" }),
4554
+ selectedElement && selectedElement.getType() === "caption" && /* @__PURE__ */ jsx("h3", { className: "properties-title", children: "Caption" }),
3684
4555
  selectedElement && selectedElement.getType() !== "caption" && /* @__PURE__ */ jsx("h3", { className: "properties-title", children: title })
3685
4556
  ] }),
3686
4557
  /* @__PURE__ */ jsxs("div", { className: "prop-content", children: [
3687
4558
  !selectedElement && /* @__PURE__ */ jsxs("div", { className: "panel-container", children: [
3688
4559
  /* @__PURE__ */ jsx("div", { className: "panel-title", children: "Canvas & Render" }),
3689
- /* @__PURE__ */ jsx("div", { className: "properties-group", children: /* @__PURE__ */ jsxs("div", { className: "property-section", children: [
3690
- /* @__PURE__ */ jsx("span", { className: "property-label", children: "Size" }),
3691
- /* @__PURE__ */ jsxs("span", { className: "properties-size-readonly", children: [
3692
- videoResolution.width,
3693
- " × ",
3694
- videoResolution.height
4560
+ /* @__PURE__ */ jsxs("div", { className: "properties-group", children: [
4561
+ /* @__PURE__ */ jsxs("div", { className: "property-section", children: [
4562
+ /* @__PURE__ */ jsx("span", { className: "property-label", children: "Size" }),
4563
+ /* @__PURE__ */ jsxs("span", { className: "properties-size-readonly", children: [
4564
+ videoResolution.width,
4565
+ " × ",
4566
+ videoResolution.height
4567
+ ] })
4568
+ ] }),
4569
+ /* @__PURE__ */ jsxs("div", { className: "color-control", children: [
4570
+ /* @__PURE__ */ jsx("label", { className: "label-small", children: "Background Color" }),
4571
+ /* @__PURE__ */ jsxs("div", { className: "color-inputs", children: [
4572
+ /* @__PURE__ */ jsx(
4573
+ "input",
4574
+ {
4575
+ type: "color",
4576
+ value: backgroundColor,
4577
+ onChange: (e) => handleBackgroundColorChange(e.target.value),
4578
+ className: "color-picker"
4579
+ }
4580
+ ),
4581
+ /* @__PURE__ */ jsx(
4582
+ "input",
4583
+ {
4584
+ type: "text",
4585
+ value: backgroundColor,
4586
+ onChange: (e) => handleBackgroundColorChange(e.target.value),
4587
+ className: "color-text"
4588
+ }
4589
+ )
4590
+ ] })
3695
4591
  ] })
3696
- ] }) })
4592
+ ] })
3697
4593
  ] }),
3698
- selectedElement && selectedElement.getType() === "caption" ? null : selectedElement && /* @__PURE__ */ jsx(Fragment, { children: (() => {
4594
+ selectedElement instanceof CaptionElement && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
4595
+ CaptionPropPanel,
4596
+ {
4597
+ selectedElement,
4598
+ updateElement
4599
+ }
4600
+ ) }),
4601
+ selectedElement && !(selectedElement instanceof CaptionElement) && /* @__PURE__ */ jsx(Fragment, { children: (() => {
3699
4602
  const isText = selectedElement instanceof TextElement;
3700
4603
  const isVideo = selectedElement instanceof VideoElement;
3701
4604
  const isAudio = selectedElement instanceof AudioElement;
@@ -3857,7 +4760,7 @@ function TwickStudio({ studioConfig }) {
3857
4760
  addElement,
3858
4761
  updateElement
3859
4762
  } = useStudioManager();
3860
- const { videoResolution, setVideoResolution } = useTimelineContext();
4763
+ const { editor, present, videoResolution, setVideoResolution } = useTimelineContext();
3861
4764
  const {
3862
4765
  onNewProject,
3863
4766
  onLoadProject,
@@ -3871,16 +4774,20 @@ function TwickStudio({ studioConfig }) {
3871
4774
  pollingIntervalMs
3872
4775
  } = useGenerateCaptions(studioConfig);
3873
4776
  const twickStudiConfig = useMemo(
3874
- () => ({
3875
- canvasMode: true,
3876
- ...studioConfig || {},
3877
- videoProps: {
3878
- ...(studioConfig == null ? void 0 : studioConfig.videoProps) || {},
3879
- width: videoResolution.width,
3880
- height: videoResolution.height
3881
- }
3882
- }),
3883
- [videoResolution, studioConfig]
4777
+ () => {
4778
+ var _a2;
4779
+ return {
4780
+ canvasMode: true,
4781
+ ...studioConfig || {},
4782
+ videoProps: {
4783
+ ...(studioConfig == null ? void 0 : studioConfig.videoProps) || {},
4784
+ width: videoResolution.width,
4785
+ height: videoResolution.height,
4786
+ backgroundColor: (present == null ? void 0 : present.backgroundColor) ?? editor.getBackgroundColor() ?? ((_a2 = studioConfig == null ? void 0 : studioConfig.videoProps) == null ? void 0 : _a2.backgroundColor)
4787
+ }
4788
+ };
4789
+ },
4790
+ [videoResolution, studioConfig, present == null ? void 0 : present.backgroundColor, editor]
3884
4791
  );
3885
4792
  return /* @__PURE__ */ jsx(MediaProvider, { children: /* @__PURE__ */ jsxs("div", { className: "studio-container", children: [
3886
4793
  /* @__PURE__ */ jsx(
@@ -3909,7 +4816,9 @@ function TwickStudio({ studioConfig }) {
3909
4816
  setSelectedTool,
3910
4817
  selectedElement,
3911
4818
  addElement,
3912
- updateElement
4819
+ updateElement,
4820
+ uploadConfig: twickStudiConfig.uploadConfig,
4821
+ studioConfig: twickStudiConfig
3913
4822
  }
3914
4823
  ) }),
3915
4824
  /* @__PURE__ */ jsx("main", { className: "main-container", children: /* @__PURE__ */ jsx("div", { className: "canvas-wrapper", children: /* @__PURE__ */ jsx(
@@ -3989,11 +4898,12 @@ export {
3989
4898
  CAPTION_FONT,
3990
4899
  CAPTION_PROPS,
3991
4900
  CAPTION_STYLE2 as CAPTION_STYLE,
3992
- CAPTION_STYLE_OPTIONS,
4901
+ CAPTION_STYLE_OPTIONS2 as CAPTION_STYLE_OPTIONS,
3993
4902
  CaptionElement2 as CaptionElement,
3994
4903
  CaptionsPanel,
3995
4904
  CircleElement2 as CircleElement,
3996
4905
  CirclePanel,
4906
+ CloudMediaUpload,
3997
4907
  ElementAdder,
3998
4908
  ElementAnimation2 as ElementAnimation,
3999
4909
  ElementCloner,
@@ -4044,6 +4954,7 @@ export {
4044
4954
  isElementId,
4045
4955
  isTrackId,
4046
4956
  setElementColors,
4957
+ useCloudMediaUpload,
4047
4958
  useEditorManager2 as useEditorManager,
4048
4959
  useGenerateCaptions,
4049
4960
  useLivePlayerContext2 as useLivePlayerContext,