@hyperframes/studio 0.7.58 → 0.7.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-Bm07FLkl.js} +1 -1
  2. package/dist/assets/{index-C47jAC3Q.js → index-B995FG46.js} +1 -1
  3. package/dist/assets/index-CrkAdJkb.js +426 -0
  4. package/dist/assets/index-Dj5p8U_A.css +1 -0
  5. package/dist/assets/{index-DeQPzqwH.js → index-FvzmPhfG.js} +1 -1
  6. package/dist/index.d.ts +12 -3
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +13514 -8528
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.d.ts +5 -0
  11. package/dist/styles/tailwind-preset.js +8 -1
  12. package/dist/styles/tailwind-preset.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +3 -3
  15. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  16. package/src/components/StudioRightPanel.tsx +29 -51
  17. package/src/components/editor/AnimationCard.test.tsx +134 -0
  18. package/src/components/editor/AnimationCard.tsx +19 -4
  19. package/src/components/editor/ArcPathControls.tsx +1 -0
  20. package/src/components/editor/GestureRecordControl.tsx +7 -1
  21. package/src/components/editor/GsapAnimationSection.tsx +109 -14
  22. package/src/components/editor/InspectorHeaderActions.tsx +42 -4
  23. package/src/components/editor/PropertyPanel.test.tsx +948 -0
  24. package/src/components/editor/PropertyPanel.tsx +144 -148
  25. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  26. package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
  27. package/src/components/editor/PropertyPanelFlat.tsx +580 -0
  28. package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
  29. package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
  30. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
  31. package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
  32. package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
  33. package/src/components/editor/gsapLivePreview.ts +34 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +10 -0
  35. package/src/components/editor/manualEditingAvailability.ts +9 -0
  36. package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
  37. package/src/components/editor/propertyPanelColor.test.tsx +28 -0
  38. package/src/components/editor/propertyPanelColor.tsx +31 -1
  39. package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
  40. package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
  41. package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
  42. package/src/components/editor/propertyPanelFill.tsx +22 -5
  43. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
  44. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
  45. package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
  46. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
  47. package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
  48. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
  49. package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
  50. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
  51. package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
  52. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
  53. package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
  54. package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
  55. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
  56. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
  57. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
  58. package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
  59. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
  60. package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
  61. package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
  62. package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
  63. package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
  64. package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
  65. package/src/components/editor/propertyPanelFont.test.tsx +30 -0
  66. package/src/components/editor/propertyPanelFont.tsx +136 -95
  67. package/src/components/editor/propertyPanelHelpers.ts +73 -0
  68. package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
  69. package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
  70. package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
  71. package/src/components/editor/propertyPanelSections.test.tsx +161 -0
  72. package/src/components/editor/propertyPanelSections.tsx +154 -90
  73. package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
  74. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  75. package/src/components/editor/propertyPanelTypes.ts +14 -1
  76. package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
  77. package/src/components/editor/propertyPanelValueTier.ts +28 -0
  78. package/src/components/editor/useColorGradingController.test.ts +418 -0
  79. package/src/components/editor/useColorGradingController.ts +596 -0
  80. package/src/components/panels/VariablesPanel.tsx +4 -0
  81. package/src/components/sidebar/CompositionsTab.test.ts +28 -2
  82. package/src/components/sidebar/CompositionsTab.tsx +3 -1
  83. package/src/components/storyboard/FramePoster.tsx +24 -4
  84. package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
  85. package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
  86. package/src/components/storyboard/StoryboardGrid.tsx +23 -1
  87. package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
  88. package/src/components/storyboard/StoryboardView.tsx +9 -2
  89. package/src/components/storyboard/frameComments.test.ts +95 -0
  90. package/src/components/storyboard/frameComments.ts +115 -0
  91. package/src/components/storyboard/useFrameComments.ts +82 -0
  92. package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
  93. package/src/contexts/DesignPanelInputContext.tsx +48 -0
  94. package/src/contexts/DomEditContext.tsx +4 -0
  95. package/src/contexts/VariablePromoteContext.test.tsx +67 -0
  96. package/src/contexts/VariablePromoteContext.tsx +10 -5
  97. package/src/hooks/domEditCommitRunner.ts +11 -0
  98. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  99. package/src/hooks/timelineTrackVisibility.test.ts +52 -0
  100. package/src/hooks/timelineTrackVisibility.ts +27 -10
  101. package/src/hooks/useAppHotkeys.ts +9 -0
  102. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  103. package/src/hooks/useDomEditCommits.ts +29 -175
  104. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  105. package/src/hooks/useDomEditSession.ts +17 -5
  106. package/src/hooks/useDomEditTextCommits.ts +14 -9
  107. package/src/hooks/useDomEditWiring.ts +10 -9
  108. package/src/hooks/useElementLifecycleOps.ts +8 -3
  109. package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
  110. package/src/hooks/useFileManager.ts +82 -60
  111. package/src/hooks/useGsapAnimationOps.ts +7 -6
  112. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  113. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  114. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  115. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  116. package/src/hooks/useGsapScriptCommits.ts +150 -50
  117. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  118. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  119. package/src/hooks/useInspectorSplitResize.ts +51 -0
  120. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  121. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  122. package/src/hooks/usePersistentEditHistory.ts +71 -31
  123. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  124. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  125. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  126. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  127. package/src/hooks/useSdkSession.ts +155 -14
  128. package/src/hooks/useSlideshowPersist.ts +7 -1
  129. package/src/hooks/useStoryboard.ts +23 -4
  130. package/src/hooks/useTimelineEditing.test.tsx +4 -1
  131. package/src/hooks/useTimelineEditing.ts +16 -10
  132. package/src/hooks/useTimelineEditingTypes.ts +3 -0
  133. package/src/hooks/useTimelineGroupEditing.ts +16 -2
  134. package/src/hooks/useVariablesPersist.ts +9 -7
  135. package/src/icons/SystemIcons.tsx +2 -0
  136. package/src/styles/studio.css +22 -0
  137. package/src/styles/tailwind-preset.shared.js +7 -0
  138. package/src/utils/designInputTracking.test.ts +97 -0
  139. package/src/utils/designInputTracking.ts +80 -0
  140. package/src/utils/sdkCutover.gate.test.ts +7 -7
  141. package/src/utils/sdkCutover.test.ts +490 -57
  142. package/src/utils/sdkCutover.ts +181 -219
  143. package/src/utils/sdkEditTransaction.ts +292 -0
  144. package/src/utils/sdkResolverAttempts.ts +94 -0
  145. package/src/utils/sdkResolverShadow.test.ts +148 -0
  146. package/src/utils/sdkResolverShadow.ts +139 -106
  147. package/src/utils/setSlideshowManifest.test.ts +0 -9
  148. package/src/utils/setSlideshowManifest.ts +20 -27
  149. package/src/utils/studioFileHistory.test.ts +41 -0
  150. package/src/utils/studioFileHistory.ts +30 -27
  151. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  152. package/src/utils/studioHelpers.test.ts +45 -0
  153. package/src/utils/studioHelpers.ts +45 -14
  154. package/src/utils/studioSaveDiagnostics.ts +17 -0
  155. package/dist/assets/index-B_UvTX3E.js +0 -423
  156. package/dist/assets/index-uahwWkgw.css +0 -1
@@ -0,0 +1,73 @@
1
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
2
+
3
+ export function PropertyPanelFlatFooter({
4
+ onAskAgent,
5
+ recordingState,
6
+ recordingDuration,
7
+ onToggleRecording,
8
+ }: {
9
+ onAskAgent?: () => void;
10
+ recordingState?: "idle" | "recording" | "preview";
11
+ recordingDuration?: number;
12
+ onToggleRecording?: () => void;
13
+ }) {
14
+ const track = useTrackDesignInput();
15
+ const recording = recordingState === "recording";
16
+ const recordTitle = recording
17
+ ? `Stop recording ${(recordingDuration ?? 0).toFixed(1)}s`
18
+ : "Record gesture (R)";
19
+
20
+ return (
21
+ // No border-t here: every possible element immediately above this footer
22
+ // in the new fixed-headers + scrollable-open-section layout (a collapsed
23
+ // FlatGroupHeader, or the open group's scrollable body wrapper) already
24
+ // draws its own border-b in normal document flow — nothing here is
25
+ // `position: sticky` anymore, so there's no rounding seam to seal (see
26
+ // p11-scrollable-open-section-report.md).
27
+ <div className="flex items-center justify-between bg-panel-bg px-4 py-[11px]">
28
+ <button
29
+ type="button"
30
+ data-flat-footer-ask="true"
31
+ onClick={() => {
32
+ track("button", "Ask agent");
33
+ onAskAgent?.();
34
+ }}
35
+ disabled={!onAskAgent}
36
+ className="flex items-center gap-[7px] text-[11px] font-medium text-panel-text-2 disabled:cursor-not-allowed"
37
+ >
38
+ <svg
39
+ width="13"
40
+ height="13"
41
+ viewBox="0 0 16 16"
42
+ fill="currentColor"
43
+ className="text-panel-accent"
44
+ >
45
+ <path d="M8 1l1.4 4.6L14 7l-4.6 1.4L8 13l-1.4-4.6L2 7l4.6-1.4z" />
46
+ </svg>
47
+ Ask agent about this element
48
+ </button>
49
+ {onToggleRecording && (
50
+ <button
51
+ type="button"
52
+ data-flat-footer-record="true"
53
+ aria-label={recordTitle}
54
+ title={recordTitle}
55
+ onMouseDown={(e) => e.preventDefault()}
56
+ onClick={() => {
57
+ track("button", "Gesture recording");
58
+ onToggleRecording();
59
+ }}
60
+ className={recording ? "text-panel-danger animate-pulse" : "text-panel-danger"}
61
+ >
62
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
63
+ {recording ? (
64
+ <rect x="2" y="2" width="10" height="10" rx="1.5" />
65
+ ) : (
66
+ <circle cx="7" cy="7" r="6" />
67
+ )}
68
+ </svg>
69
+ </button>
70
+ )}
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,79 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
7
+
8
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
9
+
10
+ afterEach(() => {
11
+ document.body.innerHTML = "";
12
+ });
13
+
14
+ function renderHeader(overrides: Partial<Parameters<typeof PropertyPanelFlatHeader>[0]> = {}) {
15
+ const host = document.createElement("div");
16
+ document.body.append(host);
17
+ const root = createRoot(host);
18
+ const props = {
19
+ name: "Mono Label",
20
+ meta: ".mono-label · div",
21
+ elementKind: "text" as const,
22
+ hidden: false,
23
+ copied: false,
24
+ onCopy: vi.fn(),
25
+ onClear: vi.fn(),
26
+ showUngroup: false,
27
+ ...overrides,
28
+ };
29
+ act(() => {
30
+ root.render(<PropertyPanelFlatHeader {...props} />);
31
+ });
32
+ return { host, root, props };
33
+ }
34
+
35
+ describe("PropertyPanelFlatHeader", () => {
36
+ it("renders name, meta, and the mint text-type icon", () => {
37
+ const { host, root } = renderHeader();
38
+ expect(host.textContent).toContain("Mono Label");
39
+ expect(host.textContent).toContain(".mono-label · div");
40
+ const icon = host.querySelector('[data-flat-header-icon="true"]');
41
+ expect(icon?.className).toContain("text-panel-accent");
42
+ act(() => root.unmount());
43
+ });
44
+
45
+ it("colors the media icon cyan and the other icon amber", () => {
46
+ const { host: mediaHost, root: mediaRoot } = renderHeader({ elementKind: "media" });
47
+ expect(mediaHost.querySelector('[data-flat-header-icon="true"]')?.className).toContain(
48
+ "text-panel-media",
49
+ );
50
+ act(() => mediaRoot.unmount());
51
+
52
+ const { host: otherHost, root: otherRoot } = renderHeader({ elementKind: "other" });
53
+ expect(otherHost.querySelector('[data-flat-header-icon="true"]')?.className).toContain(
54
+ "text-panel-container",
55
+ );
56
+ act(() => otherRoot.unmount());
57
+ });
58
+
59
+ it("fires onCopy and onClear from their action buttons", () => {
60
+ const { host, root, props } = renderHeader();
61
+ const copy = host.querySelector<HTMLButtonElement>(
62
+ '[aria-label="Copy element info to clipboard"]',
63
+ );
64
+ const clear = host.querySelector<HTMLButtonElement>('[aria-label="Clear selection"]');
65
+ act(() => copy?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
66
+ act(() => clear?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
67
+ expect(props.onCopy).toHaveBeenCalledTimes(1);
68
+ expect(props.onClear).toHaveBeenCalledTimes(1);
69
+ act(() => root.unmount());
70
+ });
71
+
72
+ it("only renders Ungroup when showUngroup is true", () => {
73
+ const { host: without } = renderHeader({ showUngroup: false });
74
+ expect(without.querySelector('[aria-label="Ungroup"]')).toBeNull();
75
+
76
+ const { host: withUngroup } = renderHeader({ showUngroup: true, onUngroup: vi.fn() });
77
+ expect(withUngroup.querySelector('[aria-label="Ungroup"]')).not.toBeNull();
78
+ });
79
+ });
@@ -0,0 +1,112 @@
1
+ import { Eye, EyeSlash } from "@phosphor-icons/react";
2
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
3
+ import { ClipboardList, Film, Square, Type, X } from "../../icons/SystemIcons";
4
+
5
+ const ICON_BY_KIND = { text: Type, media: Film, other: Square } as const;
6
+ const ICON_COLOR_BY_KIND = {
7
+ text: "text-panel-accent",
8
+ media: "text-panel-media",
9
+ other: "text-panel-container",
10
+ } as const;
11
+
12
+ export function PropertyPanelFlatHeader({
13
+ name,
14
+ meta,
15
+ elementKind,
16
+ hidden,
17
+ onToggleHidden,
18
+ copied,
19
+ onCopy,
20
+ onClear,
21
+ onUngroup,
22
+ showUngroup,
23
+ }: {
24
+ name: string;
25
+ meta: string;
26
+ elementKind: "text" | "media" | "other";
27
+ hidden: boolean;
28
+ onToggleHidden?: () => void;
29
+ copied: boolean;
30
+ onCopy: () => void;
31
+ onClear: () => void;
32
+ onUngroup?: () => void;
33
+ showUngroup: boolean;
34
+ }) {
35
+ const track = useTrackDesignInput();
36
+ const Icon = ICON_BY_KIND[elementKind];
37
+ const visibilityLabel = hidden ? "Show element" : "Hide element";
38
+
39
+ return (
40
+ <div className="flex items-center gap-2.5 border-b border-panel-hairline px-4 py-3">
41
+ <Icon
42
+ size={15}
43
+ data-flat-header-icon="true"
44
+ className={`flex-shrink-0 ${ICON_COLOR_BY_KIND[elementKind]}`}
45
+ />
46
+ <div className="flex min-w-0 flex-1 items-baseline gap-2">
47
+ <span className="truncate text-[13px] font-semibold text-panel-text-0">{name}</span>
48
+ <span className="truncate font-mono text-[10px] text-panel-text-4">{meta}</span>
49
+ </div>
50
+ <div className="flex flex-shrink-0 items-center gap-2.5 text-panel-text-3">
51
+ {showUngroup && (
52
+ <button
53
+ type="button"
54
+ aria-label="Ungroup"
55
+ title="Ungroup (⌘⇧G)"
56
+ onClick={() => {
57
+ track("button", "Ungroup");
58
+ onUngroup?.();
59
+ }}
60
+ >
61
+ <svg
62
+ width="13"
63
+ height="13"
64
+ viewBox="0 0 16 16"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ strokeWidth="1.5"
68
+ >
69
+ <rect x="1.5" y="1.5" width="7" height="7" rx="1" />
70
+ <rect x="7.5" y="7.5" width="7" height="7" rx="1" />
71
+ </svg>
72
+ </button>
73
+ )}
74
+ {onToggleHidden && (
75
+ <button
76
+ type="button"
77
+ aria-label={visibilityLabel}
78
+ title={visibilityLabel}
79
+ onClick={() => {
80
+ track("toggle", "Element visibility");
81
+ onToggleHidden();
82
+ }}
83
+ >
84
+ {hidden ? <EyeSlash size={13} weight="bold" /> : <Eye size={13} weight="bold" />}
85
+ </button>
86
+ )}
87
+ <button
88
+ type="button"
89
+ aria-label="Copy element info to clipboard"
90
+ title={copied ? "Copied!" : "Copy element info for any AI agent"}
91
+ onClick={() => {
92
+ track("button", "Copy element info");
93
+ onCopy();
94
+ }}
95
+ className={copied ? "text-panel-accent" : undefined}
96
+ >
97
+ <ClipboardList size={13} />
98
+ </button>
99
+ <button
100
+ type="button"
101
+ aria-label="Clear selection"
102
+ onClick={() => {
103
+ track("button", "Clear selection");
104
+ onClear();
105
+ }}
106
+ >
107
+ <X size={13} />
108
+ </button>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
@@ -34,3 +34,30 @@ export interface GsapAnimationEditCallbacks {
34
34
  /** Unroll a computed (helper/loop) tween into literal tweens so it edits directly. */
35
35
  onUnroll?: (animationId: string) => void;
36
36
  }
37
+
38
+ // User-facing control label for each animation-meta field. The ease control is
39
+ // labelled "Speed" in the card UI, so ease/easeEach map there.
40
+ const ANIMATION_META_LABELS: Record<string, { control: string; name: string }> = {
41
+ duration: { control: "metric", name: "Length" },
42
+ position: { control: "metric", name: "Starts at" },
43
+ ease: { control: "select", name: "Speed" },
44
+ easeEach: { control: "select", name: "Speed" },
45
+ };
46
+
47
+ /**
48
+ * Emit design-input telemetry for an `onUpdateMeta` payload, attributing each
49
+ * changed field to the control the user actually touched. Iterates the real keys
50
+ * present rather than falling through to a single placeholder — so a meta field
51
+ * added later is attributed honestly by its own key instead of poisoning another
52
+ * control's usage count.
53
+ */
54
+ export function trackAnimationMetaUpdate(
55
+ track: (control: string, name: string) => void,
56
+ updates: Record<string, unknown>,
57
+ ): void {
58
+ for (const key of Object.keys(updates)) {
59
+ const mapped = ANIMATION_META_LABELS[key];
60
+ if (mapped) track(mapped.control, mapped.name);
61
+ else track("select", key);
62
+ }
63
+ }
@@ -0,0 +1,34 @@
1
+ import type { DomEditSelection } from "./domEditingTypes";
2
+
3
+ /**
4
+ * Build the "live preview" callback the 3D-transform sub-view fires while a
5
+ * value is being dragged: apply a gsap.set() to the matching node inside the
6
+ * preview iframe so the edit is reflected immediately, before it's committed.
7
+ *
8
+ * Extracted so the identical closure exists once — shared by the legacy
9
+ * PropertyPanel Layout section and the flat Layout group (PropertyPanelFlat).
10
+ */
11
+ // Resolve by id when unique, otherwise by selector + selectorIndex — a bare
12
+ // querySelector(selector) always hits the FIRST match, so dragging on the
13
+ // second of two same-selector siblings would animate the wrong element.
14
+ function resolvePreviewNode(
15
+ doc: Document | null | undefined,
16
+ el: DomEditSelection,
17
+ ): Element | null {
18
+ if (!doc) return null;
19
+ if (el.id) return doc.querySelector(`#${el.id}`);
20
+ if (!el.selector) return null;
21
+ return doc.querySelectorAll(el.selector)[el.selectorIndex ?? 0] ?? null;
22
+ }
23
+
24
+ export function createGsapLivePreview(iframeRef: { readonly current: HTMLIFrameElement | null }) {
25
+ return (el: DomEditSelection, props: Record<string, number>) => {
26
+ const iframe = iframeRef.current;
27
+ const win = iframe?.contentWindow as
28
+ | { gsap?: { set: (t: Element, v: Record<string, number>) => void } }
29
+ | null
30
+ | undefined;
31
+ const node = resolvePreviewNode(iframe?.contentDocument, el);
32
+ if (win?.gsap && node) win.gsap.set(node, props);
33
+ };
34
+ }
@@ -105,4 +105,14 @@ describe("manual editing availability", () => {
105
105
  expect(resolveStudioBooleanEnvFlag({ EMPTY: "" }, ["EMPTY"], true)).toBe(true);
106
106
  expect(resolveStudioBooleanEnvFlag({ UNKNOWN: "maybe" }, ["UNKNOWN"], false)).toBe(false);
107
107
  });
108
+
109
+ it("defaults the flat inspector flag to false and honors an explicit override", async () => {
110
+ const off = await loadAvailabilityWithEnv({});
111
+ expect(off.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(false);
112
+
113
+ const on = await loadAvailabilityWithEnv({
114
+ VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "true",
115
+ });
116
+ expect(on.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(true);
117
+ });
108
118
  });
@@ -97,4 +97,13 @@ export const STUDIO_SDK_RESOLVER_SHADOW_ENABLED = resolveStudioBooleanEnvFlag(
97
97
  true,
98
98
  );
99
99
 
100
+ // Studio inspector redesign ("Ledger, flat" — design_handoff_studio_inspector):
101
+ // flat identity header/footer/groups behind a flag for incremental review.
102
+ // Default false; enable via VITE_STUDIO_FLAT_INSPECTOR_ENABLED=true.
103
+ export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
104
+ env,
105
+ ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
106
+ false,
107
+ );
108
+
100
109
  export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
@@ -5,6 +5,7 @@ import { MetricField } from "./propertyPanelPrimitives";
5
5
  import { KeyframeNavigation } from "./KeyframeNavigation";
6
6
  import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
7
7
  import { Transform3DCube, type CubePose } from "./Transform3DCube";
8
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
8
9
 
9
10
  // translateZ only foreshortens under a perspective lens. Rather than hardcode one
10
11
  // (an arbitrary px value reads wrong at different canvas sizes), derive it from the
@@ -71,6 +72,7 @@ function Cube3dControl({
71
72
  onKeyframe?: () => void;
72
73
  keyframed?: boolean;
73
74
  }) {
75
+ const track = useTrackDesignInput();
74
76
  const pose: CubePose = {
75
77
  rotationX: gsapRuntimeValues.rotationX ?? 0,
76
78
  rotationY: gsapRuntimeValues.rotationY ?? 0,
@@ -96,6 +98,7 @@ function Cube3dControl({
96
98
  }
97
99
  const axes = Object.keys(changedProps);
98
100
  if (axes.length === 0) return;
101
+ track("slider", "3D rotation pose");
99
102
  // ONE keyframe for the whole pose change — avoids per-axis commits racing into
100
103
  // adjacent duplicate keyframes.
101
104
  void onCommitAnimatedProperties(element, changedProps);
@@ -111,6 +114,7 @@ function Cube3dControl({
111
114
  scale: 1,
112
115
  transformPerspective: 0,
113
116
  };
117
+ track("button", "Reset 3D transform");
114
118
  void onCommitAnimatedProperties(element, identity);
115
119
  };
116
120
  // Immediate element feedback while dragging — set the live transform without a
@@ -168,6 +172,7 @@ function Cube3dControl({
168
172
  }
169
173
  // One commit for all props so the writes can't race read-modify-write on
170
174
  // the same script (which dropped a prop and reverted after a seek).
175
+ track("slider", "3D depth");
171
176
  void onCommitAnimatedProperties(element, props);
172
177
  }}
173
178
  onRecenter={recenter}
@@ -0,0 +1,28 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { ColorField } from "./propertyPanelColor";
7
+
8
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
9
+
10
+ afterEach(() => {
11
+ document.body.innerHTML = "";
12
+ });
13
+
14
+ describe("ColorField flat trigger", () => {
15
+ it("renders label and value inline with a small swatch, no boxed border", () => {
16
+ const host = document.createElement("div");
17
+ document.body.append(host);
18
+ const root = createRoot(host);
19
+ act(() => {
20
+ root.render(<ColorField flat label="Color" value="rgb(255, 176, 32)" onCommit={vi.fn()} />);
21
+ });
22
+ const trigger = host.querySelector<HTMLButtonElement>('[data-flat-color-trigger="true"]');
23
+ expect(trigger).not.toBeNull();
24
+ expect(trigger?.className).not.toContain("border-neutral-800");
25
+ expect(host.textContent).toContain("Color");
26
+ act(() => root.unmount());
27
+ });
28
+ });
@@ -11,6 +11,7 @@ import {
11
11
  } from "./colorValue";
12
12
  import { resolveFloatingPanelPosition, type FloatingPosition } from "./floatingPanel";
13
13
  import { colorFromCss, FIELD, LABEL } from "./propertyPanelHelpers";
14
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
14
15
 
15
16
  const COLOR_PICKER_SIZE = { width: 292, height: 386 };
16
17
 
@@ -121,13 +122,16 @@ export function ColorField({
121
122
  label,
122
123
  value,
123
124
  disabled,
125
+ flat,
124
126
  onCommit,
125
127
  }: {
126
128
  label: string;
127
129
  value: string;
128
130
  disabled?: boolean;
131
+ flat?: boolean;
129
132
  onCommit: (nextValue: string) => void;
130
133
  }) {
134
+ const track = useTrackDesignInput();
131
135
  const buttonRef = useRef<HTMLButtonElement | null>(null);
132
136
  const panelRef = useRef<HTMLDivElement | null>(null);
133
137
  const [open, setOpen] = useState(false);
@@ -201,7 +205,9 @@ export function ColorField({
201
205
  const commitColor = (nextColor: ParsedColor) => {
202
206
  setDraftColor(nextColor);
203
207
  setHexDraft(toHexColor(nextColor).toUpperCase());
204
- onCommit(formatCssColor(nextColor));
208
+ const nextValue = formatCssColor(nextColor);
209
+ if (nextValue !== value) track("color", label);
210
+ onCommit(nextValue);
205
211
  };
206
212
 
207
213
  const commitHsv = (nextHsv: { hue?: number; saturation?: number; value?: number }) => {
@@ -349,6 +355,30 @@ export function ColorField({
349
355
  }
350
356
  };
351
357
 
358
+ if (flat) {
359
+ return (
360
+ <div className="flex min-h-[30px] items-center justify-between">
361
+ <span className="text-[11px] text-panel-text-2">{label}</span>
362
+ <button
363
+ type="button"
364
+ data-flat-color-trigger="true"
365
+ disabled={disabled}
366
+ aria-label={`Pick ${label.toLowerCase()} color`}
367
+ ref={buttonRef}
368
+ onClick={openPicker}
369
+ className="flex items-center gap-2 disabled:cursor-not-allowed"
370
+ >
371
+ <span
372
+ className="h-4 w-4 flex-shrink-0 rounded-[4px]"
373
+ style={{ backgroundColor: value || "transparent" }}
374
+ />
375
+ <span className="font-mono text-[11px] text-panel-text-0">{value}</span>
376
+ </button>
377
+ {picker}
378
+ </div>
379
+ );
380
+ }
381
+
352
382
  return (
353
383
  <div className="grid min-w-0 gap-1.5">
354
384
  <span className={LABEL}>{label}</span>
@@ -11,6 +11,7 @@ import { ChevronDown, ChevronRight, Plus, X } from "../../icons/SystemIcons";
11
11
  import { LUT_EXT } from "../../utils/mediaTypes";
12
12
  import { LABEL } from "./propertyPanelHelpers";
13
13
  import { ColorGradingSliderControl } from "./propertyPanelColorGradingSlider";
14
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
14
15
 
15
16
  const LUT_UPLOAD_DIR = "assets/luts";
16
17
 
@@ -175,6 +176,7 @@ export function ColorGradingControls({
175
176
  onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
176
177
  onCommitColorGrading: (nextGrading: NormalizedHfColorGrading) => void;
177
178
  }) {
179
+ const track = useTrackDesignInput();
178
180
  const lutInputRef = useRef<HTMLInputElement>(null);
179
181
  const [lutOpen, setLutOpen] = useState(false);
180
182
  const [detailSettings, setDetailSettings] = useState<"vignette" | "grain" | null>(null);
@@ -195,7 +197,10 @@ export function ColorGradingControls({
195
197
 
196
198
  const applyPreset = (preset: string) => {
197
199
  const next = normalizeHfColorGrading({ preset, intensity: 1, lut: grading.lut });
198
- if (next) onCommitColorGrading(next);
200
+ if (next) {
201
+ track("select", "Preset");
202
+ onCommitColorGrading(next);
203
+ }
199
204
  };
200
205
  const updateFilterIntensity = (value: number) => {
201
206
  onCommitColorGrading({
@@ -218,7 +223,10 @@ export function ColorGradingControls({
218
223
  if (!files?.length || !onImportAssets) return;
219
224
  const uploaded = await onImportAssets(files, LUT_UPLOAD_DIR);
220
225
  const firstLut = uploaded.find((asset) => LUT_EXT.test(asset));
221
- if (firstLut) applyLut(firstLut, 1);
226
+ if (firstLut) {
227
+ track("button", "Import LUT");
228
+ applyLut(firstLut, 1);
229
+ }
222
230
  };
223
231
  const commitDetailSlider = (slider: DetailSlider, next: number) => {
224
232
  onCommitColorGrading({
@@ -313,6 +321,7 @@ export function ColorGradingControls({
313
321
  value={selectedLut}
314
322
  onChange={(event) => {
315
323
  const nextSrc = event.target.value;
324
+ track("select", "Custom LUT");
316
325
  applyLut(
317
326
  nextSrc || null,
318
327
  nextSrc && grading.lut?.src === nextSrc ? grading.lut.intensity : 1,