mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-12 20:47:30 +00:00
fix(studio): route inspector geometry through manual edits
This commit is contained in:
committed by
Miguel Ángel
parent
d17288fc70
commit
62b83a0e2b
+50
-84
@@ -239,10 +239,7 @@ function normalizeDomEditStyleValue(property: string, value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
|
||||
if (
|
||||
["left", "top", "width", "height", "border-radius", "font-size"].includes(property) &&
|
||||
/^-?\d+(\.\d+)?$/.test(trimmed)
|
||||
) {
|
||||
if (["border-radius", "font-size"].includes(property) && /^-?\d+(\.\d+)?$/.test(trimmed)) {
|
||||
return `${trimmed}px`;
|
||||
}
|
||||
|
||||
@@ -253,27 +250,6 @@ function isImageBackgroundValue(value: string): boolean {
|
||||
return /^url\(/i.test(value.trim());
|
||||
}
|
||||
|
||||
function shouldDetachOppositeEdges(selection: DomEditSelection): boolean {
|
||||
return Boolean(
|
||||
selection.inlineStyles.inset || selection.inlineStyles.right || selection.inlineStyles.bottom,
|
||||
);
|
||||
}
|
||||
|
||||
function buildOppositeEdgePatchOperations(
|
||||
selection: DomEditSelection,
|
||||
dimension: "width" | "height" | "both",
|
||||
): PatchOperation[] {
|
||||
if (!shouldDetachOppositeEdges(selection)) return [];
|
||||
const operations: PatchOperation[] = [];
|
||||
if (dimension === "width" || dimension === "both") {
|
||||
operations.push({ type: "inline-style", property: "right", value: "auto" });
|
||||
}
|
||||
if (dimension === "height" || dimension === "both") {
|
||||
operations.push({ type: "inline-style", property: "bottom", value: "auto" });
|
||||
}
|
||||
return operations;
|
||||
}
|
||||
|
||||
function getEventTargetElement(target: EventTarget | null): HTMLElement | null {
|
||||
if (!target || typeof target !== "object") return null;
|
||||
const maybeNode = target as {
|
||||
@@ -339,12 +315,8 @@ function findMatchingTimelineElementId(
|
||||
return null;
|
||||
}
|
||||
|
||||
function isMoveStyleProperty(property: string): boolean {
|
||||
return property === "left" || property === "top";
|
||||
}
|
||||
|
||||
function isResizeStyleProperty(property: string): boolean {
|
||||
return property === "width" || property === "height";
|
||||
function isManualGeometryStyleProperty(property: string): boolean {
|
||||
return property === "left" || property === "top" || property === "width" || property === "height";
|
||||
}
|
||||
|
||||
function getPreviewTargetFromPointer(
|
||||
@@ -1987,45 +1959,6 @@ export function StudioApp() {
|
||||
[activeCompPath, editHistory.recordEdit, writeProjectFile],
|
||||
);
|
||||
|
||||
const handleDomPathOffsetCommit = useCallback(
|
||||
(selection: DomEditSelection, next: { x: number; y: number }) => {
|
||||
commitStudioManualEditManifestOptimistically(
|
||||
(manifest) => upsertStudioPathOffsetEdit(manifest, selection, next),
|
||||
{
|
||||
label: "Move layer",
|
||||
coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
|
||||
},
|
||||
);
|
||||
},
|
||||
[commitStudioManualEditManifestOptimistically],
|
||||
);
|
||||
|
||||
const handleDomBoxSizeCommit = useCallback(
|
||||
(selection: DomEditSelection, next: { width: number; height: number }) => {
|
||||
commitStudioManualEditManifestOptimistically(
|
||||
(manifest) => upsertStudioBoxSizeEdit(manifest, selection, next),
|
||||
{
|
||||
label: "Resize layer box",
|
||||
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
||||
},
|
||||
);
|
||||
},
|
||||
[commitStudioManualEditManifestOptimistically],
|
||||
);
|
||||
|
||||
const handleDomRotationCommit = useCallback(
|
||||
(selection: DomEditSelection, next: { angle: number }) => {
|
||||
commitStudioManualEditManifestOptimistically(
|
||||
(manifest) => upsertStudioRotationEdit(manifest, selection, next),
|
||||
{
|
||||
label: "Rotate layer",
|
||||
coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
|
||||
},
|
||||
);
|
||||
},
|
||||
[commitStudioManualEditManifestOptimistically],
|
||||
);
|
||||
|
||||
const refreshDomEditSelectionFromPreview = useCallback(
|
||||
(selection: DomEditSelection) => {
|
||||
const iframe = previewIframeRef.current;
|
||||
@@ -2048,6 +1981,48 @@ export function StudioApp() {
|
||||
[applyDomSelection, buildDomSelectionFromTarget],
|
||||
);
|
||||
|
||||
const handleDomPathOffsetCommit = useCallback(
|
||||
(selection: DomEditSelection, next: { x: number; y: number }) => {
|
||||
commitStudioManualEditManifestOptimistically(
|
||||
(manifest) => upsertStudioPathOffsetEdit(manifest, selection, next),
|
||||
{
|
||||
label: "Move layer",
|
||||
coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
|
||||
},
|
||||
);
|
||||
refreshDomEditSelectionFromPreview(selection);
|
||||
},
|
||||
[commitStudioManualEditManifestOptimistically, refreshDomEditSelectionFromPreview],
|
||||
);
|
||||
|
||||
const handleDomBoxSizeCommit = useCallback(
|
||||
(selection: DomEditSelection, next: { width: number; height: number }) => {
|
||||
commitStudioManualEditManifestOptimistically(
|
||||
(manifest) => upsertStudioBoxSizeEdit(manifest, selection, next),
|
||||
{
|
||||
label: "Resize layer box",
|
||||
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
||||
},
|
||||
);
|
||||
refreshDomEditSelectionFromPreview(selection);
|
||||
},
|
||||
[commitStudioManualEditManifestOptimistically, refreshDomEditSelectionFromPreview],
|
||||
);
|
||||
|
||||
const handleDomRotationCommit = useCallback(
|
||||
(selection: DomEditSelection, next: { angle: number }) => {
|
||||
commitStudioManualEditManifestOptimistically(
|
||||
(manifest) => upsertStudioRotationEdit(manifest, selection, next),
|
||||
{
|
||||
label: "Rotate layer",
|
||||
coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
|
||||
},
|
||||
);
|
||||
refreshDomEditSelectionFromPreview(selection);
|
||||
},
|
||||
[commitStudioManualEditManifestOptimistically, refreshDomEditSelectionFromPreview],
|
||||
);
|
||||
|
||||
const handleDomManualEditsReset = useCallback(
|
||||
(selection: DomEditSelection) => {
|
||||
commitStudioManualEditManifestOptimistically(
|
||||
@@ -2070,11 +2045,8 @@ export function StudioApp() {
|
||||
const handleDomStyleCommit = useCallback(
|
||||
async (property: string, value: string) => {
|
||||
if (!domEditSelection) return;
|
||||
const isMoveStyle = isMoveStyleProperty(property);
|
||||
const isResizeStyle = isResizeStyleProperty(property);
|
||||
if (isMoveStyle && !domEditSelection.capabilities.canMove) return;
|
||||
if (isResizeStyle && !domEditSelection.capabilities.canResize) return;
|
||||
if (!isMoveStyle && !isResizeStyle && !domEditSelection.capabilities.canEditStyles) return;
|
||||
if (isManualGeometryStyleProperty(property)) return;
|
||||
if (!domEditSelection.capabilities.canEditStyles) return;
|
||||
const importedFont = property === "font-family" ? resolveImportedFontAsset(value) : null;
|
||||
const iframe = previewIframeRef.current;
|
||||
const doc = iframe?.contentDocument;
|
||||
@@ -2086,10 +2058,6 @@ export function StudioApp() {
|
||||
injectPreviewGoogleFont(doc, value);
|
||||
if (importedFont) injectPreviewImportedFont(doc, importedFont);
|
||||
}
|
||||
if (shouldDetachOppositeEdges(domEditSelection)) {
|
||||
if (property === "width") el.style.right = "auto";
|
||||
if (property === "height") el.style.bottom = "auto";
|
||||
}
|
||||
if (property === "background-image" && isImageBackgroundValue(value)) {
|
||||
el.style.setProperty("background-position", "center");
|
||||
el.style.setProperty("background-repeat", "no-repeat");
|
||||
@@ -2100,11 +2068,7 @@ export function StudioApp() {
|
||||
const operations: PatchOperation[] = [
|
||||
buildDomEditStylePatchOperation(property, normalizeDomEditStyleValue(property, value)),
|
||||
];
|
||||
if (property === "width") {
|
||||
operations.push(...buildOppositeEdgePatchOperations(domEditSelection, "width"));
|
||||
} else if (property === "height") {
|
||||
operations.push(...buildOppositeEdgePatchOperations(domEditSelection, "height"));
|
||||
} else if (property === "background-image" && isImageBackgroundValue(value)) {
|
||||
if (property === "background-image" && isImageBackgroundValue(value)) {
|
||||
operations.push(
|
||||
buildDomEditStylePatchOperation("background-position", "center"),
|
||||
buildDomEditStylePatchOperation("background-repeat", "no-repeat"),
|
||||
@@ -3278,6 +3242,8 @@ export function StudioApp() {
|
||||
copiedAgentPrompt={copiedAgentPrompt}
|
||||
onClearSelection={clearDomSelection}
|
||||
onSetStyle={handleDomStyleCommit}
|
||||
onSetManualOffset={handleDomPathOffsetCommit}
|
||||
onSetManualSize={handleDomBoxSizeCommit}
|
||||
onSetText={handleDomTextCommit}
|
||||
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
|
||||
onAddTextField={handleDomAddTextField}
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type GradientModel,
|
||||
} from "./gradientValue";
|
||||
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
||||
import { readStudioBoxSize, readStudioPathOffset } from "./manualEdits";
|
||||
import {
|
||||
COMMON_LOCAL_FONT_FAMILIES,
|
||||
googleFontStylesheetUrl,
|
||||
@@ -54,6 +55,8 @@ interface PropertyPanelProps {
|
||||
copiedAgentPrompt: boolean;
|
||||
onClearSelection: () => void;
|
||||
onSetStyle: (prop: string, value: string) => void;
|
||||
onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
|
||||
onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
|
||||
onSetText: (value: string, fieldKey?: string) => void;
|
||||
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
||||
onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
|
||||
@@ -171,6 +174,17 @@ function parseNumericToken(value: string | undefined): ParsedNumericToken | null
|
||||
};
|
||||
}
|
||||
|
||||
function parsePxMetricValue(value: string): number | null {
|
||||
const token = parseNumericToken(value);
|
||||
if (!token) return null;
|
||||
if (token.unit && token.unit.toLowerCase() !== "px") return null;
|
||||
return token.value;
|
||||
}
|
||||
|
||||
function formatPxMetricValue(value: number): string {
|
||||
return `${formatNumericValue(value)}px`;
|
||||
}
|
||||
|
||||
function adjustNumericToken(
|
||||
value: string,
|
||||
direction: 1 | -1,
|
||||
@@ -1962,6 +1976,8 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
copiedAgentPrompt,
|
||||
onClearSelection,
|
||||
onSetStyle,
|
||||
onSetManualOffset,
|
||||
onSetManualSize,
|
||||
onSetText,
|
||||
onSetTextFieldStyle,
|
||||
onAddTextField,
|
||||
@@ -2006,22 +2022,60 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
<Eye size={18} className="mb-3 text-neutral-600" />
|
||||
<p className="text-sm font-medium text-neutral-200">Select an element in the preview.</p>
|
||||
<p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
|
||||
The inspector is tuned for direct DOM edits with safer geometry controls, color picking,
|
||||
and cleaner grouped layer controls.
|
||||
The inspector is tuned for element edits with safer geometry controls, color picking, and
|
||||
cleaner grouped layer controls.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styleEditingDisabled = !element.capabilities.canEditStyles;
|
||||
const moveEditingDisabled = !element.capabilities.canMove;
|
||||
const resizeEditingDisabled = !element.capabilities.canResize;
|
||||
const manualOffsetEditingDisabled = !element.capabilities.canApplyManualOffset;
|
||||
const manualSizeEditingDisabled = !element.capabilities.canApplyManualSize;
|
||||
const isFlex = styles.display === "flex" || styles.display === "inline-flex";
|
||||
const radiusValue = parseNumericValue(styles["border-radius"]) ?? 0;
|
||||
const opacityValue = Math.round((parseNumericValue(styles.opacity) ?? 1) * 100);
|
||||
const clipContent = ["hidden", "clip"].includes((styles.overflow ?? "").trim());
|
||||
const sourceLabel = element.id ? `#${element.id}` : element.selector;
|
||||
const showEditableSections = element.capabilities.canEditStyles;
|
||||
const manualOffset = readStudioPathOffset(element.element);
|
||||
const manualSize = readStudioBoxSize(element.element);
|
||||
const resolvedWidth =
|
||||
manualSize.width > 0
|
||||
? manualSize.width
|
||||
: (parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width);
|
||||
const resolvedHeight =
|
||||
manualSize.height > 0
|
||||
? manualSize.height
|
||||
: (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height);
|
||||
|
||||
const commitManualOffset = (axis: "x" | "y", nextValue: string) => {
|
||||
const parsed = parsePxMetricValue(nextValue);
|
||||
if (parsed == null) return;
|
||||
const current = readStudioPathOffset(element.element);
|
||||
onSetManualOffset(element, {
|
||||
x: axis === "x" ? parsed : current.x,
|
||||
y: axis === "y" ? parsed : current.y,
|
||||
});
|
||||
};
|
||||
|
||||
const commitManualSize = (axis: "width" | "height", nextValue: string) => {
|
||||
const parsed = parsePxMetricValue(nextValue);
|
||||
if (parsed == null || parsed <= 0) return;
|
||||
const current = readStudioBoxSize(element.element);
|
||||
const width =
|
||||
current.width > 0
|
||||
? current.width
|
||||
: (parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width);
|
||||
const height =
|
||||
current.height > 0
|
||||
? current.height
|
||||
: (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height);
|
||||
onSetManualSize(element, {
|
||||
width: axis === "width" ? parsed : width,
|
||||
height: axis === "height" ? parsed : height,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFillModeChange = (nextMode: string) => {
|
||||
setPreferredFillMode(nextMode);
|
||||
@@ -2083,27 +2137,27 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
<div className={RESPONSIVE_GRID}>
|
||||
<MetricField
|
||||
label="X"
|
||||
value={styles.left ?? "auto"}
|
||||
disabled={moveEditingDisabled}
|
||||
onCommit={(next) => onSetStyle("left", next)}
|
||||
value={formatPxMetricValue(manualOffset.x)}
|
||||
disabled={manualOffsetEditingDisabled}
|
||||
onCommit={(next) => commitManualOffset("x", next)}
|
||||
/>
|
||||
<MetricField
|
||||
label="Y"
|
||||
value={styles.top ?? "auto"}
|
||||
disabled={moveEditingDisabled}
|
||||
onCommit={(next) => onSetStyle("top", next)}
|
||||
value={formatPxMetricValue(manualOffset.y)}
|
||||
disabled={manualOffsetEditingDisabled}
|
||||
onCommit={(next) => commitManualOffset("y", next)}
|
||||
/>
|
||||
<MetricField
|
||||
label="W"
|
||||
value={styles.width ?? "auto"}
|
||||
disabled={resizeEditingDisabled}
|
||||
onCommit={(next) => onSetStyle("width", next)}
|
||||
value={formatPxMetricValue(resolvedWidth)}
|
||||
disabled={manualSizeEditingDisabled}
|
||||
onCommit={(next) => commitManualSize("width", next)}
|
||||
/>
|
||||
<MetricField
|
||||
label="H"
|
||||
value={styles.height ?? "auto"}
|
||||
disabled={resizeEditingDisabled}
|
||||
onCommit={(next) => onSetStyle("height", next)}
|
||||
value={formatPxMetricValue(resolvedHeight)}
|
||||
disabled={manualSizeEditingDisabled}
|
||||
onCommit={(next) => commitManualSize("height", next)}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
Reference in New Issue
Block a user