diff --git a/packages/studio/src/components/editor/ArcPathControls.tsx b/packages/studio/src/components/editor/ArcPathControls.tsx index e9f46607de..481efc9323 100644 --- a/packages/studio/src/components/editor/ArcPathControls.tsx +++ b/packages/studio/src/components/editor/ArcPathControls.tsx @@ -113,6 +113,7 @@ export const ArcPathControls = memo(function ArcPathControls({ )} + )} {onUngroup && element.dataAttributes["hf-group"] != null && ( - )} - +
{onToggleRecording && ( - + + + )}
); + return {classicPanel}; }); diff --git a/packages/studio/src/components/editor/PropertyPanelFlat.tsx b/packages/studio/src/components/editor/PropertyPanelFlat.tsx index ac90cfba79..28b61936b0 100644 --- a/packages/studio/src/components/editor/PropertyPanelFlat.tsx +++ b/packages/studio/src/components/editor/PropertyPanelFlat.tsx @@ -1,5 +1,7 @@ import { type ReactNode, useEffect, useRef, useState } from "react"; import { resolveEditingSections } from "@hyperframes/core/editing"; +import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext"; +import { slugifyDesignInput } from "../../utils/designInputTracking"; import type { DomEditSelection } from "./domEditing"; import { isTextEditableSelection } from "./domEditing"; import type { PropertyPanelProps } from "./propertyPanelHelpers"; @@ -502,67 +504,77 @@ export function PropertyPanelFlat({ const afterOpen = openIndex === -1 ? [] : groups.slice(openIndex + 1); return ( -
-
{showUngroup && ( - @@ -75,12 +88,22 @@ export function PropertyPanelFlatHeader({ type="button" aria-label="Copy element info to clipboard" title={copied ? "Copied!" : "Copy element info for any AI agent"} - onClick={onCopy} + onClick={() => { + track("button", "Copy element info"); + onCopy(); + }} className={copied ? "text-panel-accent" : undefined} > -
diff --git a/packages/studio/src/components/editor/gsapAnimationCallbacks.ts b/packages/studio/src/components/editor/gsapAnimationCallbacks.ts index e35f5cdd6e..c07d9c83b4 100644 --- a/packages/studio/src/components/editor/gsapAnimationCallbacks.ts +++ b/packages/studio/src/components/editor/gsapAnimationCallbacks.ts @@ -34,3 +34,30 @@ export interface GsapAnimationEditCallbacks { /** Unroll a computed (helper/loop) tween into literal tweens so it edits directly. */ onUnroll?: (animationId: string) => void; } + +// User-facing control label for each animation-meta field. The ease control is +// labelled "Speed" in the card UI, so ease/easeEach map there. +const ANIMATION_META_LABELS: Record = { + duration: { control: "metric", name: "Length" }, + position: { control: "metric", name: "Starts at" }, + ease: { control: "select", name: "Speed" }, + easeEach: { control: "select", name: "Speed" }, +}; + +/** + * Emit design-input telemetry for an `onUpdateMeta` payload, attributing each + * changed field to the control the user actually touched. Iterates the real keys + * present rather than falling through to a single placeholder — so a meta field + * added later is attributed honestly by its own key instead of poisoning another + * control's usage count. + */ +export function trackAnimationMetaUpdate( + track: (control: string, name: string) => void, + updates: Record, +): void { + for (const key of Object.keys(updates)) { + const mapped = ANIMATION_META_LABELS[key]; + if (mapped) track(mapped.control, mapped.name); + else track("select", key); + } +} diff --git a/packages/studio/src/components/editor/propertyPanel3dTransform.tsx b/packages/studio/src/components/editor/propertyPanel3dTransform.tsx index ad29d80d29..a74ebc9bcc 100644 --- a/packages/studio/src/components/editor/propertyPanel3dTransform.tsx +++ b/packages/studio/src/components/editor/propertyPanel3dTransform.tsx @@ -5,6 +5,7 @@ import { MetricField } from "./propertyPanelPrimitives"; import { KeyframeNavigation } from "./KeyframeNavigation"; import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers"; import { Transform3DCube, type CubePose } from "./Transform3DCube"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; // translateZ only foreshortens under a perspective lens. Rather than hardcode one // (an arbitrary px value reads wrong at different canvas sizes), derive it from the @@ -71,6 +72,7 @@ function Cube3dControl({ onKeyframe?: () => void; keyframed?: boolean; }) { + const track = useTrackDesignInput(); const pose: CubePose = { rotationX: gsapRuntimeValues.rotationX ?? 0, rotationY: gsapRuntimeValues.rotationY ?? 0, @@ -96,6 +98,7 @@ function Cube3dControl({ } const axes = Object.keys(changedProps); if (axes.length === 0) return; + track("slider", "3D rotation pose"); // ONE keyframe for the whole pose change — avoids per-axis commits racing into // adjacent duplicate keyframes. void onCommitAnimatedProperties(element, changedProps); @@ -111,6 +114,7 @@ function Cube3dControl({ scale: 1, transformPerspective: 0, }; + track("button", "Reset 3D transform"); void onCommitAnimatedProperties(element, identity); }; // Immediate element feedback while dragging — set the live transform without a @@ -168,6 +172,7 @@ function Cube3dControl({ } // One commit for all props so the writes can't race read-modify-write on // the same script (which dropped a prop and reverted after a seek). + track("slider", "3D depth"); void onCommitAnimatedProperties(element, props); }} onRecenter={recenter} diff --git a/packages/studio/src/components/editor/propertyPanelColor.tsx b/packages/studio/src/components/editor/propertyPanelColor.tsx index ea6dbb6008..2ef353a435 100644 --- a/packages/studio/src/components/editor/propertyPanelColor.tsx +++ b/packages/studio/src/components/editor/propertyPanelColor.tsx @@ -11,6 +11,7 @@ import { } from "./colorValue"; import { resolveFloatingPanelPosition, type FloatingPosition } from "./floatingPanel"; import { colorFromCss, FIELD, LABEL } from "./propertyPanelHelpers"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; const COLOR_PICKER_SIZE = { width: 292, height: 386 }; @@ -130,6 +131,7 @@ export function ColorField({ flat?: boolean; onCommit: (nextValue: string) => void; }) { + const track = useTrackDesignInput(); const buttonRef = useRef(null); const panelRef = useRef(null); const [open, setOpen] = useState(false); @@ -203,7 +205,9 @@ export function ColorField({ const commitColor = (nextColor: ParsedColor) => { setDraftColor(nextColor); setHexDraft(toHexColor(nextColor).toUpperCase()); - onCommit(formatCssColor(nextColor)); + const nextValue = formatCssColor(nextColor); + if (nextValue !== value) track("color", label); + onCommit(nextValue); }; const commitHsv = (nextHsv: { hue?: number; saturation?: number; value?: number }) => { diff --git a/packages/studio/src/components/editor/propertyPanelColorGradingControls.tsx b/packages/studio/src/components/editor/propertyPanelColorGradingControls.tsx index 138130a103..080dd3b137 100644 --- a/packages/studio/src/components/editor/propertyPanelColorGradingControls.tsx +++ b/packages/studio/src/components/editor/propertyPanelColorGradingControls.tsx @@ -11,6 +11,7 @@ import { ChevronDown, ChevronRight, Plus, X } from "../../icons/SystemIcons"; import { LUT_EXT } from "../../utils/mediaTypes"; import { LABEL } from "./propertyPanelHelpers"; import { ColorGradingSliderControl } from "./propertyPanelColorGradingSlider"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; const LUT_UPLOAD_DIR = "assets/luts"; @@ -175,6 +176,7 @@ export function ColorGradingControls({ onImportAssets?: (files: FileList, dir?: string) => Promise; onCommitColorGrading: (nextGrading: NormalizedHfColorGrading) => void; }) { + const track = useTrackDesignInput(); const lutInputRef = useRef(null); const [lutOpen, setLutOpen] = useState(false); const [detailSettings, setDetailSettings] = useState<"vignette" | "grain" | null>(null); @@ -195,7 +197,10 @@ export function ColorGradingControls({ const applyPreset = (preset: string) => { const next = normalizeHfColorGrading({ preset, intensity: 1, lut: grading.lut }); - if (next) onCommitColorGrading(next); + if (next) { + track("select", "Preset"); + onCommitColorGrading(next); + } }; const updateFilterIntensity = (value: number) => { onCommitColorGrading({ @@ -218,7 +223,10 @@ export function ColorGradingControls({ if (!files?.length || !onImportAssets) return; const uploaded = await onImportAssets(files, LUT_UPLOAD_DIR); const firstLut = uploaded.find((asset) => LUT_EXT.test(asset)); - if (firstLut) applyLut(firstLut, 1); + if (firstLut) { + track("button", "Import LUT"); + applyLut(firstLut, 1); + } }; const commitDetailSlider = (slider: DetailSlider, next: number) => { onCommitColorGrading({ @@ -313,6 +321,7 @@ export function ColorGradingControls({ value={selectedLut} onChange={(event) => { const nextSrc = event.target.value; + track("select", "Custom LUT"); applyLut( nextSrc || null, nextSrc && grading.lut?.src === nextSrc ? grading.lut.intensity : 1, diff --git a/packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx b/packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx index 7331cba4ef..a0a49e4e68 100644 --- a/packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelColorGradingSection.tsx @@ -9,6 +9,7 @@ import { type MediaMetadata, type RuntimeColorGradingStatus, } from "./useColorGradingController"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; function StatusPill({ status }: { status: RuntimeColorGradingStatus }) { const dotClass = @@ -68,10 +69,12 @@ function HoldBeforeButton({ disabled: boolean; onHoldChange: (holding: boolean) => void; }) { + const track = useTrackDesignInput(); const startHold = (event: ReactPointerEvent) => { if (disabled) return; event.preventDefault(); event.stopPropagation(); + track("toggle", "Compare original"); onHoldChange(true); const release = () => { onHoldChange(false); @@ -105,7 +108,10 @@ function HoldBeforeButton({ onKeyDown={(event) => { if (disabled || (event.key !== " " && event.key !== "Enter")) return; event.preventDefault(); - if (!active) onHoldChange(true); + if (!active) { + track("toggle", "Compare original"); + onHoldChange(true); + } }} onKeyUp={(event) => { if (disabled || (event.key !== " " && event.key !== "Enter")) return; @@ -148,6 +154,7 @@ export function ColorGradingSection({ value: string | null, ) => Promise<{ changedFiles: number; changedElements: number }>; }) { + const track = useTrackDesignInput(); const { grading, compareEnabled, @@ -184,6 +191,7 @@ export function ColorGradingSection({ type="button" onClick={(event) => { event.stopPropagation(); + track("button", "Reset color grading"); resetGrading(); }} className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1" @@ -205,7 +213,10 @@ export function ColorGradingSection({
onSetApplyScope(e.target.value as "source-file" | "project")} + onChange={(e) => { + track("select", "Copy grade scope"); + onSetApplyScope(e.target.value as "source-file" | "project"); + }} disabled={applyBusy} className="bg-transparent font-mono text-[11px] text-panel-text-0 outline-none disabled:opacity-50" > @@ -540,7 +555,10 @@ export function FlatColorGradingSection({ type="button" data-flat-grade-apply="true" disabled={applyBusy} - onClick={onApplyToScope} + onClick={() => { + track("button", "Apply grade to scope"); + onApplyToScope(); + }} className="text-[11px] font-medium text-panel-accent hover:text-panel-accent/80 disabled:cursor-not-allowed disabled:opacity-50" > {applyBusy ? "Applying" : "Apply"} diff --git a/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx index 60988350e4..da217814f5 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx @@ -1,3 +1,4 @@ +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives"; import { KeyframeNavigation } from "./KeyframeNavigation"; import { formatPxMetricValue } from "./propertyPanelHelpers"; @@ -67,6 +68,7 @@ function KeyframeGutter({ | "onRemoveKeyframe" | "onConvertToKeyframes" >) { + const track = useTrackDesignInput(); if (!STUDIO_KEYFRAMES_ENABLED || !gsapAnimId) return null; const hasKeyframesOnProp = Boolean(navKeyframes?.some((kf) => property in kf.properties)); return ( @@ -76,11 +78,21 @@ function KeyframeGutter({ keyframes={navKeyframes} currentPercentage={currentPct} onSeek={seekFromKfPct} - onAddKeyframe={() => - onCommitAnimatedProperty && void onCommitAnimatedProperty(element, property, displayValue) - } - onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp(property), pct)} - onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp(property))} + onAddKeyframe={() => { + if (!onCommitAnimatedProperty) return; + track("button", `Add ${property} keyframe`); + void onCommitAnimatedProperty(element, property, displayValue); + }} + onRemoveKeyframe={(pct) => { + if (!onRemoveKeyframe) return; + track("button", `Remove ${property} keyframe`); + onRemoveKeyframe(animIdForProp(property), pct); + }} + onConvertToKeyframes={() => { + if (!onConvertToKeyframes) return; + track("button", `Convert ${property} to keyframes`); + onConvertToKeyframes(animIdForProp(property)); + }} /> ); diff --git a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx index c63ef12019..1850a48905 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; import { Check, ClipboardList } from "../../icons/SystemIcons"; import type { DomEditSelection } from "./domEditing"; import { @@ -37,6 +38,7 @@ export function FlatMediaSection({ }, ) => Promise; }) { + const track = useTrackDesignInput(); const isVideo = element.tagName === "video"; const isAudio = element.tagName === "audio"; const isImage = element.tagName === "img"; @@ -91,6 +93,7 @@ export function FlatMediaSection({ const runBackgroundRemoval = async () => { if (!onRemoveBackground || !projectSrc || removeBusy) return; + track("button", "Remove background"); setRemoveBusy(true); setRemoveProgress({ status: "processing", progress: 0, stage: "Preparing" }); try { @@ -126,6 +129,7 @@ export function FlatMediaSection({ type="button" data-flat-media-copy="true" onClick={() => { + track("button", "Copy media path"); void navigator.clipboard.writeText(absoluteSrc).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); diff --git a/packages/studio/src/components/editor/propertyPanelFlatMotionSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatMotionSection.tsx index f457979c68..f7813cfed8 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatMotionSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatMotionSection.tsx @@ -1,12 +1,16 @@ import { useState } from "react"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; import type { DomEditSelection } from "./domEditing"; import { formatTimingValue, RESPONSIVE_GRID } from "./propertyPanelHelpers"; import { parseTimingValue } from "./propertyPanelTimingSection"; import { CommitField } from "./propertyPanelPrimitives"; import { AnimationCard } from "./AnimationCard"; import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants"; -import type { GsapAnimationEditCallbacks } from "./gsapAnimationCallbacks"; +import { + trackAnimationMetaUpdate, + type GsapAnimationEditCallbacks, +} from "./gsapAnimationCallbacks"; import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation"; export function FlatTimingRow({ @@ -25,6 +29,7 @@ export function FlatTimingRow({ * documented below) when the caller doesn't wire it up. */ onSetAttributes?: (selection: DomEditSelection, attrs: Record) => Promise; }) { + const track = useTrackDesignInput(); const { start, duration, inferred: derived } = deriveElementTiming(element, animations); const end = start + duration; @@ -81,7 +86,13 @@ export function FlatTimingRow({
{label} - + { + track("metric", label); + onCommit(next); + }} + />
); @@ -122,7 +133,17 @@ export function FlatMotionSection({ onSetAttributes?: (selection: DomEditSelection, attrs: Record) => Promise; onAddAnimation: (method: "to" | "from" | "set" | "fromTo") => void; } & GsapAnimationEditCallbacks) { + 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); + }; return (
@@ -155,7 +176,97 @@ export function FlatMotionSection({ animation={anim} defaultExpanded={index === 0} flat - {...callbacks} + onUpdateProperty={(animationId, property, value) => { + trackProperty(property); + callbacks.onUpdateProperty(animationId, property, value); + }} + onUpdateMeta={(animationId, updates) => { + trackAnimationMetaUpdate(track, updates); + callbacks.onUpdateMeta(animationId, updates); + }} + onDeleteAnimation={(animationId) => { + track("button", "Remove animation"); + callbacks.onDeleteAnimation(animationId); + }} + onAddProperty={(animationId, property) => { + track("select", "Add effect property"); + callbacks.onAddProperty(animationId, property); + }} + onRemoveProperty={(animationId, property) => { + track("button", `Remove ${property}`); + callbacks.onRemoveProperty(animationId, property); + }} + onUpdateFromProperty={ + callbacks.onUpdateFromProperty + ? (animationId, property, value) => { + trackProperty(property); + callbacks.onUpdateFromProperty?.(animationId, property, value); + } + : undefined + } + onAddFromProperty={ + callbacks.onAddFromProperty + ? (animationId, property) => { + track("select", "Add from property"); + callbacks.onAddFromProperty?.(animationId, property); + } + : undefined + } + onRemoveFromProperty={ + callbacks.onRemoveFromProperty + ? (animationId, property) => { + track("button", `Remove from ${property}`); + callbacks.onRemoveFromProperty?.(animationId, property); + } + : undefined + } + onLivePreview={callbacks.onLivePreview} + onLivePreviewEnd={callbacks.onLivePreviewEnd} + onSetArcPath={ + callbacks.onSetArcPath + ? (animationId, config) => { + track( + "toggle", + config.autoRotate !== undefined ? "Auto rotate" : "Arc motion", + ); + callbacks.onSetArcPath?.(animationId, config); + } + : undefined + } + onUpdateArcSegment={ + callbacks.onUpdateArcSegment + ? (animationId, segmentIndex, update) => { + if (update.curviness === undefined) { + track("button", `Reset arc segment ${segmentIndex + 1}`); + } + callbacks.onUpdateArcSegment?.(animationId, segmentIndex, update); + } + : undefined + } + onUpdateKeyframeEase={ + callbacks.onUpdateKeyframeEase + ? (animationId, percentage, ease) => { + track("select", "Keyframe ease"); + callbacks.onUpdateKeyframeEase?.(animationId, percentage, ease); + } + : undefined + } + onSetAllKeyframeEases={ + callbacks.onSetAllKeyframeEases + ? (animationId, ease) => { + track("select", "All keyframe eases"); + callbacks.onSetAllKeyframeEases?.(animationId, ease); + } + : undefined + } + onUnroll={ + callbacks.onUnroll + ? (animationId) => { + track("button", "Unroll animation"); + callbacks.onUnroll?.(animationId); + } + : undefined + } /> ))}
@@ -167,6 +278,7 @@ export function FlatMotionSection({ type="button" title={METHOD_TOOLTIPS[method]} onClick={() => { + track("button", `Add ${method} animation`); onAddAnimation(method); setAddMenuOpen(false); }} diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx index d8b1752d7c..9ac716f45b 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState, type ReactNode } from "react"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; import { RotateCcw } from "../../icons/SystemIcons"; import { CommitField } from "./propertyPanelPrimitives"; import { @@ -33,6 +34,7 @@ export function FlatRow({ onCommit: (nextValue: string) => void; onReset?: () => void; }) { + const track = useTrackDesignInput(); return (
{label} @@ -49,7 +51,10 @@ export function FlatRow({ value={value} disabled={disabled} liveCommit={liveCommit} - onCommit={onCommit} + onCommit={(nextValue) => { + track("metric", label); + onCommit(nextValue); + }} /> {suffix} @@ -58,7 +63,10 @@ export function FlatRow({ type="button" data-flat-row-reset="true" title="Remove — fall back to default" - onClick={onReset} + onClick={() => { + track("button", `Reset ${label}`); + onReset(); + }} className="flex-shrink-0 text-panel-text-3 opacity-0 transition-opacity hover:text-panel-text-1 group-hover:opacity-100" > @@ -109,6 +117,7 @@ export function FlatSegmentedRow({ spacerAfterIndex?: number; onChange: (nextKey: string) => void; }) { + const track = useTrackDesignInput(); return (
{label} @@ -121,7 +130,10 @@ export function FlatSegmentedRow({ aria-label={option.label} aria-pressed={option.active} disabled={disabled} - onClick={() => onChange(option.key)} + onClick={() => { + if (!option.active) track("segmented", label); + onChange(option.key); + }} className={`px-1.5 py-1 text-[11px] transition-colors disabled:cursor-not-allowed ${ option.active ? "border-b-2 border-panel-accent text-panel-text-0" @@ -270,6 +282,7 @@ export function FlatSlider({ onReset?: () => void; onCommit: (nextValue: number) => void; }) { + const track = useTrackDesignInput(); // `draft` gives the knob instant, drag-local visual feedback. `onCommit` is // throttled (not debounced) to at most once per 40ms: a real drag fires // pointermove faster than that, and a pure debounce (reset the timer on @@ -439,6 +452,7 @@ export function FlatSlider({ const stepped = stepFromClientX(e.clientX, e.currentTarget.getBoundingClientRect()); setDraft(stepped); commitDraft(stepped); + if (stepped !== dragStartValueRef.current) track("slider", label); }} onPointerCancel={(e) => { // A native pointercancel means the platform aborted the gesture (a @@ -481,6 +495,7 @@ export function FlatSlider({ e.preventDefault(); setDraft(next); commitDraft(next); + if (next !== draft) track("slider", label); }} onContextMenu={(e) => { // Right-click during a drag must cancel it (revert to the pre-drag @@ -530,7 +545,10 @@ export function FlatSlider({ data-flat-slider-reset="true" title="Remove — fall back to default" disabled={disabled} - onClick={onReset} + onClick={() => { + track("button", `Reset ${label}`); + onReset(); + }} className="text-panel-text-3 hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40" > diff --git a/packages/studio/src/components/editor/propertyPanelFlatSelectRow.tsx b/packages/studio/src/components/editor/propertyPanelFlatSelectRow.tsx index f3ef5cc6d8..f081c507d9 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatSelectRow.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatSelectRow.tsx @@ -1,4 +1,5 @@ import { RotateCcw } from "../../icons/SystemIcons"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; import { VALUE_TIER_LABEL_CLASS, VALUE_TIER_VALUE_CLASS, @@ -32,6 +33,8 @@ export function FlatSelectRow({ onChange: (nextValue: string) => void; onReset?: () => void; }) { + const track = useTrackDesignInput(); + const trackName = ariaLabel || label; const normalizedOptions = options.map((option) => typeof option === "string" ? { value: option, label: option } : option, ); @@ -55,7 +58,10 @@ export function FlatSelectRow({ value={value} disabled={disabled} aria-label={ariaLabel || label || undefined} - onChange={(e) => onChange(e.target.value)} + onChange={(e) => { + track("select", trackName); + onChange(e.target.value); + }} className={`appearance-none bg-transparent text-right font-mono text-[11px] outline-none disabled:cursor-not-allowed ${VALUE_TIER_VALUE_CLASS[tier]}`} > {renderedOptions.map((option) => ( @@ -80,7 +86,10 @@ export function FlatSelectRow({ data-flat-select-reset="true" title="Remove — fall back to default" disabled={disabled} - onClick={onReset} + onClick={() => { + track("button", `Reset ${trackName}`); + onReset(); + }} className="flex-shrink-0 text-panel-text-3 opacity-0 transition-opacity hover:text-panel-text-1 group-hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-40" > diff --git a/packages/studio/src/components/editor/propertyPanelFlatTextSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatTextSection.tsx index 8f8d046d46..7a05c488dd 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatTextSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatTextSection.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; import { Plus, X } from "../../icons/SystemIcons"; import { isTextEditableSelection, type DomEditSelection } from "./domEditing"; import type { ImportedFontAsset } from "./fontAssets"; @@ -56,6 +57,7 @@ function FlatTextFieldEditor({ onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void; autoFocus?: boolean; }) { + const track = useTrackDesignInput(); const weight = getTextStyleValue(field, styles, "font-weight", "400"); const weightOptions = detectAvailableWeights( field.computedStyles["font-family"] || styles["font-family"] || "", @@ -112,7 +114,10 @@ function FlatTextFieldEditor({