@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
@@ -1,10 +1,10 @@
1
1
  import { memo, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Move } from "../../icons/SystemIcons";
3
- import { Eye, EyeSlash } from "@phosphor-icons/react";
4
3
  import { InspectorHeaderActions } from "./InspectorHeaderActions";
5
4
  import { useStudioShellContext } from "../../contexts/StudioContext";
6
5
  import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
7
6
  import {
7
+ buildElementInfoText,
8
8
  EMPTY_STYLES,
9
9
  formatPxMetricValue,
10
10
  parsePxMetricValue,
@@ -12,6 +12,7 @@ import {
12
12
  readGsapRuntimeValuesForPanel,
13
13
  readGsapBorderRadiusForPanel,
14
14
  isSelectedElementHidden,
15
+ selectionIdentityKey,
15
16
  } from "./propertyPanelHelpers";
16
17
  import { MetricField, Section } from "./propertyPanelPrimitives";
17
18
  import { createTransformCommitHandlers } from "./propertyPanelTransformCommit";
@@ -24,12 +25,19 @@ import { TextSection, StyleSections } from "./propertyPanelSections";
24
25
  import { GsapAnimationSection } from "./GsapAnimationSection";
25
26
  import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
26
27
  import { KeyframeNavigation } from "./KeyframeNavigation";
27
- import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
28
+ import {
29
+ STUDIO_FLAT_INSPECTOR_ENABLED,
30
+ STUDIO_GSAP_PANEL_ENABLED,
31
+ STUDIO_KEYFRAMES_ENABLED,
32
+ } from "./manualEditingAvailability";
33
+ import { PropertyPanelFlat } from "./PropertyPanelFlat";
34
+ import { createGsapLivePreview } from "./gsapLivePreview";
28
35
  import { usePlayerStore, liveTime } from "../../player";
29
36
  import { TimingSection } from "./propertyPanelTimingSection";
30
37
  import { type PropertyPanelProps } from "./propertyPanelHelpers";
31
38
  import { GestureRecordPanelButton } from "./GestureRecordControl";
32
39
  import { PropertyPanelEmptyState } from "./PropertyPanelEmptyState";
40
+ import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
33
41
 
34
42
  // Re-export helpers that external consumers import from this module
35
43
  export {
@@ -46,61 +54,64 @@ export {
46
54
  } from "./propertyPanelHelpers";
47
55
 
48
56
  // fallow-ignore-next-line complexity
49
- export const PropertyPanel = memo(function PropertyPanel({
50
- projectId,
51
- projectDir,
52
- assets,
53
- element,
54
- multiSelectCount = 0,
55
- copiedAgentPrompt: _copiedAgentPrompt,
56
- onClearSelection,
57
- onUngroup,
58
- onSetStyle,
59
- onSetAttribute,
60
- onSetAttributeLive,
61
- onApplyColorGradingScope,
62
- onSetHtmlAttribute,
63
- onRemoveBackground,
64
- onSetManualOffset,
65
- onSetManualSize,
66
- onSetManualRotation,
67
- onSetText,
68
- onSetTextFieldStyle,
69
- onAddTextField,
70
- onRemoveTextField,
71
- onAskAgent: _onAskAgent,
72
- onToggleElementHidden,
73
- onImportAssets,
74
- fontAssets = [],
75
- onImportFonts,
76
- previewIframeRef,
77
- gsapAnimations = [],
78
- gsapMultipleTimelines,
79
- gsapUnsupportedTimelinePattern,
80
- onUpdateGsapProperty,
81
- onUpdateGsapMeta,
82
- onDeleteGsapAnimation,
83
- onAddGsapProperty,
84
- onRemoveGsapProperty,
85
- onUpdateGsapFromProperty,
86
- onAddGsapFromProperty,
87
- onRemoveGsapFromProperty,
88
- onAddGsapAnimation,
89
- onSetArcPath,
90
- onUpdateArcSegment,
91
- onUnroll,
92
- onUpdateKeyframeEase,
93
- onSetAllKeyframeEases,
94
- onAddKeyframe,
95
- onRemoveKeyframe,
96
- onConvertToKeyframes,
97
- onCommitAnimatedProperty,
98
- onCommitAnimatedProperties,
99
- onSeekToTime,
100
- recordingState,
101
- recordingDuration,
102
- onToggleRecording,
103
- }: PropertyPanelProps) {
57
+ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelProps) {
58
+ const {
59
+ projectId,
60
+ projectDir,
61
+ assets,
62
+ element,
63
+ multiSelectCount = 0,
64
+ multiSelectedElements,
65
+ onGroupSelection,
66
+ onHideAllSelected,
67
+ copiedAgentPrompt: _copiedAgentPrompt,
68
+ onClearSelection,
69
+ onUngroup,
70
+ onSetStyle,
71
+ onSetAttribute,
72
+ onSetAttributeLive,
73
+ onApplyColorGradingScope,
74
+ onSetHtmlAttribute,
75
+ onRemoveBackground,
76
+ onSetManualOffset,
77
+ onSetManualSize,
78
+ onSetManualRotation,
79
+ onSetText,
80
+ onSetTextFieldStyle,
81
+ onAddTextField,
82
+ onRemoveTextField,
83
+ onToggleElementHidden,
84
+ onImportAssets,
85
+ fontAssets = [],
86
+ onImportFonts,
87
+ previewIframeRef,
88
+ gsapAnimations = [],
89
+ gsapMultipleTimelines,
90
+ gsapUnsupportedTimelinePattern,
91
+ onUpdateGsapProperty,
92
+ onUpdateGsapMeta,
93
+ onDeleteGsapAnimation,
94
+ onAddGsapProperty,
95
+ onRemoveGsapProperty,
96
+ onUpdateGsapFromProperty,
97
+ onAddGsapFromProperty,
98
+ onRemoveGsapFromProperty,
99
+ onAddGsapAnimation,
100
+ onSetArcPath,
101
+ onUpdateArcSegment,
102
+ onUnroll,
103
+ onUpdateKeyframeEase,
104
+ onSetAllKeyframeEases,
105
+ onAddKeyframe,
106
+ onRemoveKeyframe,
107
+ onConvertToKeyframes,
108
+ onCommitAnimatedProperty,
109
+ onCommitAnimatedProperties,
110
+ onSeekToTime,
111
+ recordingState,
112
+ recordingDuration,
113
+ onToggleRecording,
114
+ } = props;
104
115
  const styles = element?.computedStyles ?? EMPTY_STYLES;
105
116
  const { showToast } = useStudioShellContext();
106
117
  const [clipboardCopied, setClipboardCopied] = useState(false);
@@ -170,13 +181,22 @@ export const PropertyPanel = memo(function PropertyPanel({
170
181
  };
171
182
 
172
183
  if (!element) {
173
- return <PropertyPanelEmptyState multiSelectCount={multiSelectCount} />;
184
+ return (
185
+ <PropertyPanelEmptyState
186
+ flat={STUDIO_FLAT_INSPECTOR_ENABLED}
187
+ multiSelectCount={multiSelectCount}
188
+ multiSelectedElements={multiSelectedElements}
189
+ onGroupSelection={onGroupSelection}
190
+ onHideAllSelected={onHideAllSelected}
191
+ onClearSelection={onClearSelection}
192
+ />
193
+ );
174
194
  }
175
195
 
176
196
  const manualOffsetEditingDisabled = !element.capabilities.canApplyManualOffset;
177
197
  const manualSizeEditingDisabled = !element.capabilities.canApplyManualSize;
178
198
  const manualRotationEditingDisabled = !element.capabilities.canApplyManualRotation;
179
- const sourceLabel = element.id ? `#${element.id}` : element.selector;
199
+ const sourceLabel = element.id ? `#${element.id}` : (element.selector ?? "");
180
200
  const showEditableSections = element.capabilities.canEditStyles;
181
201
  // Capabilities are already resolved on the selection; recompute only sections,
182
202
  // feeding the live GSAP tween count (arrives on the gsapAnimations prop, not the
@@ -234,48 +254,8 @@ export const PropertyPanel = memo(function PropertyPanel({
234
254
  const displayH = gsapRuntimeValues?.height ?? resolvedHeight;
235
255
  const displayR = gsapRuntimeValues?.rotation ?? manualRotation.angle;
236
256
 
237
- // fallow-ignore-next-line complexity
238
257
  const handleCopyElementInfo = () => {
239
- const file = element.sourceFile ?? "index.html";
240
- let lineNum: number | null = null;
241
- try {
242
- const src = previewIframeRef?.current?.contentDocument?.documentElement?.outerHTML ?? "";
243
- if (src && element.id) {
244
- const idx = src.indexOf(`id="${element.id}"`);
245
- if (idx > -1) lineNum = src.slice(0, idx).split("\n").length;
246
- }
247
- if (!lineNum && element.selector) {
248
- const tag = element.tagName.toLowerCase();
249
- const cls = element.selector.startsWith(".")
250
- ? element.selector.slice(1).split(".")[0]
251
- : null;
252
- const search = cls ? `class="${cls}` : `<${tag}`;
253
- const idx = src.indexOf(search);
254
- if (idx > -1) lineNum = src.slice(0, idx).split("\n").length;
255
- }
256
- } catch {}
257
- const fileLoc = lineNum ? `${file}:${lineNum}` : file;
258
- const lines = [
259
- `Element: ${element.label} (${sourceLabel})`,
260
- `File: ${fileLoc}`,
261
- `Position: x=${Math.round(element.boundingBox.x)}, y=${Math.round(element.boundingBox.y)}`,
262
- `Size: ${Math.round(element.boundingBox.width)}×${Math.round(element.boundingBox.height)}`,
263
- `Tag: <${element.tagName}>`,
264
- ];
265
- if (element.computedStyles["z-index"] && element.computedStyles["z-index"] !== "auto") {
266
- lines.push(`Z-index: ${element.computedStyles["z-index"]}`);
267
- }
268
- if (gsapAnimations.length > 0) {
269
- const anim = gsapAnimations[0];
270
- lines.push(
271
- `Animation: ${anim.method}() ${anim.duration}s at ${anim.position}s, ease: ${anim.ease ?? "default"}`,
272
- );
273
- const props = Object.entries(anim.properties)
274
- .map(([k, v]) => `${k}: ${v}`)
275
- .join(", ");
276
- if (props) lines.push(`Properties: ${props}`);
277
- }
278
- const text = lines.join("\n");
258
+ const text = buildElementInfoText(element, sourceLabel, gsapAnimations, previewIframeRef);
279
259
  void navigator.clipboard.writeText(text);
280
260
  showToast(`Copied element info for ${element.label} — paste into any AI agent`, "info");
281
261
  setClipboardCopied(true);
@@ -283,51 +263,80 @@ export const PropertyPanel = memo(function PropertyPanel({
283
263
  clipboardTimerRef.current = setTimeout(() => setClipboardCopied(false), 1500);
284
264
  };
285
265
 
286
- return (
266
+ if (STUDIO_FLAT_INSPECTOR_ENABLED) {
267
+ // Forward the raw props (handlers, ids, assets, recording, fonts, etc.) and
268
+ // the values the legacy path already computed above (so they aren't derived
269
+ // twice). PropertyPanelFlat owns the one-open group state.
270
+ return (
271
+ <PropertyPanelFlat
272
+ {...props}
273
+ key={selectionIdentityKey(element)}
274
+ element={element}
275
+ styles={styles}
276
+ sections={sections}
277
+ sourceLabel={sourceLabel}
278
+ gsapBorderRadius={gsapBorderRadius}
279
+ showEditableSections={showEditableSections}
280
+ selectedElementHidden={selectedElementHidden}
281
+ selectedElementId={selectedElementId}
282
+ clipboardCopied={clipboardCopied}
283
+ onCopyElementInfo={handleCopyElementInfo}
284
+ displayX={displayX}
285
+ displayY={displayY}
286
+ displayW={displayW}
287
+ displayH={displayH}
288
+ displayR={displayR}
289
+ manualOffsetEditingDisabled={manualOffsetEditingDisabled}
290
+ manualSizeEditingDisabled={manualSizeEditingDisabled}
291
+ manualRotationEditingDisabled={manualRotationEditingDisabled}
292
+ commitManualOffset={commitManualOffset}
293
+ commitManualSize={commitManualSize}
294
+ commitManualRotation={commitManualRotation}
295
+ gsapAnimId={gsapAnimId}
296
+ navKeyframes={navKeyframes}
297
+ currentTime={currentTime}
298
+ animIdForProp={animIdForProp}
299
+ gsapRuntimeValues={gsap3dValues}
300
+ elStart={elStart}
301
+ elDuration={elDuration}
302
+ />
303
+ );
304
+ }
305
+
306
+ const classicPanel = (
287
307
  <div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
288
- <div className="px-4 py-3">
289
- <div className="flex items-center justify-between gap-4">
290
- <div className="min-w-0">
291
- <div className="truncate text-[13px] font-semibold text-neutral-100">
292
- {element.label}
308
+ <DesignPanelInputProvider section="header">
309
+ <div className="px-4 py-3">
310
+ <div className="flex items-center justify-between gap-4">
311
+ <div className="min-w-0">
312
+ <div className="truncate text-[13px] font-semibold text-neutral-100">
313
+ {element.label}
314
+ </div>
315
+ <div className="mt-0.5 truncate text-[11px] text-neutral-500">{sourceLabel}</div>
293
316
  </div>
294
- <div className="mt-0.5 truncate text-[11px] text-neutral-500">{sourceLabel}</div>
295
- </div>
296
- <div className="flex items-center gap-1">
297
- {selectedElementId && onToggleElementHidden && (
298
- <button
299
- type="button"
300
- aria-label={visibilityToggleLabel}
301
- title={visibilityToggleLabel}
302
- onClick={() => {
303
- void onToggleElementHidden(selectedElementId, !selectedElementHidden);
304
- }}
305
- className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
306
- >
307
- {selectedElementHidden ? (
308
- <EyeSlash size={13} weight="bold" aria-hidden="true" />
309
- ) : (
310
- <Eye size={13} weight="bold" aria-hidden="true" />
311
- )}
312
- </button>
313
- )}
314
317
  <InspectorHeaderActions
315
318
  element={element}
316
319
  copied={clipboardCopied}
317
320
  onCopy={handleCopyElementInfo}
318
321
  onClear={onClearSelection}
319
322
  onUngroup={onUngroup}
323
+ selectedElementId={selectedElementId}
324
+ selectedElementHidden={selectedElementHidden}
325
+ visibilityLabel={visibilityToggleLabel}
326
+ onToggleHidden={onToggleElementHidden}
320
327
  />
321
328
  </div>
322
329
  </div>
323
- </div>
330
+ </DesignPanelInputProvider>
324
331
  <div className="flex-1 overflow-y-auto">
325
332
  {onToggleRecording && (
326
- <GestureRecordPanelButton
327
- recordingState={recordingState}
328
- recordingDuration={recordingDuration}
329
- onToggleRecording={onToggleRecording}
330
- />
333
+ <DesignPanelInputProvider section="footer">
334
+ <GestureRecordPanelButton
335
+ recordingState={recordingState}
336
+ recordingDuration={recordingDuration}
337
+ onToggleRecording={onToggleRecording}
338
+ />
339
+ </DesignPanelInputProvider>
331
340
  )}
332
341
 
333
342
  <TextSection
@@ -353,12 +362,7 @@ export const PropertyPanel = memo(function PropertyPanel({
353
362
  )}
354
363
  {sections.colorGrading && (
355
364
  <ColorGradingSection
356
- key={[
357
- element.id ?? "",
358
- element.hfId ?? "",
359
- element.selector ?? "",
360
- String(element.selectorIndex ?? ""),
361
- ].join("|")}
365
+ key={selectionIdentityKey(element)}
362
366
  projectId={projectId}
363
367
  element={element}
364
368
  assets={assets}
@@ -522,16 +526,7 @@ export const PropertyPanel = memo(function PropertyPanel({
522
526
  onSeekToTime={onSeekToTime}
523
527
  onRemoveKeyframe={onRemoveKeyframe}
524
528
  onConvertToKeyframes={onConvertToKeyframes}
525
- onLivePreviewProps={(el, props) => {
526
- const iframe = iframeRef.current;
527
- const win = iframe?.contentWindow as
528
- | { gsap?: { set: (t: Element, v: Record<string, number>) => void } }
529
- | null
530
- | undefined;
531
- const sel = el.id ? `#${el.id}` : el.selector;
532
- const node = sel ? iframe?.contentDocument?.querySelector(sel) : null;
533
- if (win?.gsap && node) win.gsap.set(node, props);
534
- }}
529
+ onLivePreviewProps={createGsapLivePreview(iframeRef)}
535
530
  />
536
531
  <div className="mt-3">
537
532
  <div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
@@ -587,4 +582,5 @@ export const PropertyPanel = memo(function PropertyPanel({
587
582
  </div>
588
583
  </div>
589
584
  );
585
+ return <DesignPanelInputProvider ui="classic">{classicPanel}</DesignPanelInputProvider>;
590
586
  });
@@ -0,0 +1,74 @@
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 { PropertyPanelEmptyState } from "./PropertyPanelEmptyState";
7
+ import type { DomEditSelection } from "./domEditingTypes";
8
+
9
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
10
+
11
+ afterEach(() => {
12
+ document.body.innerHTML = "";
13
+ });
14
+
15
+ function renderInto(node: React.ReactElement) {
16
+ const host = document.createElement("div");
17
+ document.body.append(host);
18
+ const root = createRoot(host);
19
+ act(() => {
20
+ root.render(node);
21
+ });
22
+ return { host, root };
23
+ }
24
+
25
+ describe("PropertyPanelEmptyState — flat empty", () => {
26
+ it("shows the cursor glyph, headline, and the two shortcut rows", () => {
27
+ const { host, root } = renderInto(<PropertyPanelEmptyState flat multiSelectCount={0} />);
28
+ expect(host.textContent).toContain("Nothing selected");
29
+ expect(host.textContent).toContain("Record a gesture");
30
+ expect(host.textContent).toContain("Describe a change to the agent");
31
+ act(() => root.unmount());
32
+ });
33
+ });
34
+
35
+ describe("PropertyPanelEmptyState — flat multi-select", () => {
36
+ const elements = [
37
+ { id: "mono-label", selector: ".mono-label", label: "Mono Label", tagName: "div" },
38
+ { id: null, selector: "#s2-chart", label: "S2 Chart", tagName: "div" },
39
+ ] as unknown as DomEditSelection[];
40
+
41
+ it("lists each selected element and wires group/hide-all/clear actions", () => {
42
+ const onGroupSelection = vi.fn();
43
+ const onHideAllSelected = vi.fn();
44
+ const onClearSelection = vi.fn();
45
+ const { host, root } = renderInto(
46
+ <PropertyPanelEmptyState
47
+ flat
48
+ multiSelectCount={2}
49
+ multiSelectedElements={elements}
50
+ onGroupSelection={onGroupSelection}
51
+ onHideAllSelected={onHideAllSelected}
52
+ onClearSelection={onClearSelection}
53
+ />,
54
+ );
55
+ expect(host.textContent).toContain("2 elements selected");
56
+ expect(host.textContent).toContain("Mono Label");
57
+ expect(host.textContent).toContain("S2 Chart");
58
+
59
+ const group = host.querySelector<HTMLButtonElement>('[data-flat-multiselect-group="true"]');
60
+ act(() => group?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
61
+ expect(onGroupSelection).toHaveBeenCalledTimes(1);
62
+
63
+ const hideAll = host.querySelector<HTMLButtonElement>(
64
+ '[data-flat-multiselect-hide-all="true"]',
65
+ );
66
+ act(() => hideAll?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
67
+ expect(onHideAllSelected).toHaveBeenCalledTimes(1);
68
+
69
+ const clear = host.querySelector<HTMLButtonElement>('[data-flat-multiselect-clear="true"]');
70
+ act(() => clear?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
71
+ expect(onClearSelection).toHaveBeenCalledTimes(1);
72
+ act(() => root.unmount());
73
+ });
74
+ });
@@ -1,6 +1,184 @@
1
1
  import { Eye, Layers } from "../../icons/SystemIcons";
2
+ import type { DomEditSelection } from "./domEditingTypes";
3
+
4
+ function FlatEmptyState() {
5
+ return (
6
+ <div className="flex h-full flex-col items-center justify-center gap-2.5 px-8 py-10 text-center">
7
+ <span className="flex h-11 w-11 items-center justify-center rounded-xl border border-panel-border-input bg-panel-input text-panel-text-3">
8
+ <svg
9
+ width="20"
10
+ height="20"
11
+ viewBox="0 0 20 20"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ strokeWidth="1.4"
15
+ >
16
+ <path d="M4 3l6 14 2-6 6-2z" strokeLinejoin="round" />
17
+ </svg>
18
+ </span>
19
+ <div className="text-[13px] font-semibold text-panel-text-0">Nothing selected</div>
20
+ <div className="max-w-[250px] text-[11px] leading-[1.5] text-panel-text-3">
21
+ Click any element on the canvas to edit it, or drag to select several.
22
+ </div>
23
+ <div className="mt-2 flex w-full flex-col gap-1.5">
24
+ <span className="flex items-center justify-between rounded-lg border border-panel-border bg-panel-bg px-3 py-2">
25
+ <span className="flex items-center gap-2 text-[11px] text-panel-text-2">
26
+ <span className="text-panel-danger">●</span>
27
+ Record a gesture
28
+ </span>
29
+ <span className="rounded border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
30
+ R
31
+ </span>
32
+ </span>
33
+ <span className="flex items-center justify-between rounded-lg border border-panel-border bg-panel-bg px-3 py-2">
34
+ <span className="flex items-center gap-2 text-[11px] text-panel-text-2">
35
+ <span className="text-panel-accent">✦</span>
36
+ Describe a change to the agent
37
+ </span>
38
+ <span className="rounded border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
39
+ ⌘K
40
+ </span>
41
+ </span>
42
+ </div>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ function elementKindGlyph(element: DomEditSelection): { glyph: string; className: string } {
48
+ if (element.tagName === "video" || element.tagName === "audio" || element.tagName === "img") {
49
+ return { glyph: "◆", className: "bg-panel-media/10 text-panel-media" };
50
+ }
51
+ if (element.textFields?.length > 0) {
52
+ return { glyph: "T", className: "bg-panel-accent/10 text-panel-accent" };
53
+ }
54
+ return { glyph: "▦", className: "bg-panel-container/10 text-panel-container" };
55
+ }
56
+
57
+ function FlatMultiSelectState({
58
+ multiSelectCount,
59
+ multiSelectedElements = [],
60
+ onGroupSelection,
61
+ onHideAllSelected,
62
+ onClearSelection,
63
+ }: {
64
+ multiSelectCount: number;
65
+ multiSelectedElements?: DomEditSelection[];
66
+ onGroupSelection?: () => void;
67
+ onHideAllSelected?: () => void;
68
+ onClearSelection?: () => void;
69
+ }) {
70
+ return (
71
+ <div className="flex flex-col gap-3 px-4 py-3">
72
+ <div className="flex items-center gap-3 rounded-xl border border-panel-border bg-panel-surface p-3">
73
+ <span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-panel-accent/10 text-panel-accent">
74
+ <Layers size={16} />
75
+ </span>
76
+ <div className="min-w-0 flex-1">
77
+ <div className="text-[13px] font-semibold text-panel-text-0">
78
+ {multiSelectCount} elements selected
79
+ </div>
80
+ <div className="mt-px font-mono text-[10px] text-panel-text-3">
81
+ shift-click to add or remove
82
+ </div>
83
+ </div>
84
+ <button
85
+ type="button"
86
+ data-flat-multiselect-clear="true"
87
+ aria-label="Clear selection"
88
+ onClick={onClearSelection}
89
+ className="flex h-[26px] w-[26px] flex-shrink-0 items-center justify-center text-panel-text-3"
90
+ >
91
+ <svg
92
+ width="13"
93
+ height="13"
94
+ viewBox="0 0 16 16"
95
+ fill="none"
96
+ stroke="currentColor"
97
+ strokeWidth="1.5"
98
+ >
99
+ <path d="M3 3l10 10M13 3L3 13" />
100
+ </svg>
101
+ </button>
102
+ </div>
103
+ <div className="flex flex-col gap-1">
104
+ {multiSelectedElements.map((element) => {
105
+ const { glyph, className } = elementKindGlyph(element);
106
+ return (
107
+ <span
108
+ key={`${element.id ?? element.selector ?? ""}:${element.selectorIndex ?? 0}`}
109
+ className="flex items-center gap-2 rounded-lg border border-panel-border bg-panel-bg px-2.5 py-[7px]"
110
+ >
111
+ <span
112
+ className={`flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded text-[9px] font-bold ${className}`}
113
+ >
114
+ {glyph}
115
+ </span>
116
+ <span className="min-w-0 flex-1 truncate text-[11px] text-panel-text-1">
117
+ {element.label}
118
+ </span>
119
+ <span className="flex-shrink-0 font-mono text-[9px] text-panel-text-4">
120
+ {element.id ? `#${element.id}` : element.selector}
121
+ </span>
122
+ </span>
123
+ );
124
+ })}
125
+ </div>
126
+ <div className="flex gap-2">
127
+ <button
128
+ type="button"
129
+ data-flat-multiselect-group="true"
130
+ onClick={onGroupSelection}
131
+ className="flex h-[34px] flex-1 items-center justify-center gap-2 rounded-lg bg-panel-hover text-[11px] font-semibold text-panel-text-0"
132
+ >
133
+ <Layers size={13} />
134
+ Group selection
135
+ </button>
136
+ <button
137
+ type="button"
138
+ data-flat-multiselect-hide-all="true"
139
+ onClick={onHideAllSelected}
140
+ className="flex h-[34px] items-center gap-1.5 rounded-lg border border-panel-border-input bg-panel-input px-3 text-[11px] font-medium text-panel-text-2"
141
+ >
142
+ <Eye size={13} />
143
+ Hide all
144
+ </button>
145
+ </div>
146
+ <span className="text-center text-[10px] text-panel-text-5">
147
+ Select a single element to edit its properties
148
+ </span>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ export function PropertyPanelEmptyState({
154
+ multiSelectCount,
155
+ flat,
156
+ multiSelectedElements,
157
+ onGroupSelection,
158
+ onHideAllSelected,
159
+ onClearSelection,
160
+ }: {
161
+ multiSelectCount: number;
162
+ flat?: boolean;
163
+ multiSelectedElements?: DomEditSelection[];
164
+ onGroupSelection?: () => void;
165
+ onHideAllSelected?: () => void;
166
+ onClearSelection?: () => void;
167
+ }) {
168
+ if (flat) {
169
+ return multiSelectCount > 1 ? (
170
+ <FlatMultiSelectState
171
+ multiSelectCount={multiSelectCount}
172
+ multiSelectedElements={multiSelectedElements}
173
+ onGroupSelection={onGroupSelection}
174
+ onHideAllSelected={onHideAllSelected}
175
+ onClearSelection={onClearSelection}
176
+ />
177
+ ) : (
178
+ <FlatEmptyState />
179
+ );
180
+ }
2
181
 
3
- export function PropertyPanelEmptyState({ multiSelectCount }: { multiSelectCount: number }) {
4
182
  return (
5
183
  <div className="flex h-full flex-col bg-neutral-900">
6
184
  <div className="flex flex-1 flex-col items-center justify-center px-6 text-center">