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/components/editor/ArcPathControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const ArcPathControls = memo(function ArcPathControls({
)}
</div>
<SliderControl
trackName={segmentCount === 1 ? "Curviness" : `Segment ${i + 1} curviness`}
value={seg.curviness}
min={0}
max={3}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";

export type GestureRecordingState = "idle" | "recording" | "preview";

interface GestureRecordIconProps {
Expand Down Expand Up @@ -28,13 +30,17 @@ export function GestureRecordPanelButton({
onToggleRecording,
}: GestureRecordPanelButtonProps) {
const recording = recordingState === "recording";
const track = useTrackDesignInput();

return (
<div className="px-4 pb-3">
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={onToggleRecording}
onClick={() => {
track("button", "Gesture recording");
onToggleRecording();
}}
className={`w-full flex items-center justify-center gap-2 rounded-lg py-2 text-[11px] font-medium transition-colors ${
recording
? "bg-red-500/15 text-red-400 border border-red-500/30 animate-pulse"
Expand Down
123 changes: 109 additions & 14 deletions packages/studio/src/components/editor/GsapAnimationSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { Film } from "../../icons/SystemIcons";
import { Section } from "./propertyPanelPrimitives";
import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";
import { AnimationCard } from "./AnimationCard";
import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks";
import {
trackAnimationMetaUpdate,
type GsapAnimationEditCallbacks,
} from "./gsapAnimationCallbacks";
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";

interface GsapAnimationSectionProps extends GsapAnimationEditCallbacks {
animations: GsapAnimation[];
Expand Down Expand Up @@ -34,7 +38,24 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
onSetAllKeyframeEases,
onUnroll,
}: GsapAnimationSectionProps) {
const track = useTrackDesignInput();
const [addMenuOpen, setAddMenuOpen] = useState(false);
const trackProperty = (property: string) => {
const control =
property === "visibility"
? "toggle"
: property === "filter" || property === "clipPath"
? "text"
: "metric";
track(control, property);
};
const updateMeta = (
animationId: string,
updates: { duration?: number; ease?: string; position?: number },
) => {
trackAnimationMetaUpdate(track, updates);
onUpdateMeta(animationId, updates);
};

return (
<Section title="Animation" icon={<Film size={15} />}>
Expand All @@ -58,21 +79,94 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
key={anim.id}
animation={anim}
defaultExpanded={index === 0}
onUpdateProperty={onUpdateProperty}
onUpdateMeta={onUpdateMeta}
onDeleteAnimation={onDeleteAnimation}
onAddProperty={onAddProperty}
onRemoveProperty={onRemoveProperty}
onUpdateFromProperty={onUpdateFromProperty}
onAddFromProperty={onAddFromProperty}
onRemoveFromProperty={onRemoveFromProperty}
onUpdateProperty={(animationId, property, value) => {
trackProperty(property);
onUpdateProperty(animationId, property, value);
}}
onUpdateMeta={updateMeta}
onDeleteAnimation={(animationId) => {
track("button", "Remove animation");
onDeleteAnimation(animationId);
}}
onAddProperty={(animationId, property) => {
track("select", "Add effect property");
onAddProperty(animationId, property);
}}
onRemoveProperty={(animationId, property) => {
track("button", `Remove ${property}`);
onRemoveProperty(animationId, property);
}}
onUpdateFromProperty={
onUpdateFromProperty
? (animationId, property, value) => {
trackProperty(property);
onUpdateFromProperty(animationId, property, value);
}
: undefined
}
onAddFromProperty={
onAddFromProperty
? (animationId, property) => {
track("select", "Add from property");
onAddFromProperty(animationId, property);
}
: undefined
}
onRemoveFromProperty={
onRemoveFromProperty
? (animationId, property) => {
track("button", `Remove from ${property}`);
onRemoveFromProperty(animationId, property);
}
: undefined
}
onLivePreview={onLivePreview}
onLivePreviewEnd={onLivePreviewEnd}
onSetArcPath={onSetArcPath}
onUpdateArcSegment={onUpdateArcSegment}
onUpdateKeyframeEase={onUpdateKeyframeEase}
onSetAllKeyframeEases={onSetAllKeyframeEases}
onUnroll={onUnroll}
onSetArcPath={
onSetArcPath
? (animationId, config) => {
track(
"toggle",
config.autoRotate !== undefined ? "Auto rotate" : "Arc motion",
);
onSetArcPath(animationId, config);
}
: undefined
}
onUpdateArcSegment={
onUpdateArcSegment
? (animationId, segmentIndex, update) => {
if (update.curviness === undefined) {
track("button", `Reset arc segment ${segmentIndex + 1}`);
}
onUpdateArcSegment(animationId, segmentIndex, update);
}
: undefined
}
onUpdateKeyframeEase={
onUpdateKeyframeEase
? (animationId, percentage, ease) => {
track("select", "Keyframe ease");
onUpdateKeyframeEase(animationId, percentage, ease);
}
: undefined
}
onSetAllKeyframeEases={
onSetAllKeyframeEases
? (animationId, ease) => {
track("select", "All keyframe eases");
onSetAllKeyframeEases(animationId, ease);
}
: undefined
}
onUnroll={
onUnroll
? (animationId) => {
track("button", "Unroll animation");
onUnroll(animationId);
}
: undefined
}
/>
))}

Expand All @@ -85,6 +179,7 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
type="button"
title={METHOD_TOOLTIPS[method]}
onClick={() => {
track("button", `Add ${method} animation`);
onAddAnimation(method);
setAddMenuOpen(false);
}}
Expand Down
46 changes: 42 additions & 4 deletions packages/studio/src/components/editor/InspectorHeaderActions.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,58 @@
import { Eye, EyeSlash } from "@phosphor-icons/react";
import { X } from "../../icons/SystemIcons";
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
import type { DomEditSelection } from "./domEditingTypes";

/** The action buttons in the inspector header: Ungroup (groups only), copy, clear. */
/** The action buttons in the inspector header: visibility, Ungroup (groups only), copy, clear. */
export function InspectorHeaderActions({
element,
copied,
onCopy,
onClear,
onUngroup,
selectedElementId,
selectedElementHidden,
visibilityLabel,
onToggleHidden,
}: {
element: DomEditSelection;
copied: boolean;
onCopy: () => void;
onClear: () => void;
onUngroup?: () => void;
selectedElementId?: string | null;
selectedElementHidden?: boolean;
visibilityLabel?: string;
onToggleHidden?: (id: string, hidden: boolean) => void;
}) {
const track = useTrackDesignInput();
return (
<div className="flex items-center gap-1">
{selectedElementId && onToggleHidden && (
<button
type="button"
aria-label={visibilityLabel}
title={visibilityLabel}
onClick={() => {
track("toggle", "Element visibility");
void onToggleHidden(selectedElementId, !selectedElementHidden);
}}
className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
>
{selectedElementHidden ? (
<EyeSlash size={13} weight="bold" aria-hidden="true" />
) : (
<Eye size={13} weight="bold" aria-hidden="true" />
)}
</button>
)}
{onUngroup && element.dataAttributes["hf-group"] != null && (
<button
type="button"
onClick={onUngroup}
onClick={() => {
track("button", "Ungroup");
onUngroup();
}}
title="Ungroup (⌘⇧G)"
className="flex h-6 items-center rounded px-2 text-[11px] font-medium text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-neutral-200"
>
Expand All @@ -29,7 +61,10 @@ export function InspectorHeaderActions({
)}
<button
type="button"
onClick={onCopy}
onClick={() => {
track("button", "Copy element info");
onCopy();
}}
className={`flex h-6 w-6 items-center justify-center rounded transition-colors ${
copied
? "text-studio-accent"
Expand All @@ -52,7 +87,10 @@ export function InspectorHeaderActions({
<button
type="button"
aria-label="Clear selection"
onClick={onClear}
onClick={() => {
track("button", "Clear selection");
onClear();
}}
className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
>
<X size={13} />
Expand Down
56 changes: 23 additions & 33 deletions packages/studio/src/components/editor/PropertyPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { memo, useEffect, useMemo, useRef, useState } from "react";
import { Move } from "../../icons/SystemIcons";
import { Eye, EyeSlash } from "@phosphor-icons/react";
import { InspectorHeaderActions } from "./InspectorHeaderActions";
import { useStudioShellContext } from "../../contexts/StudioContext";
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
Expand Down Expand Up @@ -38,6 +37,7 @@ import { TimingSection } from "./propertyPanelTimingSection";
import { type PropertyPanelProps } from "./propertyPanelHelpers";
import { GestureRecordPanelButton } from "./GestureRecordControl";
import { PropertyPanelEmptyState } from "./PropertyPanelEmptyState";
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";

// Re-export helpers that external consumers import from this module
export {
Expand Down Expand Up @@ -303,51 +303,40 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
);
}

return (
const classicPanel = (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
<div className="px-4 py-3">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[13px] font-semibold text-neutral-100">
{element.label}
<DesignPanelInputProvider section="header">
<div className="px-4 py-3">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[13px] font-semibold text-neutral-100">
{element.label}
</div>
<div className="mt-0.5 truncate text-[11px] text-neutral-500">{sourceLabel}</div>
</div>
<div className="mt-0.5 truncate text-[11px] text-neutral-500">{sourceLabel}</div>
</div>
<div className="flex items-center gap-1">
{selectedElementId && onToggleElementHidden && (
<button
type="button"
aria-label={visibilityToggleLabel}
title={visibilityToggleLabel}
onClick={() => {
void onToggleElementHidden(selectedElementId, !selectedElementHidden);
}}
className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
>
{selectedElementHidden ? (
<EyeSlash size={13} weight="bold" aria-hidden="true" />
) : (
<Eye size={13} weight="bold" aria-hidden="true" />
)}
</button>
)}
<InspectorHeaderActions
element={element}
copied={clipboardCopied}
onCopy={handleCopyElementInfo}
onClear={onClearSelection}
onUngroup={onUngroup}
selectedElementId={selectedElementId}
selectedElementHidden={selectedElementHidden}
visibilityLabel={visibilityToggleLabel}
onToggleHidden={onToggleElementHidden}
/>
</div>
</div>
</div>
</DesignPanelInputProvider>
<div className="flex-1 overflow-y-auto">
{onToggleRecording && (
<GestureRecordPanelButton
recordingState={recordingState}
recordingDuration={recordingDuration}
onToggleRecording={onToggleRecording}
/>
<DesignPanelInputProvider section="footer">
<GestureRecordPanelButton
recordingState={recordingState}
recordingDuration={recordingDuration}
onToggleRecording={onToggleRecording}
/>
</DesignPanelInputProvider>
)}

<TextSection
Expand Down Expand Up @@ -593,4 +582,5 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
</div>
</div>
);
return <DesignPanelInputProvider ui="classic">{classicPanel}</DesignPanelInputProvider>;
});
Loading
Loading