From a1414f06f69c7f23c7abffa4ef4d5a1e3aa7cbfb Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Fri, 12 Jun 2026 09:05:44 +0330 Subject: [PATCH] feat(studio): phone editing for Video Studio + Image Editor (remove desktop gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the "desktop only" gate on phones with real mobile editing layouts. Shared: - BottomSheet (mobile slide-up panel) hosting the desktop side-docks on phones. - Side panels made width-fluid (w-full on mobile, fixed on md+): StudioSidebarContent, ImageEditorRightPanel. Video Studio (VideoStudioMobileLayout): - Canvas fills the viewport; the vertical tool dock becomes a scrollable bottom bar; each tool's panel + the timeline open as bottom sheets. Exported MAIN_DOCK_ITEMS. Image Editor (ImageEditorMobileLayout): - Canvas fills the viewport; toolbar → scrollable bottom bar; Adjust/Filters/Layers panel + shape picker open as bottom sheets. Exported IMAGE_TOOLS/IMAGE_SHAPES. - Touch editing: Stage now handles onTouchStart/Move/End (draw, select, move) with touch-action:none; draw-tool stroke works with a finger. Pointer handlers widened to MouseEvent | TouchEvent. i18n: added timeline/preview/panels keys (fa+en, parity verified). Full next build + tsc clean. (Studio is auth-gated — verify editing on a device.) Co-Authored-By: Claude Opus 4.8 --- messages/en.json | 3 + messages/fa.json | 3 + .../image-editor/ImageEditorLayout.tsx | 11 +- .../image-editor/ImageEditorMobileLayout.tsx | 135 ++++++++++++++++++ .../image-editor/ImageEditorRightPanel.tsx | 2 +- .../image-editor/ImageEditorToolbar.tsx | 6 +- .../image-editor/canvas/ImageEditorCanvas.tsx | 11 +- src/components/studio/mobile/BottomSheet.tsx | 64 +++++++++ .../studio/video/StudioSidebarContent.tsx | 2 +- .../studio/video/StudioSidebarDock.tsx | 2 +- .../studio/video/VideoStudioLayout.tsx | 12 +- .../studio/video/VideoStudioMobileLayout.tsx | 113 +++++++++++++++ 12 files changed, 352 insertions(+), 12 deletions(-) create mode 100644 src/components/image-editor/ImageEditorMobileLayout.tsx create mode 100644 src/components/studio/mobile/BottomSheet.tsx create mode 100644 src/components/studio/video/VideoStudioMobileLayout.tsx diff --git a/messages/en.json b/messages/en.json index 74b041d..a2469fd 100644 --- a/messages/en.json +++ b/messages/en.json @@ -914,6 +914,7 @@ "toolShape": "Shape", "toolDraw": "Draw", "toolAi": "AI", + "panels": "Adjust", "shapeRectangle": "Rectangle", "shapeCircle": "Circle", "shapeLine": "Line", @@ -1223,6 +1224,8 @@ "transitions": "Transitions", "font": "Font", "myWatermark": "My Watermark", + "timeline": "Timeline", + "preview": "Preview", "toolsNavLabel": "Studio tools", "guideMe": "Guide me", "guideComingSoon": "👋 Guide coming soon!", diff --git a/messages/fa.json b/messages/fa.json index 64d9d58..2bf1533 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -914,6 +914,7 @@ "toolShape": "شکل", "toolDraw": "ترسیم", "toolAi": "هوش مصنوعی", + "panels": "تنظیمات", "shapeRectangle": "مستطیل", "shapeCircle": "دایره", "shapeLine": "خط", @@ -1223,6 +1224,8 @@ "transitions": "گذارها", "font": "فونت", "myWatermark": "واترمارک من", + "timeline": "خط زمان", + "preview": "پیش‌نمایش", "toolsNavLabel": "ابزارهای استودیو", "guideMe": "راهنمایی‌ام کن", "guideComingSoon": "👋 راهنما به‌زودی ارائه می‌شود!", diff --git a/src/components/image-editor/ImageEditorLayout.tsx b/src/components/image-editor/ImageEditorLayout.tsx index 2258ed2..3af6aa6 100644 --- a/src/components/image-editor/ImageEditorLayout.tsx +++ b/src/components/image-editor/ImageEditorLayout.tsx @@ -6,8 +6,8 @@ import { AiRemoveBgModal } from "@/components/image-editor/AiRemoveBgModal"; import { ImageCropControls } from "@/components/image-editor/ImageCropControls"; import { ImageEditorRightPanel } from "@/components/image-editor/ImageEditorRightPanel"; import { ImageEditorToolbar } from "@/components/image-editor/ImageEditorToolbar"; +import { ImageEditorMobileLayout } from "@/components/image-editor/ImageEditorMobileLayout"; import { ImageEditorTopBar } from "@/components/image-editor/ImageEditorTopBar"; -import { StudioMobileGate } from "@/components/studio/StudioMobileGate"; import { Toaster } from "@/components/ui/toaster"; import { useImageProjectPersistence } from "@/hooks/useImageProjectPersistence"; import { useIsMobile } from "@/hooks/useIsMobile"; @@ -34,7 +34,14 @@ export function ImageEditorLayout({ projectId }: ImageEditorLayoutProps) { } if (isMobile) { - return ; + return ( + + ); } return ( diff --git a/src/components/image-editor/ImageEditorMobileLayout.tsx b/src/components/image-editor/ImageEditorMobileLayout.tsx new file mode 100644 index 0000000..c988d92 --- /dev/null +++ b/src/components/image-editor/ImageEditorMobileLayout.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { useState, type ComponentProps } from "react"; +import dynamic from "next/dynamic"; +import { SlidersHorizontal } from "lucide-react"; +import { useTranslations } from "next-intl"; + +import { AiRemoveBgModal } from "@/components/image-editor/AiRemoveBgModal"; +import { ImageCropControls } from "@/components/image-editor/ImageCropControls"; +import { ImageEditorRightPanel } from "@/components/image-editor/ImageEditorRightPanel"; +import { + IMAGE_SHAPES, + IMAGE_TOOLS, +} from "@/components/image-editor/ImageEditorToolbar"; +import { ImageEditorTopBar } from "@/components/image-editor/ImageEditorTopBar"; +import { BottomSheet } from "@/components/studio/mobile/BottomSheet"; +import { Toaster } from "@/components/ui/toaster"; +import { useImageEditorStore } from "@/lib/image-editor-store"; +import { cn } from "@/lib/utils"; + +const ImageEditorCanvas = dynamic( + () => + import("@/components/image-editor/canvas/ImageEditorCanvas").then( + (mod) => mod.ImageEditorCanvas, + ), + { ssr: false, loading: () =>
}, +); + +type TopBarProps = ComponentProps; + +interface Props { + projectId?: string; + projectName: TopBarProps["projectName"]; + saveStatus: TopBarProps["saveStatus"]; + onSaveRetry: TopBarProps["onSaveRetry"]; +} + +/** + * Phone layout for the Image Editor: canvas fills the viewport, the tool dock + * becomes a scrollable bottom bar, and the Adjust/Filters/Layers panel opens as a + * bottom sheet. Drawing/selection work via touch (see ImageEditorCanvas). + */ +export function ImageEditorMobileLayout({ projectId, projectName, saveStatus, onSaveRetry }: Props) { + const t = useTranslations("auto.componentsImageEditorImageEditorToolbar"); + const activeTool = useImageEditorStore((s) => s.activeTool); + const setActiveTool = useImageEditorStore((s) => s.setActiveTool); + const setPendingShape = useImageEditorStore((s) => s.setPendingShape); + const setAiModalOpen = useImageEditorStore((s) => s.setAiModalOpen); + const [panelOpen, setPanelOpen] = useState(false); + const [shapeOpen, setShapeOpen] = useState(false); + + return ( +
+ + + +
+ +
+ + {/* Bottom tool bar */} + + + setPanelOpen(false)}> + + + + setShapeOpen(false)} title={t("toolShape")}> +
+ {IMAGE_SHAPES.map((s) => ( + + ))} +
+
+ + +
+ ); +} diff --git a/src/components/image-editor/ImageEditorRightPanel.tsx b/src/components/image-editor/ImageEditorRightPanel.tsx index b452ebd..4525d71 100644 --- a/src/components/image-editor/ImageEditorRightPanel.tsx +++ b/src/components/image-editor/ImageEditorRightPanel.tsx @@ -21,7 +21,7 @@ export function ImageEditorRightPanel() { const setActivePanelTab = useImageEditorStore((s) => s.setActivePanelTab); return ( -
+ + ); +} diff --git a/src/components/studio/video/StudioSidebarContent.tsx b/src/components/studio/video/StudioSidebarContent.tsx index d338284..0b17a9a 100644 --- a/src/components/studio/video/StudioSidebarContent.tsx +++ b/src/components/studio/video/StudioSidebarContent.tsx @@ -15,7 +15,7 @@ interface StudioSidebarContentProps { export function StudioSidebarContent({ activeTool }: StudioSidebarContentProps) { return ( -
+
{activeTool === "scenes" ? : null} {activeTool === "audio" ? : null} {activeTool === "tts" ? : null} diff --git a/src/components/studio/video/StudioSidebarDock.tsx b/src/components/studio/video/StudioSidebarDock.tsx index c206de9..fa379fb 100644 --- a/src/components/studio/video/StudioSidebarDock.tsx +++ b/src/components/studio/video/StudioSidebarDock.tsx @@ -26,7 +26,7 @@ export type StudioSidebarTool = | "font" | "watermark"; -const MAIN_DOCK_ITEMS: { +export const MAIN_DOCK_ITEMS: { id: StudioSidebarTool; labelKey: string; icon: LucideIcon; diff --git a/src/components/studio/video/VideoStudioLayout.tsx b/src/components/studio/video/VideoStudioLayout.tsx index 53afc03..72c7471 100644 --- a/src/components/studio/video/VideoStudioLayout.tsx +++ b/src/components/studio/video/VideoStudioLayout.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; -import { StudioMobileGate } from "@/components/studio/StudioMobileGate"; import { Toaster } from "@/components/ui/toaster"; import { CanvasArea } from "@/components/studio/video/CanvasArea"; import { @@ -11,6 +10,7 @@ import { } from "@/components/studio/video/StudioSidebarDock"; import { StudioSidebarContent } from "@/components/studio/video/StudioSidebarContent"; import { StudioTopBar } from "@/components/studio/video/StudioTopBar"; +import { VideoStudioMobileLayout } from "@/components/studio/video/VideoStudioMobileLayout"; import { Timeline } from "@/components/studio/Timeline"; import { useIsMobile } from "@/hooks/useIsMobile"; import { useStudioProjectPersistence } from "@/hooks/useStudioProjectPersistence"; @@ -31,7 +31,15 @@ export function VideoStudioLayout({ projectId }: VideoStudioLayoutProps) { } if (isMobile) { - return ; + return ( + + ); } return ( diff --git a/src/components/studio/video/VideoStudioMobileLayout.tsx b/src/components/studio/video/VideoStudioMobileLayout.tsx new file mode 100644 index 0000000..65fdd00 --- /dev/null +++ b/src/components/studio/video/VideoStudioMobileLayout.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useState, type ComponentProps } from "react"; +import { Clock } from "lucide-react"; +import { useTranslations } from "next-intl"; + +import { BottomSheet } from "@/components/studio/mobile/BottomSheet"; +import { CanvasArea } from "@/components/studio/video/CanvasArea"; +import { + MAIN_DOCK_ITEMS, + type StudioSidebarTool, +} from "@/components/studio/video/StudioSidebarDock"; +import { StudioSidebarContent } from "@/components/studio/video/StudioSidebarContent"; +import { StudioTopBar } from "@/components/studio/video/StudioTopBar"; +import { Timeline } from "@/components/studio/Timeline"; +import { Toaster } from "@/components/ui/toaster"; +import { cn } from "@/lib/utils"; + +interface MobileProps { + projectId: string; + projectName: string; + onProjectNameChange: (name: string) => void; + saveStatus: ComponentProps["saveStatus"]; + usingLocalStorage: boolean; +} + +/** + * Phone layout for the Video Studio: canvas fills the viewport, the tool dock + * becomes a scrollable bottom bar, and each tool's panel + the timeline open as + * bottom sheets. Replaces the old "desktop only" gate. + */ +export function VideoStudioMobileLayout({ + projectId, + projectName, + onProjectNameChange, + saveStatus, + usingLocalStorage, +}: MobileProps) { + const t = useTranslations("auto.componentsStudioVideoStudioSidebarDock"); + const [tool, setTool] = useState(null); + const [showTimeline, setShowTimeline] = useState(false); + const activeLabel = tool ? t(MAIN_DOCK_ITEMS.find((i) => i.id === tool)?.labelKey ?? "scenes") : ""; + + return ( +
+ + + +
+ +
+ + {/* Bottom tool bar */} + + + setTool(null)} title={activeLabel}> + {tool && } + + + setShowTimeline(false)} title={t("timeline")} heightClass="max-h-[60vh]"> + { + setShowTimeline(false); + setTool("tts"); + }} + onOpenAudio={() => { + setShowTimeline(false); + setTool("audio"); + }} + onSceneSelect={() => setShowTimeline(false)} + /> + +
+ ); +}