mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): fix PropertyPanel format and file size regression (#1229)
The fit-to-children merge re-inlined TimingSection that was already extracted to propertyPanelTimingSection.ts, pushing the file to 687 lines (over the 600 limit) and introducing format issues. - Removed duplicate TimingSection, import from extracted module - Extracted computeFitToChildrenSize to propertyPanelHelpers - Formatted PropertyPanel.tsx (608 lines, down from 687)
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import { memo } from "react";
|
||||
import { Clock, Eye, Layers, MessageSquare, Move, X } from "../../icons/SystemIcons";
|
||||
import { type DomEditSelection } from "./domEditing";
|
||||
import { Eye, Layers, MessageSquare, Move, X } from "../../icons/SystemIcons";
|
||||
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
|
||||
import type { ImportedFontAsset } from "./fontAssets";
|
||||
import {
|
||||
EMPTY_STYLES,
|
||||
formatPxMetricValue,
|
||||
@@ -17,6 +15,8 @@ import { GsapAnimationSection } from "./GsapAnimationSection";
|
||||
import { KeyframeNavigation } from "./KeyframeNavigation";
|
||||
import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
|
||||
import { usePlayerStore } from "../../player";
|
||||
import { TimingSection } from "./propertyPanelTimingSection";
|
||||
import { computeFitToChildrenSize, type PropertyPanelProps } from "./propertyPanelHelpers";
|
||||
|
||||
// Re-export helpers that external consumers import from this module
|
||||
export {
|
||||
@@ -30,124 +30,6 @@ export {
|
||||
setCssFilterFunctionPx,
|
||||
} from "./propertyPanelHelpers";
|
||||
|
||||
interface PropertyPanelProps {
|
||||
projectId: string;
|
||||
projectDir: string | null;
|
||||
assets: string[];
|
||||
element: DomEditSelection | null;
|
||||
multiSelectCount?: number;
|
||||
copiedAgentPrompt: boolean;
|
||||
onClearSelection: () => void;
|
||||
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
||||
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
||||
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
||||
onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
|
||||
onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
|
||||
onSetManualRotation: (element: DomEditSelection, next: { angle: number }) => void;
|
||||
onSetText: (value: string, fieldKey?: string) => void;
|
||||
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
||||
onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
|
||||
onRemoveTextField: (fieldKey: string) => void;
|
||||
onAskAgent: () => void;
|
||||
onImportAssets?: (files: FileList) => Promise<string[]>;
|
||||
fontAssets?: ImportedFontAsset[];
|
||||
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
||||
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
||||
gsapAnimations?: import("@hyperframes/core/gsap-parser").GsapAnimation[];
|
||||
gsapMultipleTimelines?: boolean;
|
||||
gsapUnsupportedTimelinePattern?: boolean;
|
||||
onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
|
||||
onUpdateGsapMeta?: (
|
||||
animId: string,
|
||||
updates: { duration?: number; ease?: string; position?: number },
|
||||
) => void;
|
||||
onDeleteGsapAnimation?: (animId: string) => void;
|
||||
onAddGsapProperty?: (animId: string, prop: string) => void;
|
||||
onRemoveGsapProperty?: (animId: string, prop: string) => void;
|
||||
onUpdateGsapFromProperty?: (animId: string, prop: string, value: number | string) => void;
|
||||
onAddGsapFromProperty?: (animId: string, prop: string) => void;
|
||||
onRemoveGsapFromProperty?: (animId: string, prop: string) => void;
|
||||
onAddGsapAnimation?: (method: "to" | "from" | "set" | "fromTo") => void;
|
||||
onAddKeyframe?: (
|
||||
animationId: string,
|
||||
percentage: number,
|
||||
property: string,
|
||||
value: number | string,
|
||||
) => void;
|
||||
onRemoveKeyframe?: (animationId: string, percentage: number) => void;
|
||||
onConvertToKeyframes?: (animationId: string) => void;
|
||||
onCommitAnimatedProperty?: (
|
||||
selection: DomEditSelection,
|
||||
property: string,
|
||||
value: number | string,
|
||||
) => Promise<void>;
|
||||
onSeekToTime?: (time: number) => void;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* TimingSection */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function formatTimingValue(seconds: number): string {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) return "0.00s";
|
||||
return `${seconds.toFixed(2)}s`;
|
||||
}
|
||||
|
||||
function parseTimingValue(input: string): number | null {
|
||||
const cleaned = input.replace(/s$/i, "").trim();
|
||||
const parsed = Number.parseFloat(cleaned);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function TimingSection({
|
||||
element,
|
||||
onSetAttribute,
|
||||
}: {
|
||||
element: DomEditSelection;
|
||||
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
||||
}) {
|
||||
const start = Number.parseFloat(element.dataAttributes.start ?? "0") || 0;
|
||||
const duration =
|
||||
Number.parseFloat(
|
||||
element.dataAttributes.duration ?? element.dataAttributes["hf-authored-duration"] ?? "0",
|
||||
) || 0;
|
||||
const end = start + duration;
|
||||
|
||||
const commitStart = (nextValue: string) => {
|
||||
const parsed = parseTimingValue(nextValue);
|
||||
if (parsed == null) return;
|
||||
void onSetAttribute("start", parsed.toFixed(2));
|
||||
};
|
||||
|
||||
const commitDuration = (nextValue: string) => {
|
||||
const parsed = parseTimingValue(nextValue);
|
||||
if (parsed == null || parsed <= 0) return;
|
||||
void onSetAttribute("duration", parsed.toFixed(2));
|
||||
};
|
||||
|
||||
const commitEnd = (nextValue: string) => {
|
||||
const parsed = parseTimingValue(nextValue);
|
||||
if (parsed == null || parsed <= start) return;
|
||||
void onSetAttribute("duration", (parsed - start).toFixed(2));
|
||||
};
|
||||
|
||||
return (
|
||||
<Section title="Timing" icon={<Clock size={15} />}>
|
||||
<div className={RESPONSIVE_GRID}>
|
||||
<MetricField label="Start" value={formatTimingValue(start)} onCommit={commitStart} />
|
||||
<MetricField label="End" value={formatTimingValue(end)} onCommit={commitEnd} />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<MetricField
|
||||
label="Duration"
|
||||
value={formatTimingValue(duration)}
|
||||
onCommit={commitDuration}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* PropertyPanel */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -508,35 +390,18 @@ export const PropertyPanel = memo(function PropertyPanel({
|
||||
className="flex-shrink-0 rounded p-1 text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
|
||||
title="Fit to children"
|
||||
onClick={() => {
|
||||
const el = element.element;
|
||||
const win = el.ownerDocument?.defaultView;
|
||||
const children = Array.from(el.children).filter(
|
||||
(c): c is HTMLElement => c.nodeType === 1,
|
||||
);
|
||||
if (children.length === 0) return;
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
for (const child of children) {
|
||||
if (win) {
|
||||
const cs = win.getComputedStyle(child);
|
||||
if (cs.visibility === "hidden" || cs.display === "none") continue;
|
||||
}
|
||||
const r = child.getBoundingClientRect();
|
||||
if (r.width === 0 && r.height === 0) continue;
|
||||
minX = Math.min(minX, r.left);
|
||||
minY = Math.min(minY, r.top);
|
||||
maxX = Math.max(maxX, r.right);
|
||||
maxY = Math.max(maxY, r.bottom);
|
||||
}
|
||||
if (!isFinite(minX)) return;
|
||||
const parentRect = el.getBoundingClientRect();
|
||||
const scaleX = parentRect.width > 0 ? element.boundingBox.width / parentRect.width : 1;
|
||||
const scaleY = parentRect.height > 0 ? element.boundingBox.height / parentRect.height : 1;
|
||||
const width = Math.round((maxX - minX) * scaleX);
|
||||
const height = Math.round((maxY - minY) * scaleY);
|
||||
if (width > 0 && height > 0) onSetManualSize(element, { width, height });
|
||||
const size = computeFitToChildrenSize(element);
|
||||
if (size) onSetManualSize(element, size);
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.2">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
>
|
||||
<rect x="2" y="2" width="10" height="10" strokeDasharray="2 1.5" rx="1" />
|
||||
<path d="M2 4.5h1m-1 5h1m8-5h1m-1 5h1M4.5 2v1m5-1v1M4.5 11v1m5-1v1" />
|
||||
</svg>
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
import { parseCssColor, type ParsedColor } from "./colorValue";
|
||||
import { COMMON_LOCAL_FONT_FAMILIES } from "./fontCatalog";
|
||||
import type { DomEditSelection } from "./domEditing";
|
||||
import type { ImportedFontAsset } from "./fontAssets";
|
||||
|
||||
export interface PropertyPanelProps {
|
||||
projectId: string;
|
||||
projectDir: string | null;
|
||||
assets: string[];
|
||||
element: DomEditSelection | null;
|
||||
multiSelectCount?: number;
|
||||
copiedAgentPrompt: boolean;
|
||||
onClearSelection: () => void;
|
||||
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
||||
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
||||
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
||||
onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
|
||||
onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
|
||||
onSetManualRotation: (element: DomEditSelection, next: { angle: number }) => void;
|
||||
onSetText: (value: string, fieldKey?: string) => void;
|
||||
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
||||
onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
|
||||
onRemoveTextField: (fieldKey: string) => void;
|
||||
onAskAgent: () => void;
|
||||
onImportAssets?: (files: FileList) => Promise<string[]>;
|
||||
fontAssets?: ImportedFontAsset[];
|
||||
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
||||
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
||||
gsapAnimations?: import("@hyperframes/core/gsap-parser").GsapAnimation[];
|
||||
gsapMultipleTimelines?: boolean;
|
||||
gsapUnsupportedTimelinePattern?: boolean;
|
||||
onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
|
||||
onUpdateGsapMeta?: (
|
||||
animId: string,
|
||||
updates: { duration?: number; ease?: string; position?: number },
|
||||
) => void;
|
||||
onDeleteGsapAnimation?: (animId: string) => void;
|
||||
onAddGsapProperty?: (animId: string, prop: string) => void;
|
||||
onRemoveGsapProperty?: (animId: string, prop: string) => void;
|
||||
onUpdateGsapFromProperty?: (animId: string, prop: string, value: number | string) => void;
|
||||
onAddGsapFromProperty?: (animId: string, prop: string) => void;
|
||||
onRemoveGsapFromProperty?: (animId: string, prop: string) => void;
|
||||
onAddGsapAnimation?: (method: "to" | "from" | "set" | "fromTo") => void;
|
||||
onAddKeyframe?: (
|
||||
animationId: string,
|
||||
percentage: number,
|
||||
property: string,
|
||||
value: number | string,
|
||||
) => void;
|
||||
onRemoveKeyframe?: (animationId: string, percentage: number) => void;
|
||||
onConvertToKeyframes?: (animationId: string) => void;
|
||||
onCommitAnimatedProperty?: (
|
||||
selection: DomEditSelection,
|
||||
property: string,
|
||||
value: number | string,
|
||||
) => Promise<void>;
|
||||
onSeekToTime?: (time: number) => void;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Font types & constants (shared by font and section modules) */
|
||||
@@ -399,3 +455,37 @@ export function extractBackgroundImageUrl(value: string | undefined): string {
|
||||
if (endParen < index) return "";
|
||||
return value.slice(index, endParen).trim();
|
||||
}
|
||||
|
||||
// ── Fit to children ──────────────────────────────────────────────────
|
||||
|
||||
export function computeFitToChildrenSize(
|
||||
element: DomEditSelection,
|
||||
): { width: number; height: number } | null {
|
||||
const el = element.element;
|
||||
const win = el.ownerDocument?.defaultView;
|
||||
const children = Array.from(el.children).filter((c): c is HTMLElement => c.nodeType === 1);
|
||||
if (children.length === 0) return null;
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
for (const child of children) {
|
||||
if (win) {
|
||||
const cs = win.getComputedStyle(child);
|
||||
if (cs.visibility === "hidden" || cs.display === "none") continue;
|
||||
}
|
||||
const r = child.getBoundingClientRect();
|
||||
if (r.width === 0 && r.height === 0) continue;
|
||||
minX = Math.min(minX, r.left);
|
||||
minY = Math.min(minY, r.top);
|
||||
maxX = Math.max(maxX, r.right);
|
||||
maxY = Math.max(maxY, r.bottom);
|
||||
}
|
||||
if (!isFinite(minX)) return null;
|
||||
const parentRect = el.getBoundingClientRect();
|
||||
const scaleX = parentRect.width > 0 ? element.boundingBox.width / parentRect.width : 1;
|
||||
const scaleY = parentRect.height > 0 ? element.boundingBox.height / parentRect.height : 1;
|
||||
const width = Math.round((maxX - minX) * scaleX);
|
||||
const height = Math.round((maxY - minY) * scaleY);
|
||||
return width > 0 && height > 0 ? { width, height } : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user