diff --git a/messages/en.json b/messages/en.json
index 1eede62..69d1b8c 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1189,6 +1189,14 @@
"replaceImage": "Replace image",
"uploadImage": "Upload image"
},
+ "componentsStudioSidebarBlockFieldForm": {
+ "panelTitle": "Edit Scene",
+ "emptyState": "This scene has no editable fields.",
+ "fieldFallback": "Field {index}",
+ "textPlaceholder": "Type here…",
+ "replaceImage": "Replace image",
+ "uploadImage": "Upload image"
+ },
"componentsStudioSidebarTransitionsSidebarContent": {
"heading": "Transitions",
"randomTransition": "Random Transition",
diff --git a/messages/fa.json b/messages/fa.json
index 0abf193..82f3041 100644
--- a/messages/fa.json
+++ b/messages/fa.json
@@ -1189,6 +1189,14 @@
"replaceImage": "جایگزینی تصویر",
"uploadImage": "بارگذاری تصویر"
},
+ "componentsStudioSidebarBlockFieldForm": {
+ "panelTitle": "ویرایش صحنه",
+ "emptyState": "این صحنه فیلد قابلویرایشی ندارد.",
+ "fieldFallback": "فیلد {index}",
+ "textPlaceholder": "اینجا بنویسید…",
+ "replaceImage": "جایگزینی تصویر",
+ "uploadImage": "بارگذاری تصویر"
+ },
"componentsStudioSidebarTransitionsSidebarContent": {
"heading": "ترانزیشنها",
"randomTransition": "ترانزیشن تصادفی",
diff --git a/src/components/studio/sidebar/BlockFieldForm.tsx b/src/components/studio/sidebar/BlockFieldForm.tsx
new file mode 100644
index 0000000..2fc90f4
--- /dev/null
+++ b/src/components/studio/sidebar/BlockFieldForm.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import { useRef } from "react";
+import { ImagePlus, Type } from "lucide-react";
+import { useTranslations } from "next-intl";
+
+import { getTextProps, mergeLayerProps } from "@/lib/studio-layer-props";
+import { useStudioStore } from "@/lib/studio-store";
+
+/**
+ * Clean per-field content editor for FlexStory (scene-engine) projects. Each
+ * scene's editable content is already bridged to `c-`-prefixed text/image layers
+ * carrying the field's Persian label in `layer.name`; this renders one labelled
+ * input per field and writes back through the SAME updateLayer path that already
+ * persists to saved_scene_contents — so it's purely a cleaner presentation of the
+ * existing, working content path (no Konva geometry, no layer chrome).
+ */
+export function BlockFieldForm() {
+ const t = useTranslations("auto.componentsStudioSidebarBlockFieldForm");
+ const scenes = useStudioStore((s) => s.scenes);
+ const activeSceneId = useStudioStore((s) => s.activeSceneId);
+ const updateLayer = useStudioStore((s) => s.updateLayer);
+
+ const activeScene = scenes.find((s) => s.id === activeSceneId);
+ const fields = (activeScene?.layers ?? []).filter(
+ (l) => l.type === "text" || l.type === "image"
+ );
+
+ return (
+
+
+
+ {t("panelTitle")}
+
+ {activeScene && (
+
{activeScene.name}
+ )}
+
+
+
+ {fields.length === 0 ? (
+
+ ) : (
+ fields.map((layer, idx) =>
+ layer.type === "image" ? (
+
+ updateLayer(layer.id, { props: mergeLayerProps(layer.props, { src }) })
+ }
+ />
+ ) : (
+
+ updateLayer(layer.id, { props: mergeLayerProps(layer.props, { text }) })
+ }
+ />
+ )
+ )
+ )}
+
+
+ );
+}
+
+function TextField({
+ label,
+ value,
+ onChange,
+ placeholder,
+}: {
+ label: string;
+ value: string;
+ onChange: (v: string) => void;
+ placeholder: string;
+}) {
+ const MAX = 190;
+ return (
+
+
+
+ MAX
+ ? "text-[10px] tabular-nums text-red-500"
+ : "text-[10px] tabular-nums text-gray-400"
+ }
+ >
+ {value.length}/{MAX}
+
+
+
+ );
+}
+
+function ImageField({
+ label,
+ src,
+ onReplace,
+ replaceLabel,
+ uploadLabel,
+}: {
+ label: string;
+ src: string | null;
+ onReplace: (src: string) => void;
+ replaceLabel: string;
+ uploadLabel: string;
+}) {
+ const inputRef = useRef(null);
+ const handleFile = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ if (typeof reader.result === "string") onReplace(reader.result);
+ };
+ reader.readAsDataURL(file);
+ e.target.value = "";
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/studio/video/StudioSidebarContent.tsx b/src/components/studio/video/StudioSidebarContent.tsx
index 0b17a9a..8bbda57 100644
--- a/src/components/studio/video/StudioSidebarContent.tsx
+++ b/src/components/studio/video/StudioSidebarContent.tsx
@@ -1,6 +1,7 @@
"use client";
import { AudioSidebarContent } from "@/components/studio/sidebar/AudioSidebarContent";
+import { BlockFieldForm } from "@/components/studio/sidebar/BlockFieldForm";
import { ColorsSidebarContent } from "@/components/studio/sidebar/ColorsSidebarContent";
import { FontSidebarContent } from "@/components/studio/sidebar/FontSidebarContent";
import { SceneEditSidebarContent } from "@/components/studio/sidebar/SceneEditSidebarContent";
@@ -8,15 +9,23 @@ import { TransitionsSidebarContent } from "@/components/studio/sidebar/Transitio
import { TtsSidebarContent } from "@/components/studio/sidebar/TtsSidebarContent";
import { WatermarkSidebarContent } from "@/components/studio/sidebar/WatermarkSidebarContent";
import type { StudioSidebarTool } from "@/components/studio/video/StudioSidebarDock";
+import { isFlexStoryProject, useStudioStore } from "@/lib/studio-store";
interface StudioSidebarContentProps {
activeTool: StudioSidebarTool;
}
export function StudioSidebarContent({ activeTool }: StudioSidebarContentProps) {
+ // FlexStory (scene-engine) projects get the clean block-field editor; AE/Konva
+ // templates keep the layer editor — zero regression for existing projects.
+ const flexStory = useStudioStore((s) => isFlexStoryProject(s.chooseMode));
return (
- {activeTool === "scenes" ?
: null}
+ {activeTool === "scenes"
+ ? flexStory
+ ?
+ :
+ : null}
{activeTool === "audio" ?
: null}
{activeTool === "tts" ?
: null}
{activeTool === "colors" ?
: null}
diff --git a/src/lib/studio-store.ts b/src/lib/studio-store.ts
index c4f941b..8b86dac 100644
--- a/src/lib/studio-store.ts
+++ b/src/lib/studio-store.ts
@@ -859,6 +859,12 @@ export function isFixedSceneMode(chooseMode: string | null | undefined): boolean
return FIXED_SCENE_MODES.has((chooseMode ?? "").toLowerCase());
}
+/** FLEXIBLE projects are the FlexStory scene-engine → use the clean block-field
+ * editor (BlockFieldForm) instead of the Konva layer panel. */
+export function isFlexStoryProject(chooseMode: string | null | undefined): boolean {
+ return (chooseMode ?? "").toLowerCase() === "flexible";
+}
+
export function getActiveScene(
state: Pick
): Scene | undefined {