Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/studio/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ export function StudioApp() {
shouldShowSelectedDomBounds,
} = useInspectorState(
panelLayout.rightPanelTab,
panelLayout.rightInspectorPanes,
panelLayout.rightCollapsed,
isPlaying,
gestureState === "recording",
Expand Down
236 changes: 161 additions & 75 deletions packages/studio/src/components/StudioRightPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCallback, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
import { Tooltip } from "./ui";
import { PropertyPanel } from "./editor/PropertyPanel";
import { LayersPanel } from "./editor/LayersPanel";
Expand All @@ -14,6 +15,9 @@ import { useFileManagerContext } from "../contexts/FileManagerContext";
import { useDomEditContext } from "../contexts/DomEditContext";
import { usePlayerStore } from "../player";

const MIN_INSPECTOR_SPLIT_PERCENT = 20;
const MAX_INSPECTOR_SPLIT_PERCENT = 75;

export interface StudioRightPanelProps {
designPanelActive: boolean;
activeBlockParams?: {
Expand Down Expand Up @@ -41,6 +45,8 @@ export function StudioRightPanel({
rightWidth,
rightPanelTab,
setRightPanelTab,
rightInspectorPanes,
toggleRightInspectorPane,
handlePanelResizeStart,
handlePanelResizeMove,
handlePanelResizeEnd,
Expand All @@ -63,6 +69,7 @@ export function StudioRightPanel({
clearDomSelection,
handleDomStyleCommit,
handleDomAttributeCommit,
handleDomAttributeLiveCommit,
handleDomHtmlAttributeCommit,
handleDomPathOffsetCommit,
handleDomBoxSizeCommit,
Expand Down Expand Up @@ -96,7 +103,130 @@ export function StudioRightPanel({
const { assets, fontAssets, projectDir, handleImportFiles, handleImportFonts } =
useFileManagerContext();

const [layersPanePercent, setLayersPanePercent] = useState(40);
const splitContainerRef = useRef<HTMLDivElement>(null);
const splitDragRef = useRef<{
startY: number;
startPercent: number;
height: number;
} | null>(null);

const renderJobs = renderQueue.jobs as RenderJob[];
const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
const layersPaneOpen =
inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;

const handleInspectorPaneButtonClick = (pane: "design" | "layers") => {
if (!inspectorTabActive) {
setRightPanelTab(pane);
return;
}
toggleRightInspectorPane(pane);
};

const handleInspectorSplitResizeStart = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
const height = splitContainerRef.current?.getBoundingClientRect().height ?? 0;
splitDragRef.current = {
startY: event.clientY,
startPercent: layersPanePercent,
height,
};
},
[layersPanePercent],
);

const handleInspectorSplitResizeMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
const drag = splitDragRef.current;
if (!drag || drag.height <= 0) return;
const deltaPercent = ((event.clientY - drag.startY) / drag.height) * 100;
const next = Math.min(
MAX_INSPECTOR_SPLIT_PERCENT,
Math.max(MIN_INSPECTOR_SPLIT_PERCENT, drag.startPercent + deltaPercent),
);
setLayersPanePercent(next);
}, []);

const handleInspectorSplitResizeEnd = useCallback(() => {
splitDragRef.current = null;
}, []);

const propertyPanel = (
<PropertyPanel
projectId={projectId}
projectDir={projectDir}
assets={assets}
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
multiSelectCount={domEditGroupSelections.length}
copiedAgentPrompt={copiedAgentPrompt}
onClearSelection={clearDomSelection}
onSetStyle={handleDomStyleCommit}
onSetAttribute={handleDomAttributeCommit}
onSetAttributeLive={handleDomAttributeLiveCommit}
onSetHtmlAttribute={handleDomHtmlAttributeCommit}
onSetManualOffset={handleDomPathOffsetCommit}
onSetManualSize={handleDomBoxSizeCommit}
onSetManualRotation={handleDomRotationCommit}
onSetText={handleDomTextCommit}
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
onAddTextField={handleDomAddTextField}
onRemoveTextField={handleDomRemoveTextField}
onAskAgent={handleAskAgent}
onImportAssets={handleImportFiles}
fontAssets={fontAssets}
onImportFonts={handleImportFonts}
previewIframeRef={previewIframeRef}
gsapAnimations={selectedGsapAnimations}
gsapMultipleTimelines={gsapMultipleTimelines}
gsapUnsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
onUpdateGsapProperty={handleGsapUpdateProperty}
onUpdateGsapMeta={handleGsapUpdateMeta}
onDeleteGsapAnimation={handleGsapDeleteAnimation}
onAddGsapProperty={handleGsapAddProperty}
onRemoveGsapProperty={handleGsapRemoveProperty}
onUpdateGsapFromProperty={handleGsapUpdateFromProperty}
onAddGsapFromProperty={handleGsapAddFromProperty}
onRemoveGsapFromProperty={handleGsapRemoveFromProperty}
onAddGsapAnimation={handleGsapAddAnimation}
onCommitAnimatedProperty={commitAnimatedProperty}
onAddKeyframe={handleGsapAddKeyframe}
onRemoveKeyframe={handleGsapRemoveKeyframe}
onConvertToKeyframes={handleGsapConvertToKeyframes}
onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)}
onSetArcPath={handleSetArcPath}
onUpdateArcSegment={handleUpdateArcSegment}
onUnroll={handleUnroll}
recordingState={recordingState}
recordingDuration={recordingDuration}
onToggleRecording={onToggleRecording}
/>
);

const renderQueuePanel = (
<RenderQueue
jobs={renderJobs}
projectId={projectId}
onDelete={renderQueue.deleteRender}
onClearCompleted={renderQueue.clearCompleted}
onStartRender={async (format, quality, resolution, fps) => {
await waitForPendingDomEditSaves();
const composition =
activeCompPath && activeCompPath !== "index.html" ? activeCompPath : undefined;
await renderQueue.startRender({
fps,
quality,
format,
resolution,
composition,
});
}}
compositionDimensions={compositionDimensions}
isRendering={renderQueue.isRendering}
/>
);

return (
<>
Expand All @@ -123,9 +253,9 @@ export function StudioRightPanel({
<Tooltip label="Element styles and properties" side="bottom">
<button
type="button"
onClick={() => setRightPanelTab("design")}
onClick={() => handleInspectorPaneButtonClick("design")}
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
rightPanelTab === "design"
designPaneOpen
? "bg-neutral-800 text-white"
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
}`}
Expand All @@ -136,9 +266,9 @@ export function StudioRightPanel({
<Tooltip label="Composition layer stack" side="bottom">
<button
type="button"
onClick={() => setRightPanelTab("layers")}
onClick={() => handleInspectorPaneButtonClick("layers")}
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
rightPanelTab === "layers"
layersPaneOpen
? "bg-neutral-800 text-white"
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
}`}
Expand Down Expand Up @@ -171,79 +301,35 @@ export function StudioRightPanel({
compositionPath={activeBlockParams.compositionPath}
onClose={onCloseBlockParams ?? (() => {})}
/>
) : rightPanelTab === "layers" ? (
) : layersPaneOpen && designPaneOpen ? (
<div ref={splitContainerRef} className="flex h-full min-h-0 flex-col">
<div
className="min-h-[120px] overflow-hidden"
style={{ flexBasis: `${layersPanePercent}%`, flexShrink: 0 }}
>
<LayersPanel />
</div>
<div
role="separator"
aria-label="Resize Layers and Design panes"
aria-orientation="horizontal"
className="group flex h-2 flex-shrink-0 cursor-row-resize items-center justify-center border-y border-neutral-800 bg-neutral-900"
style={{ touchAction: "none" }}
onPointerDown={handleInspectorSplitResizeStart}
onPointerMove={handleInspectorSplitResizeMove}
onPointerUp={handleInspectorSplitResizeEnd}
onPointerCancel={handleInspectorSplitResizeEnd}
>
<div className="h-px w-10 rounded-full bg-white/12 transition-colors group-hover:bg-white/24 group-active:bg-studio-accent/70" />
</div>
<div className="min-h-0 flex-1 overflow-hidden">{propertyPanel}</div>
</div>
) : layersPaneOpen ? (
<LayersPanel />
) : designPanelActive ? (
<PropertyPanel
projectId={projectId}
projectDir={projectDir}
assets={assets}
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
multiSelectCount={domEditGroupSelections.length}
copiedAgentPrompt={copiedAgentPrompt}
onClearSelection={clearDomSelection}
onSetStyle={handleDomStyleCommit}
onSetAttribute={handleDomAttributeCommit}
onSetHtmlAttribute={handleDomHtmlAttributeCommit}
onSetManualOffset={handleDomPathOffsetCommit}
onSetManualSize={handleDomBoxSizeCommit}
onSetManualRotation={handleDomRotationCommit}
onSetText={handleDomTextCommit}
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
onAddTextField={handleDomAddTextField}
onRemoveTextField={handleDomRemoveTextField}
onAskAgent={handleAskAgent}
onImportAssets={handleImportFiles}
fontAssets={fontAssets}
onImportFonts={handleImportFonts}
previewIframeRef={previewIframeRef}
gsapAnimations={selectedGsapAnimations}
gsapMultipleTimelines={gsapMultipleTimelines}
gsapUnsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
onUpdateGsapProperty={handleGsapUpdateProperty}
onUpdateGsapMeta={handleGsapUpdateMeta}
onDeleteGsapAnimation={handleGsapDeleteAnimation}
onAddGsapProperty={handleGsapAddProperty}
onRemoveGsapProperty={handleGsapRemoveProperty}
onUpdateGsapFromProperty={handleGsapUpdateFromProperty}
onAddGsapFromProperty={handleGsapAddFromProperty}
onRemoveGsapFromProperty={handleGsapRemoveFromProperty}
onAddGsapAnimation={handleGsapAddAnimation}
onCommitAnimatedProperty={commitAnimatedProperty}
onAddKeyframe={handleGsapAddKeyframe}
onRemoveKeyframe={handleGsapRemoveKeyframe}
onConvertToKeyframes={handleGsapConvertToKeyframes}
onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)}
onSetArcPath={handleSetArcPath}
onUpdateArcSegment={handleUpdateArcSegment}
onUnroll={handleUnroll}
recordingState={recordingState}
recordingDuration={recordingDuration}
onToggleRecording={onToggleRecording}
/>
) : designPaneOpen ? (
propertyPanel
) : (
<RenderQueue
jobs={renderJobs}
projectId={projectId}
onDelete={renderQueue.deleteRender}
onClearCompleted={renderQueue.clearCompleted}
onStartRender={async (format, quality, resolution, fps) => {
await waitForPendingDomEditSaves();
const composition =
activeCompPath && activeCompPath !== "index.html"
? activeCompPath
: undefined;
await renderQueue.startRender({
fps,
quality,
format,
resolution,
composition,
});
}}
compositionDimensions={compositionDimensions}
isRendering={renderQueue.isRendering}
/>
renderQueuePanel
)}
</div>
</>
Expand Down
27 changes: 26 additions & 1 deletion packages/studio/src/components/editor/PropertyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@ import { MetricField, Section } from "./propertyPanelPrimitives";
import { createTransformCommitHandlers } from "./propertyPanelTransformCommit";
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
import {
ColorGradingSection,
isColorGradingCapableElement,
} from "./propertyPanelColorGradingSection";
import { TextSection, StyleSections } from "./propertyPanelSections";
import { GsapAnimationSection } from "./GsapAnimationSection";
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
import { KeyframeNavigation } from "./KeyframeNavigation";
import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
import {
STUDIO_COLOR_GRADING_ENABLED,
STUDIO_GSAP_PANEL_ENABLED,
STUDIO_KEYFRAMES_ENABLED,
} from "./manualEditingAvailability";
import { usePlayerStore, liveTime } from "../../player";
import { TimingSection } from "./propertyPanelTimingSection";
import { type PropertyPanelProps } from "./propertyPanelHelpers";
Expand Down Expand Up @@ -47,6 +55,7 @@ export const PropertyPanel = memo(function PropertyPanel({
onClearSelection,
onSetStyle,
onSetAttribute,
onSetAttributeLive,
onSetHtmlAttribute,
onSetManualOffset,
onSetManualSize,
Expand Down Expand Up @@ -355,6 +364,22 @@ export const PropertyPanel = memo(function PropertyPanel({
/>
)}

{STUDIO_COLOR_GRADING_ENABLED && isColorGradingCapableElement(element) && (
<ColorGradingSection
key={[
element.id ?? "",
element.hfId ?? "",
element.selector ?? "",
String(element.selectorIndex ?? ""),
].join("|")}
element={element}
assets={assets}
previewIframeRef={previewIframeRef}
onImportAssets={onImportAssets}
onSetAttributeLive={onSetAttributeLive}
/>
)}

<Section title="Layout" icon={<Move size={15} />}>
<div className={RESPONSIVE_GRID}>
<div className="flex items-center gap-1">
Expand Down
13 changes: 2 additions & 11 deletions packages/studio/src/components/editor/domEditOverlayGeometry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type DomEditSelection, findElementForSelection } from "./domEditing";
import { isElementVisibleThroughAncestors } from "./domEditingDom";

export interface OverlayRect {
left: number;
Expand All @@ -21,17 +22,7 @@ export type ResolvedElementRef = {
};

export function isElementVisibleForOverlay(el: HTMLElement): boolean {
const win = el.ownerDocument.defaultView;
if (!win) return true;
let current: HTMLElement | null = el;
while (current) {
const computed = win.getComputedStyle(current);
if (computed.display === "none" || computed.visibility === "hidden") return false;
const opacity = Number.parseFloat(computed.opacity);
if (Number.isFinite(opacity) && opacity <= 0.01) return false;
current = current.parentElement;
}
return true;
return isElementVisibleThroughAncestors(el);
}

function readPositiveDimension(value: string | null): number | null {
Expand Down
Loading
Loading