import { memo } from "react"; import { Clock, Eye, Layers, MessageSquare, Move, X } from "../../icons/SystemIcons"; import { type DomEditSelection } from "./domEditing"; import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits"; import type { ImportedFontAsset } from "./fontAssets"; import { EMPTY_STYLES, formatPxMetricValue, LABEL, parsePxMetricValue, RESPONSIVE_GRID, } from "./propertyPanelHelpers"; import { MetricField, Section } from "./propertyPanelPrimitives"; import { isMediaElement, MediaSection } from "./propertyPanelMediaSection"; import { TextSection, StyleSections } from "./propertyPanelSections"; // Re-export helpers that external consumers import from this module export { buildStrokeStyleUpdates, buildStrokeWidthStyleUpdates, clampPanelNumber, getCssFilterFunctionPx, getClipPathInsetPx, inferBoxShadowPreset, inferClipPathPreset, normalizePanelPxValue, 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; onSetAttribute: (attr: string, value: string) => void | Promise; onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise; 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 | null; onRemoveTextField: (fieldKey: string) => void; onAskAgent: () => void; onImportAssets?: (files: FileList) => Promise; fontAssets?: ImportedFontAsset[]; onImportFonts?: (files: FileList | File[]) => Promise; } /* ------------------------------------------------------------------ */ /* 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; }) { const start = Number.parseFloat(element.dataAttributes.start ?? "0") || 0; const duration = Number.parseFloat(element.dataAttributes.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 (
}>
); } /* ------------------------------------------------------------------ */ /* PropertyPanel */ /* ------------------------------------------------------------------ */ export const PropertyPanel = memo(function PropertyPanel({ projectId, projectDir, assets, element, multiSelectCount = 0, copiedAgentPrompt, onClearSelection, onSetStyle, onSetAttribute, onSetHtmlAttribute, onSetManualOffset, onSetManualSize, onSetManualRotation, onSetText, onSetTextFieldStyle, onAddTextField, onRemoveTextField, onAskAgent, onImportAssets, fontAssets = [], onImportFonts, }: PropertyPanelProps) { const styles = element?.computedStyles ?? EMPTY_STYLES; if (!element) { return (
{multiSelectCount > 1 ? ( <>

{multiSelectCount} elements selected

Select a single element to edit its properties. Click an element in the preview or use the timeline layer panel.

) : ( <>

Select an element in the preview.

The inspector is tuned for element edits with safer geometry controls, color picking, and cleaner grouped layer controls.

)}
); } const manualOffsetEditingDisabled = !element.capabilities.canApplyManualOffset; const manualSizeEditingDisabled = !element.capabilities.canApplyManualSize; 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 manualRotation = readStudioRotation(element.element); const commitManualRotation = (nextValue: string) => { const parsed = Number.parseFloat(nextValue); if (!Number.isFinite(parsed)) return; onSetManualRotation(element, { angle: parsed }); }; return (
Document
{element.label}
{sourceLabel}
{element.dataAttributes.start != null && ( )} {isMediaElement(element) && ( )}
}>
commitManualOffset("x", next)} /> commitManualOffset("y", next)} /> commitManualSize("width", next)} /> commitManualSize("height", next)} /> commitManualRotation(next.replace("°", ""))} />
onSetStyle("z-index", next)} />
{showEditableSections && ( )}
); });