diff --git a/packages/studio/src/components/StudioRightPanel.tsx b/packages/studio/src/components/StudioRightPanel.tsx index b7d2c6810..ef82b14ac 100644 --- a/packages/studio/src/components/StudioRightPanel.tsx +++ b/packages/studio/src/components/StudioRightPanel.tsx @@ -12,8 +12,7 @@ import { /** Motion data without targeting metadata. */ type StudioMotionData = Omit; -import { useCallback } from "react"; -import { resolveDomEditSelection, type DomEditLayerItem } from "./editor/domEditing"; + import { useStudioContext } from "../contexts/StudioContext"; import { usePanelLayoutContext } from "../contexts/PanelLayoutContext"; import { useFileManagerContext } from "../contexts/FileManagerContext"; @@ -67,23 +66,10 @@ export function StudioRightPanel({ handleAskAgent, handleDomMotionCommit, handleDomMotionClear, - applyDomSelection, } = useDomEditContext(); - const { assets, fontAssets, handleImportFiles, handleImportFonts } = useFileManagerContext(); - - const isMasterView = !activeCompPath || activeCompPath === "index.html"; - const handleSelectLayer = useCallback( - (layer: DomEditLayerItem) => { - const selection = resolveDomEditSelection(layer.element, { - activeCompositionPath: activeCompPath, - isMasterView, - preferClipAncestor: false, - }); - if (selection) applyDomSelection(selection); - }, - [activeCompPath, isMasterView, applyDomSelection], - ); + const { assets, fontAssets, projectDir, handleImportFiles, handleImportFonts } = + useFileManagerContext(); const renderJobs = renderQueue.jobs as RenderJob[]; @@ -164,6 +150,7 @@ export function StudioRightPanel({ ) : designPanelActive ? ( 1 ? null : domEditSelection} multiSelectCount={domEditGroupSelections.length} @@ -183,8 +170,6 @@ export function StudioRightPanel({ onImportAssets={handleImportFiles} fontAssets={fontAssets} onImportFonts={handleImportFonts} - activeCompositionPath={activeCompPath} - onSelectLayer={handleSelectLayer} /> ) : motionPanelActive ? ( Promise; fontAssets?: ImportedFontAsset[]; onImportFonts?: (files: FileList | File[]) => Promise; - activeCompositionPath?: string | null; - onSelectLayer?: (layer: DomEditLayerItem) => void; -} - -/* ------------------------------------------------------------------ */ -/* LayerTree */ -/* ------------------------------------------------------------------ */ - -function LayerTree({ - element, - activeCompositionPath, - onSelectLayer, -}: { - element: DomEditSelection | null; - activeCompositionPath: string | null; - onSelectLayer: (layer: DomEditLayerItem) => void; -}) { - const isMasterView = !activeCompositionPath || activeCompositionPath === "index.html"; - const layers = collectDomEditLayerItems(element?.element, { - activeCompositionPath, - isMasterView, - }); - if (layers.length <= 1) return null; - - const selectedKey = element ? getDomEditLayerKey(element) : null; - - return ( -
}> -
- {layers.map((layer) => { - const selected = layer.key === selectedKey; - return ( - - ); - })} -
-
- ); } /* ------------------------------------------------------------------ */ @@ -184,6 +118,7 @@ function TimingSection({ export const PropertyPanel = memo(function PropertyPanel({ projectId, + projectDir, assets, element, multiSelectCount = 0, @@ -203,8 +138,6 @@ export const PropertyPanel = memo(function PropertyPanel({ onImportAssets, fontAssets = [], onImportFonts, - activeCompositionPath = null, - onSelectLayer, }: PropertyPanelProps) { const styles = element?.computedStyles ?? EMPTY_STYLES; @@ -334,11 +267,18 @@ export const PropertyPanel = memo(function PropertyPanel({ onRemoveTextField={onRemoveTextField} /> - {onSelectLayer && ( - + )} + + {isMediaElement(element) && ( + )} @@ -388,20 +328,6 @@ export const PropertyPanel = memo(function PropertyPanel({ - {element.dataAttributes.start != null && ( - - )} - - {isMediaElement(element) && ( - - )} - {showEditableSections && ( = 0 ? parsed : null; -} - export function MediaSection({ + projectDir, element, styles, onSetStyle, onSetAttribute, onSetHtmlAttribute, }: { + projectDir: string | null; element: DomEditSelection; styles: Record; onSetStyle: (prop: string, value: string) => void | Promise; @@ -65,8 +55,17 @@ export function MediaSection({ const objectFit = styles["object-fit"] || "contain"; const objectPosition = styles["object-position"] || "center"; - const poster = el.getAttribute("poster") ?? ""; - const src = el.getAttribute("src") ?? ""; + const sourceDuration = + Number.parseFloat(element.dataAttributes["source-duration"] ?? "") || + (el as HTMLMediaElement).duration || + 0; + const mediaStartMax = Math.max(30, Math.ceil(sourceDuration || mediaStart + 10)); + + const srcAttr = el.getAttribute("src") ?? ""; + const [copied, setCopied] = useState(false); + + const absoluteSrc = + projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr; return (
: } >
- {src && ( + {srcAttr && (
-
Source
-
- {src.split("/").pop() || src} +
+
Source
+ +
+
+ {absoluteSrc}
)} @@ -98,25 +115,35 @@ export function MediaSection({ />
- { - const parsed = Number.parseFloat(next); - if (!Number.isFinite(parsed) || parsed < 0.1 || parsed > 5) return; - void onSetAttribute("playback-rate", formatNumericValue(parsed)); - }} - /> +
+ Playback rate + `${formatNumericValue(next / 100)}x`} + onCommit={(next) => { + void onSetAttribute("playback-rate", formatNumericValue(next / 100)); + }} + /> +
- { - const parsed = parseTimingValue(next); - if (parsed == null) return; - void onSetAttribute("media-start", parsed.toFixed(2)); - }} - /> +
+ Media start + formatTimingValue(next / 100)} + onCommit={(next) => { + void onSetAttribute("media-start", (next / 100).toFixed(2)); + }} + /> +
@@ -180,22 +207,25 @@ export function MediaSection({ }} options={["contain", "cover", "fill", "none", "scale-down"]} /> - { + onChange={(next) => { void onSetStyle("object-position", next); }} + options={[ + "center", + "top", + "bottom", + "left", + "right", + "left top", + "right top", + "left bottom", + "right bottom", + ]} />
- - { - void onSetHtmlAttribute("poster", next || null); - }} - /> )}
diff --git a/packages/studio/src/hooks/useAppHotkeys.ts b/packages/studio/src/hooks/useAppHotkeys.ts index 111ecc69a..31ebac4a7 100644 --- a/packages/studio/src/hooks/useAppHotkeys.ts +++ b/packages/studio/src/hooks/useAppHotkeys.ts @@ -57,7 +57,6 @@ export function useAppHotkeys({ handleTimelineElementDelete, handleDomEditElementDelete, domEditSelectionRef, - clearDomSelectionRef, editHistory, readOptionalProjectFile, readProjectFile, @@ -116,12 +115,10 @@ export function useAppHotkeys({ return; } if (result.ok && result.label) { - clearDomSelectionRef.current(); await syncHistoryPreviewAfterApply(result.paths); showToast(`Undid ${result.label}`, "info"); } }, [ - clearDomSelectionRef, editHistory, readHistoryProjectFile, showToast, @@ -141,12 +138,10 @@ export function useAppHotkeys({ return; } if (result.ok && result.label) { - clearDomSelectionRef.current(); await syncHistoryPreviewAfterApply(result.paths); showToast(`Redid ${result.label}`, "info"); } }, [ - clearDomSelectionRef, editHistory, readHistoryProjectFile, showToast, diff --git a/packages/studio/vite.adapter.ts b/packages/studio/vite.adapter.ts index c61e3cf05..bbcb553ec 100644 --- a/packages/studio/vite.adapter.ts +++ b/packages/studio/vite.adapter.ts @@ -1,6 +1,6 @@ // Vite adapter that wires the shared Studio API to the local filesystem and build tools. -import { readFileSync, readdirSync, existsSync, writeFileSync } from "node:fs"; +import { readFileSync, readdirSync, existsSync, writeFileSync, realpathSync } from "node:fs"; import { join, relative, resolve, isAbsolute } from "node:path"; import type { ViteDevServer } from "vite"; import { @@ -132,7 +132,11 @@ export function createViteAdapter(dataDir: string, server: ViteDevServer): Studi if (session.projectId) { projectDir = join(dataDir, session.projectId); if (existsSync(projectDir)) { - return { id: session.projectId, dir: projectDir, title: session.title }; + return { + id: session.projectId, + dir: realpathSync(projectDir), + title: session.title, + }; } } } catch { @@ -141,7 +145,7 @@ export function createViteAdapter(dataDir: string, server: ViteDevServer): Studi } return null; } - return { id, dir: projectDir }; + return { id, dir: realpathSync(projectDir) }; }, async bundle(dir: string) {