mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 21:17:30 +00:00
fix(studio): stale selection, rotation control, design panel polish
Fixes and improvements based on power-user testing feedback: 1. Fix stale selection after style edits — handleDomStyleCommit now calls refreshDomEditSelectionFromPreview after persisting, matching every other commit handler. Without this, the PropertyPanel showed frozen computedStyles after color/radius/shadow edits, making it look like editing "didn't work." Also adds error handling around the persist call. 2. Add rotation field to the Design panel Layout section — reads the current rotation angle from the manual edit manifest and commits via the existing handleDomRotationCommit handler. 3. Enable motion panel by default — STUDIO_MOTION_PANEL_ENABLED now defaults to true so the Motion tab is discoverable without env vars. 4. Color controls only when element has color — fill color section now only shows when the element has an explicit non-transparent background-color. Text color shows only when the element has a color style. Prevents showing color pickers on elements where color edits have no visible effect. 5. Exclude canvas from selection — added "canvas" to DOM_LAYER_IGNORED_TAGS so canvas elements are not selectable in the preview or listed in the layer panel. 6. Multi-selection feedback — shows "N elements selected" with guidance instead of the generic empty state when multiple elements are selected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
25f60bfb9d
commit
52674dfd86
@@ -2933,15 +2933,26 @@ export function StudioApp() {
|
||||
buildDomEditStylePatchOperation("background-size", "contain"),
|
||||
);
|
||||
}
|
||||
await persistDomEditOperations(domEditSelection, operations, {
|
||||
label: "Edit layer style",
|
||||
skipRefresh: true,
|
||||
prepareContent: importedFont
|
||||
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
||||
: undefined,
|
||||
});
|
||||
try {
|
||||
await persistDomEditOperations(domEditSelection, operations, {
|
||||
label: "Edit layer style",
|
||||
skipRefresh: true,
|
||||
prepareContent: importedFont
|
||||
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
||||
: undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("[Studio] Style persist failed:", err instanceof Error ? err.message : err);
|
||||
}
|
||||
refreshDomEditSelectionFromPreview(domEditSelection);
|
||||
},
|
||||
[activeCompPath, domEditSelection, persistDomEditOperations, resolveImportedFontAsset],
|
||||
[
|
||||
activeCompPath,
|
||||
domEditSelection,
|
||||
persistDomEditOperations,
|
||||
refreshDomEditSelectionFromPreview,
|
||||
resolveImportedFontAsset,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDomTextCommit = useCallback(
|
||||
@@ -4197,11 +4208,13 @@ export function StudioApp() {
|
||||
{designPanelActive ? (
|
||||
<PropertyPanel
|
||||
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
||||
multiSelectCount={domEditGroupSelections.length}
|
||||
copiedAgentPrompt={copiedAgentPrompt}
|
||||
onClearSelection={clearDomSelection}
|
||||
onSetStyle={handleDomStyleCommit}
|
||||
onSetManualOffset={handleDomPathOffsetCommit}
|
||||
onSetManualSize={handleDomBoxSizeCommit}
|
||||
onSetRotation={handleDomRotationCommit}
|
||||
onSetText={handleDomTextCommit}
|
||||
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
|
||||
onResetManualEdits={handleDomManualEditsReset}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
Eye,
|
||||
Layers,
|
||||
MessageSquare,
|
||||
Move,
|
||||
Palette,
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
type ParsedColor,
|
||||
} from "./colorValue";
|
||||
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
||||
import { readStudioBoxSize, readStudioPathOffset } from "./manualEdits";
|
||||
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
|
||||
import {
|
||||
COMMON_LOCAL_FONT_FAMILIES,
|
||||
googleFontStylesheetUrl,
|
||||
@@ -39,11 +40,13 @@ import { resolveFloatingPanelPosition, type FloatingPosition } from "./floatingP
|
||||
|
||||
interface PropertyPanelProps {
|
||||
element: DomEditSelection | null;
|
||||
multiSelectCount?: number;
|
||||
copiedAgentPrompt: boolean;
|
||||
onClearSelection: () => void;
|
||||
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
||||
onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
|
||||
onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
|
||||
onSetRotation: (element: DomEditSelection, next: { angle: number }) => void;
|
||||
onSetText: (value: string, fieldKey?: string) => void;
|
||||
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
||||
onResetManualEdits: (element: DomEditSelection) => void;
|
||||
@@ -1568,11 +1571,13 @@ export function isPropertyPanelMediaLikeSelection(input: {
|
||||
|
||||
export const PropertyPanel = memo(function PropertyPanel({
|
||||
element,
|
||||
multiSelectCount = 0,
|
||||
copiedAgentPrompt,
|
||||
onClearSelection,
|
||||
onSetStyle,
|
||||
onSetManualOffset,
|
||||
onSetManualSize,
|
||||
onSetRotation,
|
||||
onSetText,
|
||||
onSetTextFieldStyle,
|
||||
onResetManualEdits,
|
||||
@@ -1597,12 +1602,28 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
if (!element) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center bg-neutral-900 px-6 text-center">
|
||||
<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 element edits with safer geometry controls, color picking, and
|
||||
cleaner grouped layer controls.
|
||||
</p>
|
||||
{multiSelectCount > 1 ? (
|
||||
<>
|
||||
<Layers size={18} className="mb-3 text-neutral-600" />
|
||||
<p className="text-sm font-medium text-neutral-200">
|
||||
{multiSelectCount} elements selected
|
||||
</p>
|
||||
<p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
|
||||
Select a single element to edit its properties. Click an element in the preview or use
|
||||
the timeline layer panel.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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">
|
||||
Click any element to inspect and edit it. Click deeper to select child elements.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1618,8 +1639,14 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
tagName: element.tagName,
|
||||
styles,
|
||||
});
|
||||
const canShowFillColor = !mediaLikeSelection;
|
||||
const canShowTextColor = !hasTextControls && !mediaLikeSelection;
|
||||
const bgColor = styles["background-color"]?.trim();
|
||||
const hasExplicitFillColor =
|
||||
!mediaLikeSelection &&
|
||||
Boolean(bgColor) &&
|
||||
bgColor !== "rgba(0, 0, 0, 0)" &&
|
||||
bgColor !== "transparent";
|
||||
const canShowFillColor = hasExplicitFillColor;
|
||||
const canShowTextColor = !mediaLikeSelection && Boolean(styles.color?.trim());
|
||||
const visibleSections = getPropertyPanelVisibleSections({
|
||||
hasSelection: true,
|
||||
canEditStyles: showEditableSections,
|
||||
@@ -1628,6 +1655,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
});
|
||||
const manualOffset = readStudioPathOffset(element.element);
|
||||
const manualSize = readStudioBoxSize(element.element);
|
||||
const manualRotation = readStudioRotation(element.element);
|
||||
const resolvedWidth =
|
||||
manualSize.width > 0
|
||||
? manualSize.width
|
||||
@@ -1837,6 +1865,17 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
disabled={manualSizeEditingDisabled}
|
||||
onCommit={(next) => commitManualSize("height", next)}
|
||||
/>
|
||||
<MetricField
|
||||
label="Rotate"
|
||||
value={`${formatNumericValue(manualRotation.angle)}°`}
|
||||
disabled={manualOffsetEditingDisabled}
|
||||
onCommit={(next) => {
|
||||
const parsed = parseFloat(next.replace("°", "").trim());
|
||||
if (Number.isFinite(parsed)) {
|
||||
onSetRotation(element, { angle: parsed % 360 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
@@ -398,6 +398,7 @@ function buildElementLabel(el: HTMLElement): string {
|
||||
const DOM_LAYER_IGNORED_TAGS = new Set([
|
||||
"base",
|
||||
"br",
|
||||
"canvas",
|
||||
"link",
|
||||
"meta",
|
||||
"script",
|
||||
|
||||
@@ -16,13 +16,13 @@ describe("manual editing availability", () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("enables inspector selection by default while motion and manual dragging stay opt-in", async () => {
|
||||
it("enables inspector selection, motion panel, and manual editing by default", 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_MOTION_PANEL_ENABLED).toBe(true);
|
||||
expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag(
|
||||
export const STUDIO_MOTION_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
||||
env,
|
||||
[STUDIO_MOTION_PANEL_ENV, "VITE_STUDIO_MOTION_PANEL_ENABLED"],
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
export const STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED =
|
||||
|
||||
Reference in New Issue
Block a user