@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.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
  }
@@ -2828,6 +2831,42 @@ const CAPTION_PROPS = {
2828
2831
  shadowOffset: [-2, 2],
2829
2832
  shadowColor: "#000000",
2830
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
2831
2870
  }
2832
2871
  };
2833
2872
  const useCaptionsPanel = () => {
@@ -2917,13 +2956,324 @@ function CaptionsPanelContainer() {
2917
2956
  const captionsPanelProps = useCaptionsPanel();
2918
2957
  return /* @__PURE__ */ jsxRuntime.jsx(CaptionsPanel, { ...captionsPanelProps });
2919
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
+ }
2920
3269
  const ElementPanelContainer = ({
2921
3270
  selectedTool,
2922
3271
  videoResolution,
2923
3272
  selectedElement,
2924
3273
  addElement,
2925
3274
  updateElement,
2926
- uploadConfig
3275
+ uploadConfig,
3276
+ studioConfig
2927
3277
  }) => {
2928
3278
  const addNewElement = async (element) => {
2929
3279
  await addElement(element);
@@ -2994,6 +3344,17 @@ const ElementPanelContainer = ({
2994
3344
  );
2995
3345
  case "caption":
2996
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
+ );
2997
3358
  default:
2998
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: [
2999
3360
  /* @__PURE__ */ jsxRuntime.jsx(WandSparkles, { className: "empty-state-icon" }),
@@ -3468,6 +3829,258 @@ function Animation({
3468
3829
  })() })
3469
3830
  ] });
3470
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
+ }
3471
4084
  const MIN_DB = -60;
3472
4085
  const MAX_DB = 6;
3473
4086
  function linearToDb(linear) {
@@ -3915,6 +4528,7 @@ function TextPropsPanel({
3915
4528
  )
3916
4529
  ] });
3917
4530
  }
4531
+ const DEFAULT_CANVAS_BACKGROUND = "#000000";
3918
4532
  function PropertiesPanelContainer({
3919
4533
  selectedElement,
3920
4534
  updateElement,
@@ -3924,26 +4538,66 @@ function PropertiesPanelContainer({
3924
4538
  pollingIntervalMs,
3925
4539
  videoResolution
3926
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
+ );
3927
4549
  const title = selectedElement instanceof timeline.TextElement ? selectedElement.getText() : (selectedElement == null ? void 0 : selectedElement.getName()) || (selectedElement == null ? void 0 : selectedElement.getType()) || "Element";
3928
4550
  return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "properties-panel", "aria-label": "Element properties inspector", children: [
3929
4551
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "properties-header", children: [
3930
4552
  !selectedElement && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "properties-title", children: "Composition" }),
3931
- 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" }),
3932
4554
  selectedElement && selectedElement.getType() !== "caption" && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "properties-title", children: title })
3933
4555
  ] }),
3934
4556
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "prop-content", children: [
3935
4557
  !selectedElement && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-container", children: [
3936
4558
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-title", children: "Canvas & Render" }),
3937
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "properties-group", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "property-section", children: [
3938
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "property-label", children: "Size" }),
3939
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "properties-size-readonly", children: [
3940
- videoResolution.width,
3941
- " × ",
3942
- 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
+ ] })
3943
4590
  ] })
3944
- ] }) })
4591
+ ] })
3945
4592
  ] }),
3946
- 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: (() => {
3947
4601
  const isText = selectedElement instanceof timeline.TextElement;
3948
4602
  const isVideo = selectedElement instanceof timeline.VideoElement;
3949
4603
  const isAudio = selectedElement instanceof timeline.AudioElement;
@@ -4105,7 +4759,7 @@ function TwickStudio({ studioConfig }) {
4105
4759
  addElement,
4106
4760
  updateElement
4107
4761
  } = useStudioManager();
4108
- const { videoResolution, setVideoResolution } = timeline.useTimelineContext();
4762
+ const { editor, present, videoResolution, setVideoResolution } = timeline.useTimelineContext();
4109
4763
  const {
4110
4764
  onNewProject,
4111
4765
  onLoadProject,
@@ -4119,16 +4773,20 @@ function TwickStudio({ studioConfig }) {
4119
4773
  pollingIntervalMs
4120
4774
  } = useGenerateCaptions(studioConfig);
4121
4775
  const twickStudiConfig = react.useMemo(
4122
- () => ({
4123
- canvasMode: true,
4124
- ...studioConfig || {},
4125
- videoProps: {
4126
- ...(studioConfig == null ? void 0 : studioConfig.videoProps) || {},
4127
- width: videoResolution.width,
4128
- height: videoResolution.height
4129
- }
4130
- }),
4131
- [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]
4132
4790
  );
4133
4791
  return /* @__PURE__ */ jsxRuntime.jsx(MediaProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "studio-container", children: [
4134
4792
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4158,7 +4816,8 @@ function TwickStudio({ studioConfig }) {
4158
4816
  selectedElement,
4159
4817
  addElement,
4160
4818
  updateElement,
4161
- uploadConfig: twickStudiConfig.uploadConfig
4819
+ uploadConfig: twickStudiConfig.uploadConfig,
4820
+ studioConfig: twickStudiConfig
4162
4821
  }
4163
4822
  ) }),
4164
4823
  /* @__PURE__ */ jsxRuntime.jsx("main", { className: "main-container", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-wrapper", children: /* @__PURE__ */ jsxRuntime.jsx(