@hyperframes/studio 0.7.57 → 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 (247) hide show
  1. package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
  2. package/dist/assets/{index-KsfE1bUu.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-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
  6. package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
  7. package/dist/chunk-OBAG3GWK.js.map +1 -0
  8. package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
  9. package/dist/index.d.ts +27 -7
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +18029 -11563
  12. package/dist/index.js.map +1 -1
  13. package/dist/styles/tailwind-preset.d.ts +5 -0
  14. package/dist/styles/tailwind-preset.js +8 -1
  15. package/dist/styles/tailwind-preset.js.map +1 -1
  16. package/package.json +7 -7
  17. package/src/App.tsx +6 -6
  18. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  19. package/src/components/StudioFeedbackBar.tsx +2 -2
  20. package/src/components/StudioRightPanel.tsx +29 -51
  21. package/src/components/editor/AnimationCard.test.tsx +134 -0
  22. package/src/components/editor/AnimationCard.tsx +19 -4
  23. package/src/components/editor/ArcPathControls.tsx +1 -0
  24. package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
  25. package/src/components/editor/CanvasContextMenu.tsx +71 -6
  26. package/src/components/editor/DomEditOverlay.tsx +5 -2
  27. package/src/components/editor/GestureRecordControl.tsx +7 -1
  28. package/src/components/editor/GsapAnimationSection.tsx +109 -14
  29. package/src/components/editor/InspectorHeaderActions.tsx +42 -4
  30. package/src/components/editor/LayersPanel.tsx +110 -99
  31. package/src/components/editor/PropertyPanel.test.tsx +948 -0
  32. package/src/components/editor/PropertyPanel.tsx +144 -148
  33. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  34. package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
  35. package/src/components/editor/PropertyPanelFlat.tsx +580 -0
  36. package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
  37. package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
  38. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
  39. package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
  40. package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
  41. package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
  42. package/src/components/editor/domEditingDom.ts +5 -6
  43. package/src/components/editor/domEditingElement.ts +15 -32
  44. package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
  45. package/src/components/editor/gsapLivePreview.ts +34 -0
  46. package/src/components/editor/layersPanelSort.ts +80 -0
  47. package/src/components/editor/manualEditingAvailability.test.ts +10 -0
  48. package/src/components/editor/manualEditingAvailability.ts +9 -0
  49. package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
  50. package/src/components/editor/propertyPanelColor.test.tsx +28 -0
  51. package/src/components/editor/propertyPanelColor.tsx +31 -1
  52. package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
  53. package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
  54. package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
  55. package/src/components/editor/propertyPanelFill.tsx +22 -5
  56. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
  57. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
  58. package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
  59. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
  60. package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
  61. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
  62. package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
  63. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
  64. package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
  65. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
  66. package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
  67. package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
  68. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
  69. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
  70. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
  71. package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
  72. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
  73. package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
  74. package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
  75. package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
  76. package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
  77. package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
  78. package/src/components/editor/propertyPanelFont.test.tsx +30 -0
  79. package/src/components/editor/propertyPanelFont.tsx +136 -95
  80. package/src/components/editor/propertyPanelHelpers.ts +73 -0
  81. package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
  82. package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
  83. package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
  84. package/src/components/editor/propertyPanelSections.test.tsx +161 -0
  85. package/src/components/editor/propertyPanelSections.tsx +154 -90
  86. package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
  87. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  88. package/src/components/editor/propertyPanelTypes.ts +14 -1
  89. package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
  90. package/src/components/editor/propertyPanelValueTier.ts +28 -0
  91. package/src/components/editor/useColorGradingController.test.ts +418 -0
  92. package/src/components/editor/useColorGradingController.ts +596 -0
  93. package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
  94. package/src/components/editor/useLayerRevealOverride.ts +389 -0
  95. package/src/components/nle/PreviewOverlays.tsx +27 -2
  96. package/src/components/nle/TimelinePane.test.ts +3 -0
  97. package/src/components/nle/TimelinePane.tsx +11 -1
  98. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
  99. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
  100. package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
  101. package/src/components/nle/zLaneGesture.test.ts +176 -0
  102. package/src/components/nle/zLaneGesture.ts +77 -0
  103. package/src/components/panels/VariablesPanel.tsx +4 -0
  104. package/src/components/sidebar/CompositionsTab.test.ts +28 -2
  105. package/src/components/sidebar/CompositionsTab.tsx +3 -1
  106. package/src/components/storyboard/FramePoster.tsx +24 -4
  107. package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
  108. package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
  109. package/src/components/storyboard/StoryboardGrid.tsx +23 -1
  110. package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
  111. package/src/components/storyboard/StoryboardView.tsx +9 -2
  112. package/src/components/storyboard/frameComments.test.ts +95 -0
  113. package/src/components/storyboard/frameComments.ts +115 -0
  114. package/src/components/storyboard/useFrameComments.ts +82 -0
  115. package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
  116. package/src/contexts/DesignPanelInputContext.tsx +48 -0
  117. package/src/contexts/DomEditContext.tsx +4 -0
  118. package/src/contexts/VariablePromoteContext.test.tsx +67 -0
  119. package/src/contexts/VariablePromoteContext.tsx +10 -5
  120. package/src/hooks/domEditCommitRunner.ts +11 -0
  121. package/src/hooks/domEditCommitTypes.ts +17 -1
  122. package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
  123. package/src/hooks/fetchStubTestUtils.ts +16 -0
  124. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  125. package/src/hooks/timelineEditingHelpers.test.ts +92 -1
  126. package/src/hooks/timelineEditingHelpers.ts +32 -15
  127. package/src/hooks/timelineMoveAdapter.ts +2 -1
  128. package/src/hooks/timelineTimingSync.test.ts +703 -33
  129. package/src/hooks/timelineTimingSync.ts +282 -97
  130. package/src/hooks/timelineTrackVisibility.test.ts +55 -1
  131. package/src/hooks/timelineTrackVisibility.ts +31 -13
  132. package/src/hooks/useAppHotkeys.ts +9 -0
  133. package/src/hooks/useClipboard.ts +19 -17
  134. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  135. package/src/hooks/useDomEditCommits.test.tsx +316 -38
  136. package/src/hooks/useDomEditCommits.ts +81 -162
  137. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  138. package/src/hooks/useDomEditSession.test.tsx +3 -1
  139. package/src/hooks/useDomEditSession.ts +18 -6
  140. package/src/hooks/useDomEditTextCommits.ts +14 -9
  141. package/src/hooks/useDomEditWiring.ts +10 -9
  142. package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
  143. package/src/hooks/useElementLifecycleOps.ts +182 -84
  144. package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
  145. package/src/hooks/useFileManager.ts +82 -60
  146. package/src/hooks/useGsapAnimationOps.ts +7 -6
  147. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  148. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  149. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  150. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  151. package/src/hooks/useGsapScriptCommits.ts +150 -50
  152. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  153. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  154. package/src/hooks/useInspectorSplitResize.ts +51 -0
  155. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  156. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  157. package/src/hooks/usePersistentEditHistory.ts +71 -31
  158. package/src/hooks/usePreviewPersistence.ts +2 -2
  159. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  160. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  161. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  162. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  163. package/src/hooks/useSdkSession.ts +155 -14
  164. package/src/hooks/useSlideshowPersist.ts +7 -1
  165. package/src/hooks/useStoryboard.ts +23 -4
  166. package/src/hooks/useTimelineEditing.test.tsx +306 -155
  167. package/src/hooks/useTimelineEditing.ts +111 -101
  168. package/src/hooks/useTimelineEditingTypes.ts +4 -1
  169. package/src/hooks/useTimelineGroupEditing.ts +51 -5
  170. package/src/hooks/useVariablesPersist.ts +9 -7
  171. package/src/icons/SystemIcons.tsx +2 -0
  172. package/src/player/components/Timeline.test.ts +31 -21
  173. package/src/player/components/Timeline.tsx +62 -91
  174. package/src/player/components/TimelineCanvas.tsx +36 -5
  175. package/src/player/components/TimelineLanes.tsx +32 -10
  176. package/src/player/components/TimelineOverlays.tsx +36 -0
  177. package/src/player/components/TimelineRuler.tsx +18 -5
  178. package/src/player/components/TrackGapContextMenu.tsx +116 -0
  179. package/src/player/components/timelineCallbacks.ts +4 -1
  180. package/src/player/components/timelineClipDragCommit.test.ts +279 -3
  181. package/src/player/components/timelineClipDragCommit.ts +173 -71
  182. package/src/player/components/timelineGapCommit.test.ts +163 -0
  183. package/src/player/components/timelineGapCommit.ts +103 -0
  184. package/src/player/components/timelineGaps.test.ts +221 -0
  185. package/src/player/components/timelineGaps.ts +182 -0
  186. package/src/player/components/timelineLayout.test.ts +2 -1
  187. package/src/player/components/timelineLayout.ts +22 -6
  188. package/src/player/components/timelineMarquee.test.ts +20 -10
  189. package/src/player/components/timelineMarquee.ts +9 -2
  190. package/src/player/components/timelineStackingSync.test.ts +42 -1
  191. package/src/player/components/timelineStackingSync.ts +31 -16
  192. package/src/player/components/timelineZMirror.test.ts +667 -0
  193. package/src/player/components/timelineZMirror.ts +393 -0
  194. package/src/player/components/timelineZoom.test.ts +2 -2
  195. package/src/player/components/timelineZoom.ts +7 -3
  196. package/src/player/components/useTimelineClipDrag.ts +1 -1
  197. package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
  198. package/src/player/components/useTimelineGapHighlights.ts +110 -0
  199. package/src/player/components/useTimelinePlayhead.ts +4 -3
  200. package/src/player/components/useTimelineRangeSelection.ts +6 -4
  201. package/src/player/components/useTimelineScrollViewport.ts +73 -0
  202. package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
  203. package/src/player/components/useTimelineStackingSync.ts +5 -3
  204. package/src/player/components/useTimelineTrackDerivations.ts +38 -0
  205. package/src/player/components/useTrackGapMenu.ts +155 -0
  206. package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
  207. package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
  208. package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
  209. package/src/player/lib/layerOrdering.test.ts +1 -15
  210. package/src/player/lib/layerOrdering.ts +5 -28
  211. package/src/player/lib/timelineDOM.test.ts +72 -0
  212. package/src/player/lib/timelineDOM.ts +3 -0
  213. package/src/player/lib/timelineElementHelpers.ts +35 -11
  214. package/src/player/store/playerStore.ts +12 -1
  215. package/src/styles/studio.css +22 -0
  216. package/src/styles/tailwind-preset.shared.js +7 -0
  217. package/src/telemetry/events.test.ts +10 -0
  218. package/src/telemetry/events.ts +1 -0
  219. package/src/utils/designInputTracking.test.ts +97 -0
  220. package/src/utils/designInputTracking.ts +80 -0
  221. package/src/utils/domEditSaveQueue.ts +4 -3
  222. package/src/utils/editHistory.test.ts +82 -0
  223. package/src/utils/editHistory.ts +14 -2
  224. package/src/utils/gsapSoftReload.test.ts +54 -115
  225. package/src/utils/gsapSoftReload.ts +42 -152
  226. package/src/utils/gsapUndoRestore.test.ts +269 -0
  227. package/src/utils/gsapUndoRestore.ts +269 -0
  228. package/src/utils/sdkCutover.gate.test.ts +7 -7
  229. package/src/utils/sdkCutover.test.ts +490 -57
  230. package/src/utils/sdkCutover.ts +181 -219
  231. package/src/utils/sdkEditTransaction.ts +292 -0
  232. package/src/utils/sdkResolverAttempts.ts +94 -0
  233. package/src/utils/sdkResolverShadow.test.ts +148 -0
  234. package/src/utils/sdkResolverShadow.ts +139 -106
  235. package/src/utils/setSlideshowManifest.test.ts +0 -9
  236. package/src/utils/setSlideshowManifest.ts +20 -27
  237. package/src/utils/sourceScopedSelectorIndex.ts +29 -0
  238. package/src/utils/studioFileHistory.test.ts +41 -0
  239. package/src/utils/studioFileHistory.ts +30 -27
  240. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  241. package/src/utils/studioHelpers.test.ts +45 -0
  242. package/src/utils/studioHelpers.ts +45 -14
  243. package/src/utils/studioSaveDiagnostics.ts +17 -0
  244. package/dist/assets/index-DfmYkU44.js +0 -423
  245. package/dist/assets/index-_pqzyxB1.css +0 -1
  246. package/dist/chunk-5QSIMBEJ.js.map +0 -1
  247. /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
@@ -18,6 +18,7 @@ import {
18
18
  type FontOption,
19
19
  type LocalFontData,
20
20
  } from "./propertyPanelHelpers";
21
+ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
21
22
 
22
23
  /* ------------------------------------------------------------------ */
23
24
  /* Font helper functions */
@@ -123,16 +124,19 @@ function loadImportedFontStylesheet(asset: ImportedFontAsset): void {
123
124
  export function FontFamilyField({
124
125
  value,
125
126
  disabled,
127
+ flat,
126
128
  importedFonts,
127
129
  onImportFonts,
128
130
  onCommit,
129
131
  }: {
130
132
  value: string;
131
133
  disabled?: boolean;
134
+ flat?: boolean;
132
135
  importedFonts: ImportedFontAsset[];
133
136
  onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
134
137
  onCommit: (nextValue: string) => void;
135
138
  }) {
139
+ const track = useTrackDesignInput();
136
140
  const currentFamily = primaryFontFamily(value);
137
141
  const containerRef = useRef<HTMLDivElement | null>(null);
138
142
  const inputRef = useRef<HTMLInputElement | null>(null);
@@ -148,6 +152,10 @@ export function FontFamilyField({
148
152
  const [fontNotice, setFontNotice] = useState<string | null>(null);
149
153
  const canQueryLocalFonts =
150
154
  typeof window !== "undefined" && typeof window.queryLocalFonts === "function";
155
+ const commitFontFamily = (nextValue: string) => {
156
+ if (nextValue !== value) track("select", "Font family");
157
+ onCommit(nextValue);
158
+ };
151
159
 
152
160
  useEffect(() => {
153
161
  if (!open) return;
@@ -245,7 +253,7 @@ export function FontFamilyField({
245
253
  for (const font of imported) loadImportedFontStylesheet(font);
246
254
  const first = imported[0];
247
255
  if (first) {
248
- onCommit(buildFontFamilyValue(first.family));
256
+ commitFontFamily(buildFontFamilyValue(first.family));
249
257
  setQuery("");
250
258
  setOpen(false);
251
259
  } else {
@@ -347,7 +355,7 @@ export function FontFamilyField({
347
355
  : await importSystemFont(option.family);
348
356
  if (imported) {
349
357
  loadImportedFontStylesheet(imported);
350
- onCommit(buildFontFamilyValue(imported.family));
358
+ commitFontFamily(buildFontFamilyValue(imported.family));
351
359
  setQuery("");
352
360
  setOpen(false);
353
361
  return;
@@ -361,11 +369,135 @@ export function FontFamilyField({
361
369
  (f) => f.family.toLowerCase() === option.family.toLowerCase(),
362
370
  );
363
371
  if (imported) loadImportedFontStylesheet(imported);
364
- onCommit(buildFontFamilyValue(option.family));
372
+ commitFontFamily(buildFontFamilyValue(option.family));
365
373
  setQuery("");
366
374
  setOpen(false);
367
375
  };
368
376
 
377
+ const dropdown = open && (
378
+ <div className="absolute left-0 right-0 top-[calc(100%+6px)] z-50 overflow-hidden rounded-xl border border-neutral-700 bg-neutral-950 shadow-2xl">
379
+ <div className="grid grid-cols-[minmax(0,1fr)_auto_auto] gap-2 border-b border-neutral-800 p-2">
380
+ <input
381
+ ref={inputRef}
382
+ type="text"
383
+ value={query}
384
+ disabled={disabled}
385
+ placeholder={loadingGoogleFonts ? "Loading Google Fonts..." : "Search fonts"}
386
+ onChange={(e) => setQuery(e.target.value)}
387
+ onKeyDown={(e) => {
388
+ if (e.key === "Escape") {
389
+ e.preventDefault();
390
+ setOpen(false);
391
+ }
392
+ if (e.key === "Enter" && filteredOptions[0]) {
393
+ e.preventDefault();
394
+ commitFamily(filteredOptions[0]);
395
+ }
396
+ }}
397
+ className="min-w-0 rounded-lg border border-neutral-800 bg-neutral-900 px-2.5 py-2 text-[11px] font-medium text-neutral-100 outline-none placeholder:text-neutral-600 focus:border-neutral-600"
398
+ />
399
+ {canQueryLocalFonts && (
400
+ <button
401
+ type="button"
402
+ disabled={disabled || loadingLocalFonts}
403
+ onClick={loadBrowserLocalFonts}
404
+ className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
405
+ >
406
+ {loadingLocalFonts ? "..." : "Local"}
407
+ </button>
408
+ )}
409
+ <button
410
+ type="button"
411
+ disabled={disabled || importingFonts || !onImportFonts}
412
+ onClick={() => fontInputRef.current?.click()}
413
+ className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
414
+ >
415
+ {importingFonts ? "..." : "Import"}
416
+ </button>
417
+ <input
418
+ ref={fontInputRef}
419
+ type="file"
420
+ accept=".ttf,.otf,.ttc,.woff,.woff2,.eot,font/*"
421
+ multiple
422
+ aria-label="Import local font files"
423
+ disabled={disabled || importingFonts || !onImportFonts}
424
+ className="hidden"
425
+ onChange={async (event) => {
426
+ await handleImportFonts(event.target.files);
427
+ event.target.value = "";
428
+ }}
429
+ />
430
+ </div>
431
+ {fontNotice && (
432
+ <div className="border-b border-neutral-800 px-3 py-2 text-[10px] leading-4 text-neutral-500">
433
+ {fontNotice}
434
+ </div>
435
+ )}
436
+ <div className="max-h-64 overflow-y-auto p-1">
437
+ {filteredOptions.length === 0 ? (
438
+ <div className="px-2 py-3 text-[11px] text-neutral-500">No fonts found.</div>
439
+ ) : (
440
+ filteredOptions.map((option) => (
441
+ <button
442
+ key={`${option.source}-${option.family}`}
443
+ type="button"
444
+ onClick={() => commitFamily(option)}
445
+ className={`flex w-full min-w-0 items-center justify-between gap-3 rounded-lg px-2 py-2 text-left text-[11px] transition-colors ${
446
+ option.family === currentFamily
447
+ ? "bg-studio-accent/15 text-neutral-50"
448
+ : "text-neutral-300 hover:bg-neutral-900 hover:text-neutral-100"
449
+ }`}
450
+ >
451
+ <span className="flex min-w-0 items-center gap-1.5">
452
+ <span className="truncate font-medium">{option.family}</span>
453
+ {renderAliasFor(option.family) && (
454
+ <span className="flex-shrink-0 text-[9px] text-neutral-500">
455
+ → {renderAliasFor(option.family)}
456
+ </span>
457
+ )}
458
+ </span>
459
+ <span className="flex-shrink-0 text-[9px] uppercase tracking-[0.14em] text-neutral-600">
460
+ {option.source}
461
+ </span>
462
+ </button>
463
+ ))
464
+ )}
465
+ </div>
466
+ </div>
467
+ );
468
+
469
+ if (flat) {
470
+ return (
471
+ <div ref={containerRef} className="relative flex min-h-[30px] items-center justify-between">
472
+ <span className="text-[11px] text-panel-text-2">Font</span>
473
+ <button
474
+ type="button"
475
+ data-flat-font-trigger="true"
476
+ disabled={disabled}
477
+ onClick={() => setOpen((next) => !next)}
478
+ className="flex items-center gap-1.5 disabled:cursor-not-allowed"
479
+ >
480
+ <span
481
+ className="max-w-[200px] truncate font-mono text-[11px] text-panel-text-0"
482
+ style={{ fontFamily: value }}
483
+ >
484
+ {currentFamily}
485
+ </span>
486
+ <svg
487
+ width="10"
488
+ height="10"
489
+ viewBox="0 0 10 10"
490
+ fill="currentColor"
491
+ className="flex-shrink-0 text-panel-text-5"
492
+ >
493
+ <path d="M2 3l3 4 3-4z" />
494
+ </svg>
495
+ </button>
496
+ {dropdown}
497
+ </div>
498
+ );
499
+ }
500
+
369
501
  return (
370
502
  <div ref={containerRef} className="relative grid min-w-0 gap-1.5">
371
503
  <span className={LABEL}>Font family</span>
@@ -385,98 +517,7 @@ export function FontFamilyField({
385
517
  Font
386
518
  </span>
387
519
  </button>
388
-
389
- {open && (
390
- <div className="absolute left-0 right-0 top-[calc(100%+6px)] z-50 overflow-hidden rounded-xl border border-neutral-700 bg-neutral-950 shadow-2xl">
391
- <div className="grid grid-cols-[minmax(0,1fr)_auto_auto] gap-2 border-b border-neutral-800 p-2">
392
- <input
393
- ref={inputRef}
394
- type="text"
395
- value={query}
396
- disabled={disabled}
397
- placeholder={loadingGoogleFonts ? "Loading Google Fonts..." : "Search fonts"}
398
- onChange={(e) => setQuery(e.target.value)}
399
- onKeyDown={(e) => {
400
- if (e.key === "Escape") {
401
- e.preventDefault();
402
- setOpen(false);
403
- }
404
- if (e.key === "Enter" && filteredOptions[0]) {
405
- e.preventDefault();
406
- commitFamily(filteredOptions[0]);
407
- }
408
- }}
409
- className="min-w-0 rounded-lg border border-neutral-800 bg-neutral-900 px-2.5 py-2 text-[11px] font-medium text-neutral-100 outline-none placeholder:text-neutral-600 focus:border-neutral-600"
410
- />
411
- {canQueryLocalFonts && (
412
- <button
413
- type="button"
414
- disabled={disabled || loadingLocalFonts}
415
- onClick={loadBrowserLocalFonts}
416
- className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
417
- >
418
- {loadingLocalFonts ? "..." : "Local"}
419
- </button>
420
- )}
421
- <button
422
- type="button"
423
- disabled={disabled || importingFonts || !onImportFonts}
424
- onClick={() => fontInputRef.current?.click()}
425
- className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
426
- >
427
- {importingFonts ? "..." : "Import"}
428
- </button>
429
- <input
430
- ref={fontInputRef}
431
- type="file"
432
- accept=".ttf,.otf,.ttc,.woff,.woff2,.eot,font/*"
433
- multiple
434
- aria-label="Import local font files"
435
- disabled={disabled || importingFonts || !onImportFonts}
436
- className="hidden"
437
- onChange={async (event) => {
438
- await handleImportFonts(event.target.files);
439
- event.target.value = "";
440
- }}
441
- />
442
- </div>
443
- {fontNotice && (
444
- <div className="border-b border-neutral-800 px-3 py-2 text-[10px] leading-4 text-neutral-500">
445
- {fontNotice}
446
- </div>
447
- )}
448
- <div className="max-h-64 overflow-y-auto p-1">
449
- {filteredOptions.length === 0 ? (
450
- <div className="px-2 py-3 text-[11px] text-neutral-500">No fonts found.</div>
451
- ) : (
452
- filteredOptions.map((option) => (
453
- <button
454
- key={`${option.source}-${option.family}`}
455
- type="button"
456
- onClick={() => commitFamily(option)}
457
- className={`flex w-full min-w-0 items-center justify-between gap-3 rounded-lg px-2 py-2 text-left text-[11px] transition-colors ${
458
- option.family === currentFamily
459
- ? "bg-studio-accent/15 text-neutral-50"
460
- : "text-neutral-300 hover:bg-neutral-900 hover:text-neutral-100"
461
- }`}
462
- >
463
- <span className="flex min-w-0 items-center gap-1.5">
464
- <span className="truncate font-medium">{option.family}</span>
465
- {renderAliasFor(option.family) && (
466
- <span className="flex-shrink-0 text-[9px] text-neutral-500">
467
- → {renderAliasFor(option.family)}
468
- </span>
469
- )}
470
- </span>
471
- <span className="flex-shrink-0 text-[9px] uppercase tracking-[0.14em] text-neutral-600">
472
- {option.source}
473
- </span>
474
- </button>
475
- ))
476
- )}
477
- </div>
478
- </div>
479
- )}
520
+ {dropdown}
480
521
  </div>
481
522
  );
482
523
  }
@@ -29,6 +29,26 @@ export function isSelectedElementHidden(
29
29
  );
30
30
  }
31
31
 
32
+ /**
33
+ * 5-part element identity for keying panel remounts on selection change —
34
+ * id or selector alone collides for id-less same-selector siblings, leaving
35
+ * mount-initialized state pointed at the previous element. sourceFile is
36
+ * required too: the same local id/selector can legitimately recur across
37
+ * different composition files (host vs. an inlined sub-composition, or two
38
+ * unrelated sub-comps), and without it those collide onto the same key.
39
+ */
40
+ export function selectionIdentityKey(
41
+ element: Pick<DomEditSelection, "id" | "hfId" | "selector" | "selectorIndex" | "sourceFile">,
42
+ ): string {
43
+ return [
44
+ element.id ?? "",
45
+ element.hfId ?? "",
46
+ element.selector ?? "",
47
+ String(element.selectorIndex ?? ""),
48
+ element.sourceFile ?? "",
49
+ ].join("|");
50
+ }
51
+
32
52
  /* ------------------------------------------------------------------ */
33
53
  /* Font types & constants (shared by font and section modules) */
34
54
  /* ------------------------------------------------------------------ */
@@ -505,3 +525,56 @@ export function readGsapBorderRadiusForPanel(
505
525
  return null;
506
526
  }
507
527
  }
528
+
529
+ /**
530
+ * Builds the multi-line "element info" text copied to the clipboard for an AI
531
+ * agent. Shared by both the legacy and flat inspector headers (the flat split
532
+ * needs the same string), so it lives here rather than as a PropertyPanel
533
+ * closure. Pure — the caller owns the clipboard write, toast, and copied state.
534
+ */
535
+ // fallow-ignore-next-line complexity
536
+ export function buildElementInfoText(
537
+ element: DomEditSelection,
538
+ sourceLabel: string,
539
+ gsapAnimations: GsapAnimation[],
540
+ previewIframeRef?: React.RefObject<HTMLIFrameElement | null>,
541
+ ): string {
542
+ const file = element.sourceFile ?? "index.html";
543
+ let lineNum: number | null = null;
544
+ try {
545
+ const src = previewIframeRef?.current?.contentDocument?.documentElement?.outerHTML ?? "";
546
+ if (src && element.id) {
547
+ const idx = src.indexOf(`id="${element.id}"`);
548
+ if (idx > -1) lineNum = src.slice(0, idx).split("\n").length;
549
+ }
550
+ if (!lineNum && element.selector) {
551
+ const tag = element.tagName.toLowerCase();
552
+ const cls = element.selector.startsWith(".") ? element.selector.slice(1).split(".")[0] : null;
553
+ const search = cls ? `class="${cls}` : `<${tag}`;
554
+ const idx = src.indexOf(search);
555
+ if (idx > -1) lineNum = src.slice(0, idx).split("\n").length;
556
+ }
557
+ } catch {}
558
+ const fileLoc = lineNum ? `${file}:${lineNum}` : file;
559
+ const lines = [
560
+ `Element: ${element.label} (${sourceLabel})`,
561
+ `File: ${fileLoc}`,
562
+ `Position: x=${Math.round(element.boundingBox.x)}, y=${Math.round(element.boundingBox.y)}`,
563
+ `Size: ${Math.round(element.boundingBox.width)}×${Math.round(element.boundingBox.height)}`,
564
+ `Tag: <${element.tagName}>`,
565
+ ];
566
+ if (element.computedStyles["z-index"] && element.computedStyles["z-index"] !== "auto") {
567
+ lines.push(`Z-index: ${element.computedStyles["z-index"]}`);
568
+ }
569
+ if (gsapAnimations.length > 0) {
570
+ const anim = gsapAnimations[0];
571
+ lines.push(
572
+ `Animation: ${anim.method}() ${anim.duration}s at ${anim.position}s, ease: ${anim.ease ?? "default"}`,
573
+ );
574
+ const props = Object.entries(anim.properties)
575
+ .map(([k, v]) => `${k}: ${v}`)
576
+ .join(", ");
577
+ if (props) lines.push(`Properties: ${props}`);
578
+ }
579
+ return lines.join("\n");
580
+ }