@twick/studio 0.15.19 → 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
@@ -3,8 +3,8 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
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
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, 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";
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
  }
@@ -2829,6 +2832,42 @@ const CAPTION_PROPS = {
2829
2832
  shadowOffset: [-2, 2],
2830
2833
  shadowColor: "#000000",
2831
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
2832
2871
  }
2833
2872
  };
2834
2873
  const useCaptionsPanel = () => {
@@ -2918,13 +2957,324 @@ function CaptionsPanelContainer() {
2918
2957
  const captionsPanelProps = useCaptionsPanel();
2919
2958
  return /* @__PURE__ */ jsx(CaptionsPanel, { ...captionsPanelProps });
2920
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
+ }
2921
3270
  const ElementPanelContainer = ({
2922
3271
  selectedTool,
2923
3272
  videoResolution,
2924
3273
  selectedElement,
2925
3274
  addElement,
2926
3275
  updateElement,
2927
- uploadConfig
3276
+ uploadConfig,
3277
+ studioConfig
2928
3278
  }) => {
2929
3279
  const addNewElement = async (element) => {
2930
3280
  await addElement(element);
@@ -2995,6 +3345,17 @@ const ElementPanelContainer = ({
2995
3345
  );
2996
3346
  case "caption":
2997
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
+ );
2998
3359
  default:
2999
3360
  return /* @__PURE__ */ jsx("div", { className: "panel-container", children: /* @__PURE__ */ jsx("div", { className: "empty-state", children: /* @__PURE__ */ jsxs("div", { className: "empty-state-content", children: [
3000
3361
  /* @__PURE__ */ jsx(WandSparkles, { className: "empty-state-icon" }),
@@ -3469,6 +3830,258 @@ function Animation({
3469
3830
  })() })
3470
3831
  ] });
3471
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
+ }
3472
4085
  const MIN_DB = -60;
3473
4086
  const MAX_DB = 6;
3474
4087
  function linearToDb(linear) {
@@ -3916,6 +4529,7 @@ function TextPropsPanel({
3916
4529
  )
3917
4530
  ] });
3918
4531
  }
4532
+ const DEFAULT_CANVAS_BACKGROUND = "#000000";
3919
4533
  function PropertiesPanelContainer({
3920
4534
  selectedElement,
3921
4535
  updateElement,
@@ -3925,26 +4539,66 @@ function PropertiesPanelContainer({
3925
4539
  pollingIntervalMs,
3926
4540
  videoResolution
3927
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
+ );
3928
4550
  const title = selectedElement instanceof TextElement ? selectedElement.getText() : (selectedElement == null ? void 0 : selectedElement.getName()) || (selectedElement == null ? void 0 : selectedElement.getType()) || "Element";
3929
4551
  return /* @__PURE__ */ jsxs("aside", { className: "properties-panel", "aria-label": "Element properties inspector", children: [
3930
4552
  /* @__PURE__ */ jsxs("div", { className: "properties-header", children: [
3931
4553
  !selectedElement && /* @__PURE__ */ jsx("h3", { className: "properties-title", children: "Composition" }),
3932
- 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" }),
3933
4555
  selectedElement && selectedElement.getType() !== "caption" && /* @__PURE__ */ jsx("h3", { className: "properties-title", children: title })
3934
4556
  ] }),
3935
4557
  /* @__PURE__ */ jsxs("div", { className: "prop-content", children: [
3936
4558
  !selectedElement && /* @__PURE__ */ jsxs("div", { className: "panel-container", children: [
3937
4559
  /* @__PURE__ */ jsx("div", { className: "panel-title", children: "Canvas & Render" }),
3938
- /* @__PURE__ */ jsx("div", { className: "properties-group", children: /* @__PURE__ */ jsxs("div", { className: "property-section", children: [
3939
- /* @__PURE__ */ jsx("span", { className: "property-label", children: "Size" }),
3940
- /* @__PURE__ */ jsxs("span", { className: "properties-size-readonly", children: [
3941
- videoResolution.width,
3942
- " × ",
3943
- 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
+ ] })
3944
4591
  ] })
3945
- ] }) })
4592
+ ] })
3946
4593
  ] }),
3947
- 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: (() => {
3948
4602
  const isText = selectedElement instanceof TextElement;
3949
4603
  const isVideo = selectedElement instanceof VideoElement;
3950
4604
  const isAudio = selectedElement instanceof AudioElement;
@@ -4106,7 +4760,7 @@ function TwickStudio({ studioConfig }) {
4106
4760
  addElement,
4107
4761
  updateElement
4108
4762
  } = useStudioManager();
4109
- const { videoResolution, setVideoResolution } = useTimelineContext();
4763
+ const { editor, present, videoResolution, setVideoResolution } = useTimelineContext();
4110
4764
  const {
4111
4765
  onNewProject,
4112
4766
  onLoadProject,
@@ -4120,16 +4774,20 @@ function TwickStudio({ studioConfig }) {
4120
4774
  pollingIntervalMs
4121
4775
  } = useGenerateCaptions(studioConfig);
4122
4776
  const twickStudiConfig = useMemo(
4123
- () => ({
4124
- canvasMode: true,
4125
- ...studioConfig || {},
4126
- videoProps: {
4127
- ...(studioConfig == null ? void 0 : studioConfig.videoProps) || {},
4128
- width: videoResolution.width,
4129
- height: videoResolution.height
4130
- }
4131
- }),
4132
- [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]
4133
4791
  );
4134
4792
  return /* @__PURE__ */ jsx(MediaProvider, { children: /* @__PURE__ */ jsxs("div", { className: "studio-container", children: [
4135
4793
  /* @__PURE__ */ jsx(
@@ -4159,7 +4817,8 @@ function TwickStudio({ studioConfig }) {
4159
4817
  selectedElement,
4160
4818
  addElement,
4161
4819
  updateElement,
4162
- uploadConfig: twickStudiConfig.uploadConfig
4820
+ uploadConfig: twickStudiConfig.uploadConfig,
4821
+ studioConfig: twickStudiConfig
4163
4822
  }
4164
4823
  ) }),
4165
4824
  /* @__PURE__ */ jsx("main", { className: "main-container", children: /* @__PURE__ */ jsx("div", { className: "canvas-wrapper", children: /* @__PURE__ */ jsx(
@@ -4239,7 +4898,7 @@ export {
4239
4898
  CAPTION_FONT,
4240
4899
  CAPTION_PROPS,
4241
4900
  CAPTION_STYLE2 as CAPTION_STYLE,
4242
- CAPTION_STYLE_OPTIONS,
4901
+ CAPTION_STYLE_OPTIONS2 as CAPTION_STYLE_OPTIONS,
4243
4902
  CaptionElement2 as CaptionElement,
4244
4903
  CaptionsPanel,
4245
4904
  CircleElement2 as CircleElement,