mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-05 05:37:09 +00:00
feat(studio): simplify inspector selection ux
This commit is contained in:
+19
-70
@@ -78,10 +78,10 @@ import {
|
||||
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
||||
STUDIO_MOTION_PANEL_ENABLED,
|
||||
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
|
||||
STUDIO_PREVIEW_SELECTION_ENABLED,
|
||||
STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED,
|
||||
} from "./components/editor/manualEditingAvailability";
|
||||
import {
|
||||
buildDefaultDomEditTextField,
|
||||
buildDomEditStylePatchOperation,
|
||||
buildDomEditTextPatchOperation,
|
||||
buildElementAgentPrompt,
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
getDomEditLayerKey,
|
||||
getDomEditTargetKey,
|
||||
isTextEditableSelection,
|
||||
resolveVisualDomEditSelectionTarget,
|
||||
serializeDomEditTextFields,
|
||||
resolveDomEditSelection,
|
||||
type DomEditLayerItem,
|
||||
@@ -362,6 +363,7 @@ function getPreviewTargetFromPointer(
|
||||
iframe: HTMLIFrameElement,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
activeCompositionPath: string | null,
|
||||
): HTMLElement | null {
|
||||
let doc: Document | null = null;
|
||||
let win: Window | null = null;
|
||||
@@ -386,6 +388,16 @@ function getPreviewTargetFromPointer(
|
||||
const localX = (clientX - iframeRect.left) / scaleX;
|
||||
const localY = (clientY - iframeRect.top) / scaleY;
|
||||
|
||||
if (typeof doc.elementsFromPoint === "function") {
|
||||
const visualTarget = resolveVisualDomEditSelectionTarget(
|
||||
doc.elementsFromPoint(localX, localY),
|
||||
{
|
||||
activeCompositionPath,
|
||||
},
|
||||
);
|
||||
if (visualTarget) return visualTarget;
|
||||
}
|
||||
|
||||
return getEventTargetElement(doc.elementFromPoint(localX, localY));
|
||||
}
|
||||
|
||||
@@ -778,9 +790,6 @@ export function StudioApp() {
|
||||
const [agentModalOpen, setAgentModalOpen] = useState(false);
|
||||
const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
|
||||
const [inspectedTimelineElementId, setInspectedTimelineElementId] = useState<string | null>(null);
|
||||
const [thumbnailedTimelineElementIds, setThumbnailedTimelineElementIds] = useState<
|
||||
ReadonlySet<string>
|
||||
>(() => new Set());
|
||||
const [previewDocumentVersion, setPreviewDocumentVersion] = useState(0);
|
||||
const refreshPreviewDocumentVersion = useCallback(() => {
|
||||
setPreviewDocumentVersion((version) => version + 1);
|
||||
@@ -2296,13 +2305,13 @@ export function StudioApp() {
|
||||
(clientX: number, clientY: number, options?: { preferClipAncestor?: boolean }) => {
|
||||
const iframe = previewIframeRef.current;
|
||||
if (!iframe || captionEditMode) return null;
|
||||
const target = getPreviewTargetFromPointer(iframe, clientX, clientY);
|
||||
const target = getPreviewTargetFromPointer(iframe, clientX, clientY, activeCompPath);
|
||||
if (!target) return null;
|
||||
return buildDomSelectionFromTarget(target, {
|
||||
preferClipAncestor: options?.preferClipAncestor,
|
||||
});
|
||||
},
|
||||
[buildDomSelectionFromTarget, captionEditMode],
|
||||
[activeCompPath, buildDomSelectionFromTarget, captionEditMode],
|
||||
);
|
||||
|
||||
const updateDomEditHoverSelection = useCallback((selection: DomEditSelection | null) => {
|
||||
@@ -2439,17 +2448,6 @@ export function StudioApp() {
|
||||
[applyDomSelection, buildDomSelectionForTimelineElement, currentTime, showToast],
|
||||
);
|
||||
|
||||
const handleToggleTimelineElementThumbnail = useCallback((element: TimelineElement) => {
|
||||
const key = getTimelineElementKey(element);
|
||||
if (!key) return;
|
||||
setThumbnailedTimelineElementIds((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleTimelineLayerSelect = useCallback(
|
||||
(layer: DomEditLayerItem) => {
|
||||
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
||||
@@ -2991,48 +2989,6 @@ export function StudioApp() {
|
||||
[commitDomTextFields, domEditSelection, handleDomStyleCommit, resolveImportedFontAsset],
|
||||
);
|
||||
|
||||
const handleDomAddTextField = useCallback(
|
||||
async (afterFieldKey?: string) => {
|
||||
if (!domEditSelection) return null;
|
||||
if (!domEditSelection.textFields.some((field) => field.source === "child")) return null;
|
||||
|
||||
const insertionIndex = domEditSelection.textFields.findIndex(
|
||||
(field) => field.key === afterFieldKey,
|
||||
);
|
||||
const baseField =
|
||||
domEditSelection.textFields[insertionIndex >= 0 ? insertionIndex : 0] ??
|
||||
domEditSelection.textFields[0];
|
||||
const nextField = buildDefaultDomEditTextField(baseField);
|
||||
const nextTextFields = [...domEditSelection.textFields];
|
||||
nextTextFields.splice(
|
||||
insertionIndex >= 0 ? insertionIndex + 1 : nextTextFields.length,
|
||||
0,
|
||||
nextField,
|
||||
);
|
||||
|
||||
await commitDomTextFields(domEditSelection, nextTextFields);
|
||||
return nextField.key;
|
||||
},
|
||||
[commitDomTextFields, domEditSelection],
|
||||
);
|
||||
|
||||
const handleDomRemoveTextField = useCallback(
|
||||
async (fieldKey: string) => {
|
||||
if (!domEditSelection) return;
|
||||
const field = domEditSelection.textFields.find((entry) => entry.key === fieldKey);
|
||||
if (!field) return;
|
||||
|
||||
if (field.source === "self") {
|
||||
await handleDomTextCommit("", fieldKey);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTextFields = domEditSelection.textFields.filter((entry) => entry.key !== fieldKey);
|
||||
await commitDomTextFields(domEditSelection, nextTextFields);
|
||||
},
|
||||
[commitDomTextFields, domEditSelection, handleDomTextCommit],
|
||||
);
|
||||
|
||||
const handleAskAgent = useCallback(() => {
|
||||
if (!domEditSelection) return;
|
||||
setAgentPromptTagSnippet(undefined);
|
||||
@@ -3083,9 +3039,9 @@ export function StudioApp() {
|
||||
|
||||
const handlePreviewCanvasMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
||||
if (!STUDIO_PREVIEW_MANUAL_EDITING_ENABLED || captionEditMode) return;
|
||||
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode) return;
|
||||
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
||||
preferClipAncestor: options?.preferClipAncestor ?? true,
|
||||
preferClipAncestor: options?.preferClipAncestor ?? false,
|
||||
});
|
||||
if (!nextSelection) {
|
||||
if (!e.shiftKey) applyDomSelection(null, { revealPanel: false });
|
||||
@@ -3100,7 +3056,7 @@ export function StudioApp() {
|
||||
|
||||
const handlePreviewCanvasPointerMove = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
||||
if (!STUDIO_PREVIEW_MANUAL_EDITING_ENABLED || captionEditMode) {
|
||||
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode) {
|
||||
updateDomEditHoverSelection(null);
|
||||
return null;
|
||||
}
|
||||
@@ -3978,8 +3934,6 @@ export function StudioApp() {
|
||||
onInspectTimelineElement={handleTimelineElementInspect}
|
||||
inspectedTimelineElementId={inspectedTimelineElementId}
|
||||
timelineLayerChildCounts={timelineLayerChildCounts}
|
||||
thumbnailedTimelineElementIds={thumbnailedTimelineElementIds}
|
||||
onToggleTimelineElementThumbnail={handleToggleTimelineElementThumbnail}
|
||||
onCompIdToSrcChange={setCompIdToSrc}
|
||||
onCompositionChange={(compPath) => {
|
||||
// Sync activeCompPath when user drills down via timeline double-click
|
||||
@@ -3997,7 +3951,7 @@ export function StudioApp() {
|
||||
iframeRef={previewIframeRef}
|
||||
activeCompositionPath={activeCompPath}
|
||||
hoverSelection={
|
||||
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED && !captionEditMode
|
||||
STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode
|
||||
? domEditHoverSelection
|
||||
: null
|
||||
}
|
||||
@@ -4103,8 +4057,6 @@ export function StudioApp() {
|
||||
<div className="min-h-0 flex-1">
|
||||
{designPanelActive ? (
|
||||
<PropertyPanel
|
||||
projectId={projectId}
|
||||
assets={assets}
|
||||
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
||||
copiedAgentPrompt={copiedAgentPrompt}
|
||||
onClearSelection={clearDomSelection}
|
||||
@@ -4113,11 +4065,8 @@ export function StudioApp() {
|
||||
onSetManualSize={handleDomBoxSizeCommit}
|
||||
onSetText={handleDomTextCommit}
|
||||
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
|
||||
onAddTextField={handleDomAddTextField}
|
||||
onRemoveTextField={handleDomRemoveTextField}
|
||||
onResetManualEdits={handleDomManualEditsReset}
|
||||
onAskAgent={handleAskAgent}
|
||||
onImportAssets={handleImportFiles}
|
||||
fontAssets={fontAssets}
|
||||
onImportFonts={handleImportFonts}
|
||||
/>
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
buildStrokeWidthStyleUpdates,
|
||||
getClipPathInsetPx,
|
||||
getCssFilterFunctionPx,
|
||||
getPropertyPanelVisibleSections,
|
||||
inferBoxShadowPreset,
|
||||
inferClipPathPreset,
|
||||
isPropertyPanelMediaLikeSelection,
|
||||
normalizePanelPxValue,
|
||||
setCssFilterFunctionPx,
|
||||
} from "./PropertyPanel";
|
||||
@@ -64,4 +66,51 @@ describe("PropertyPanel style helpers", () => {
|
||||
expect(buildStrokeStyleUpdates("none", "4px")).toEqual([["border-style", "none"]]);
|
||||
expect(buildStrokeStyleUpdates("solid", "4px")).toEqual([["border-style", "solid"]]);
|
||||
});
|
||||
|
||||
it("orders the simplified default inspector sections around high-confidence edits", () => {
|
||||
expect(
|
||||
getPropertyPanelVisibleSections({
|
||||
hasSelection: true,
|
||||
canEditStyles: true,
|
||||
hasTextControls: true,
|
||||
hasColorControls: true,
|
||||
}),
|
||||
).toEqual(["Text", "Layout", "Colors", "Radius", "Shadow"]);
|
||||
|
||||
expect(
|
||||
getPropertyPanelVisibleSections({
|
||||
hasSelection: true,
|
||||
canEditStyles: true,
|
||||
hasTextControls: false,
|
||||
hasColorControls: false,
|
||||
}),
|
||||
).toEqual(["Layout", "Radius", "Shadow"]);
|
||||
});
|
||||
|
||||
it("treats media tags and background-image layers as image-like controls", () => {
|
||||
expect(
|
||||
isPropertyPanelMediaLikeSelection({
|
||||
tagName: "img",
|
||||
styles: {},
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isPropertyPanelMediaLikeSelection({
|
||||
tagName: "div",
|
||||
styles: {
|
||||
"background-image": "url(/assets/studio.png)",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isPropertyPanelMediaLikeSelection({
|
||||
tagName: "div",
|
||||
styles: {
|
||||
"background-image": "none",
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import {
|
||||
getDomEditNonEditableReason,
|
||||
getDomEditTargetKey,
|
||||
isTextEditableSelection,
|
||||
resolveVisualDomEditSelectionTarget,
|
||||
serializeDomEditTextFields,
|
||||
type DomEditSelection,
|
||||
resolveDomEditCapabilities,
|
||||
@@ -23,6 +24,30 @@ function createDocument(markup: string): Document {
|
||||
return window.document;
|
||||
}
|
||||
|
||||
function setElementRect(
|
||||
element: HTMLElement,
|
||||
rect: Partial<Pick<DOMRect, "left" | "top" | "width" | "height">>,
|
||||
) {
|
||||
const left = rect.left ?? 0;
|
||||
const top = rect.top ?? 0;
|
||||
const width = rect.width ?? 100;
|
||||
const height = rect.height ?? 40;
|
||||
Object.defineProperty(element, "getBoundingClientRect", {
|
||||
configurable: true,
|
||||
value: () => ({
|
||||
x: left,
|
||||
y: top,
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
right: left + width,
|
||||
bottom: top + height,
|
||||
toJSON: () => null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
describe("resolveDomEditCapabilities", () => {
|
||||
it("marks absolute px-positioned layers as movable and resizable", () => {
|
||||
expect(
|
||||
@@ -199,6 +224,104 @@ describe("resolveDomEditCapabilities", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveVisualDomEditSelectionTarget", () => {
|
||||
it("prefers the visible leaf under the pointer over an oversized container", () => {
|
||||
const document = createDocument(`
|
||||
<section id="container" class="hero-shell">
|
||||
<span id="headline" class="headline">Launch faster</span>
|
||||
</section>
|
||||
`);
|
||||
const container = document.getElementById("container") as HTMLElement;
|
||||
const headline = document.getElementById("headline") as HTMLElement;
|
||||
setElementRect(container, { width: 900, height: 520 });
|
||||
setElementRect(headline, { left: 240, top: 160, width: 180, height: 36 });
|
||||
|
||||
expect(
|
||||
resolveVisualDomEditSelectionTarget([container, headline], {
|
||||
activeCompositionPath: "index.html",
|
||||
}),
|
||||
).toBe(headline);
|
||||
});
|
||||
|
||||
it("skips hidden and zero-size elements before picking a rendered candidate", () => {
|
||||
const document = createDocument(`
|
||||
<div id="hidden" style="display: none">Hidden</div>
|
||||
<div id="empty">Empty</div>
|
||||
<div id="visible">Visible</div>
|
||||
`);
|
||||
const hidden = document.getElementById("hidden") as HTMLElement;
|
||||
const empty = document.getElementById("empty") as HTMLElement;
|
||||
const visible = document.getElementById("visible") as HTMLElement;
|
||||
setElementRect(hidden, { width: 120, height: 32 });
|
||||
setElementRect(empty, { width: 0, height: 0 });
|
||||
setElementRect(visible, { width: 120, height: 32 });
|
||||
|
||||
expect(
|
||||
resolveVisualDomEditSelectionTarget([hidden, empty, visible], {
|
||||
activeCompositionPath: "index.html",
|
||||
}),
|
||||
).toBe(visible);
|
||||
});
|
||||
|
||||
it("skips transparent elements that still report a box", () => {
|
||||
const document = createDocument(`
|
||||
<button id="transparent" style="opacity: 0">Transparent</button>
|
||||
<button id="visible">Visible</button>
|
||||
`);
|
||||
const transparent = document.getElementById("transparent") as HTMLElement;
|
||||
const visible = document.getElementById("visible") as HTMLElement;
|
||||
setElementRect(transparent, { width: 120, height: 32 });
|
||||
setElementRect(visible, { width: 120, height: 32 });
|
||||
|
||||
expect(
|
||||
resolveVisualDomEditSelectionTarget([transparent, visible], {
|
||||
activeCompositionPath: "index.html",
|
||||
}),
|
||||
).toBe(visible);
|
||||
});
|
||||
|
||||
it("falls back to the nearest stable editable ancestor when a visual child has no target", () => {
|
||||
const document = createDocument(`
|
||||
<section id="card">
|
||||
<span>Unlabeled copy</span>
|
||||
</section>
|
||||
`);
|
||||
const card = document.getElementById("card") as HTMLElement;
|
||||
const span = card.querySelector("span") as HTMLElement;
|
||||
setElementRect(card, { width: 400, height: 200 });
|
||||
setElementRect(span, { left: 40, top: 40, width: 140, height: 28 });
|
||||
|
||||
expect(
|
||||
resolveVisualDomEditSelectionTarget([span, card], {
|
||||
activeCompositionPath: "index.html",
|
||||
}),
|
||||
).toBe(card);
|
||||
});
|
||||
|
||||
it("keeps explicit layer selection able to target containers", () => {
|
||||
const document = createDocument(`
|
||||
<section id="container" class="hero-shell">
|
||||
<span id="headline" class="headline">Launch faster</span>
|
||||
</section>
|
||||
`);
|
||||
const container = document.getElementById("container") as HTMLElement;
|
||||
const headline = document.getElementById("headline") as HTMLElement;
|
||||
setElementRect(container, { width: 900, height: 520 });
|
||||
setElementRect(headline, { left: 240, top: 160, width: 180, height: 36 });
|
||||
|
||||
const visualTarget = resolveVisualDomEditSelectionTarget([container, headline], {
|
||||
activeCompositionPath: "index.html",
|
||||
});
|
||||
const explicitSelection = resolveDomEditSelection(container, {
|
||||
activeCompositionPath: "index.html",
|
||||
isMasterView: false,
|
||||
});
|
||||
|
||||
expect(visualTarget).toBe(headline);
|
||||
expect(explicitSelection?.id).toBe("container");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDomEditSelection", () => {
|
||||
it("keeps composition host transforms disabled in master view", () => {
|
||||
expect(
|
||||
@@ -643,6 +766,36 @@ describe("resolveDomEditSelection", () => {
|
||||
).toBe(root);
|
||||
});
|
||||
|
||||
it("normalizes preview URLs when resolving master timeline composition clips", () => {
|
||||
const document = createDocument(`
|
||||
<div data-composition-id="main">
|
||||
<div
|
||||
id="slide-1"
|
||||
data-composition-id="slide-core-conviction"
|
||||
data-composition-src="compositions/slide-01-core-conviction.html"
|
||||
>
|
||||
<h1>Core Conviction</h1>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
const slide = document.getElementById("slide-1") as HTMLElement;
|
||||
|
||||
expect(
|
||||
findElementForTimelineElement(
|
||||
document,
|
||||
{
|
||||
id: "slide-1",
|
||||
compositionSrc:
|
||||
"http://127.0.0.1:5176/api/projects/apple-presentation-download/preview/compositions/slide-01-core-conviction.html",
|
||||
},
|
||||
{
|
||||
activeCompositionPath: null,
|
||||
isMasterView: true,
|
||||
},
|
||||
),
|
||||
).toBe(slide);
|
||||
});
|
||||
|
||||
it("does not fall back to the root composition when an explicit timeline selector misses", () => {
|
||||
const document = createDocument(`
|
||||
<div data-composition-id="hook">
|
||||
|
||||
@@ -291,6 +291,27 @@ function escapeCssString(value: string): string {
|
||||
.replace(/\f/g, "\\c ");
|
||||
}
|
||||
|
||||
function normalizeTimelineCompositionSource(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return undefined;
|
||||
|
||||
let pathname = trimmed;
|
||||
try {
|
||||
pathname = new URL(trimmed, "http://studio.local").pathname;
|
||||
} catch {
|
||||
pathname = trimmed;
|
||||
}
|
||||
|
||||
for (const marker of ["/preview/comp/", "/preview/"]) {
|
||||
const markerIndex = pathname.indexOf(marker);
|
||||
if (markerIndex < 0) continue;
|
||||
const sourcePath = pathname.slice(markerIndex + marker.length).replace(/^\/+/, "");
|
||||
return sourcePath || trimmed;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function querySelectorAllSafely(doc: Document, selector: string): Element[] {
|
||||
try {
|
||||
return Array.from(doc.querySelectorAll(selector));
|
||||
@@ -416,6 +437,66 @@ function getDomLayerPatchTarget(
|
||||
};
|
||||
}
|
||||
|
||||
function getElementDepth(el: HTMLElement): number {
|
||||
let depth = 0;
|
||||
let current = el.parentElement;
|
||||
while (current) {
|
||||
depth += 1;
|
||||
current = current.parentElement;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
function hasRenderedBox(el: HTMLElement): boolean {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width <= 1 || rect.height <= 1) return false;
|
||||
|
||||
const computed = el.ownerDocument.defaultView?.getComputedStyle(el);
|
||||
if (!computed) return true;
|
||||
if (computed.display === "none" || computed.visibility === "hidden") return false;
|
||||
|
||||
const opacity = Number.parseFloat(computed.opacity);
|
||||
if (Number.isFinite(opacity) && opacity <= 0.01) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getVisualElementScore(el: HTMLElement, pointerStackIndex: number): number {
|
||||
const tagName = el.tagName.toLowerCase();
|
||||
const rect = el.getBoundingClientRect();
|
||||
const area = Math.max(1, rect.width * rect.height);
|
||||
const smallerElementBonus = Math.max(0, 1_000_000 - Math.min(area, 1_000_000)) / 1_000;
|
||||
const visualLeafBonus =
|
||||
isEditableTextLeaf(el) || ["img", "video", "canvas", "svg"].includes(tagName) ? 2_000 : 0;
|
||||
|
||||
return getElementDepth(el) * 10_000 + visualLeafBonus + smallerElementBonus - pointerStackIndex;
|
||||
}
|
||||
|
||||
export function resolveVisualDomEditSelectionTarget(
|
||||
elementsFromPoint: Iterable<Element | null | undefined>,
|
||||
options: Pick<DomEditContextOptions, "activeCompositionPath">,
|
||||
): HTMLElement | null {
|
||||
let best: { element: HTMLElement; score: number } | null = null;
|
||||
let pointerStackIndex = 0;
|
||||
|
||||
for (const entry of elementsFromPoint) {
|
||||
if (!isHtmlElement(entry)) {
|
||||
pointerStackIndex += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasRenderedBox(entry) && getDomLayerPatchTarget(entry, options.activeCompositionPath)) {
|
||||
const score = getVisualElementScore(entry, pointerStackIndex);
|
||||
if (!best || score > best.score) {
|
||||
best = { element: entry, score };
|
||||
}
|
||||
}
|
||||
pointerStackIndex += 1;
|
||||
}
|
||||
|
||||
return best?.element ?? null;
|
||||
}
|
||||
|
||||
function getDirectLayerChildren(el: HTMLElement, options: DomEditContextOptions): HTMLElement[] {
|
||||
return Array.from(el.children).filter(
|
||||
(child): child is HTMLElement =>
|
||||
@@ -848,9 +929,14 @@ export function findElementForTimelineElement(
|
||||
options: TimelineElementDomTargetOptions,
|
||||
): HTMLElement | null {
|
||||
const elementId = typeof element.id === "string" ? element.id : "";
|
||||
const compositionSource = element.compositionSrc ?? options.compIdToSrc?.get(elementId);
|
||||
const compositionSource =
|
||||
normalizeTimelineCompositionSource(element.compositionSrc) ??
|
||||
options.compIdToSrc?.get(elementId);
|
||||
const sourceFile =
|
||||
compositionSource ?? element.sourceFile ?? options.activeCompositionPath ?? "index.html";
|
||||
compositionSource ??
|
||||
normalizeTimelineCompositionSource(element.sourceFile) ??
|
||||
options.activeCompositionPath ??
|
||||
"index.html";
|
||||
const escapedElementId = escapeCssString(elementId);
|
||||
const escapedCompositionSource = compositionSource ? escapeCssString(compositionSource) : null;
|
||||
const selector =
|
||||
|
||||
@@ -16,10 +16,11 @@ describe("manual editing availability", () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("enables inspector layers by default while motion and manual editing stay opt-in", async () => {
|
||||
it("enables inspector selection by default while motion and manual dragging stay opt-in", async () => {
|
||||
const availability = await loadAvailabilityWithEnv({});
|
||||
|
||||
expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(false);
|
||||
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true);
|
||||
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
|
||||
expect(availability.STUDIO_MOTION_PANEL_ENABLED).toBe(false);
|
||||
expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(true);
|
||||
@@ -51,6 +52,7 @@ describe("manual editing availability", () => {
|
||||
});
|
||||
|
||||
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(false);
|
||||
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(false);
|
||||
expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ export const STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED =
|
||||
true,
|
||||
);
|
||||
|
||||
export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
|
||||
|
||||
export const STUDIO_MANUAL_EDITING_ENABLED = STUDIO_PREVIEW_MANUAL_EDITING_ENABLED;
|
||||
|
||||
export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
|
||||
|
||||
@@ -55,8 +55,6 @@ interface NLELayoutProps {
|
||||
onInspectTimelineElement?: (element: TimelineElement) => void;
|
||||
inspectedTimelineElementId?: string | null;
|
||||
timelineLayerChildCounts?: ReadonlyMap<string, number>;
|
||||
thumbnailedTimelineElementIds?: ReadonlySet<string>;
|
||||
onToggleTimelineElementThumbnail?: (element: TimelineElement) => void;
|
||||
/** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
|
||||
onCompIdToSrcChange?: (map: Map<string, string>) => void;
|
||||
/** Whether the timeline panel is visible (default: true) */
|
||||
@@ -94,8 +92,6 @@ export const NLELayout = memo(function NLELayout({
|
||||
onInspectTimelineElement,
|
||||
inspectedTimelineElementId,
|
||||
timelineLayerChildCounts,
|
||||
thumbnailedTimelineElementIds,
|
||||
onToggleTimelineElementThumbnail,
|
||||
onCompIdToSrcChange,
|
||||
timelineVisible,
|
||||
onToggleTimeline,
|
||||
@@ -453,8 +449,6 @@ export const NLELayout = memo(function NLELayout({
|
||||
onInspectElement={onInspectTimelineElement}
|
||||
inspectedElementId={inspectedTimelineElementId}
|
||||
layerChildCounts={timelineLayerChildCounts}
|
||||
thumbnailedElementIds={thumbnailedTimelineElementIds}
|
||||
onToggleElementThumbnail={onToggleTimelineElementThumbnail}
|
||||
disabled={timelineDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,11 @@ import { join } from "node:path";
|
||||
import {
|
||||
createStudioManualEditsRenderBodyScript,
|
||||
type StudioManualEditsRenderScriptOptions,
|
||||
} from "@hyperframes/core/studio-api/manual-edits-render-script";
|
||||
} from "../core/src/studio-api/helpers/manualEditsRenderScript";
|
||||
import {
|
||||
createStudioMotionRenderBodyScript,
|
||||
STUDIO_MOTION_PATH,
|
||||
} from "@hyperframes/core/studio-api/studio-motion-render-script";
|
||||
} from "../core/src/studio-api/helpers/studioMotionRenderScript";
|
||||
|
||||
const STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Reference in New Issue
Block a user