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