@hyperframes/studio 0.8.113 → 0.8.115

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 (194) hide show
  1. package/dist/assets/{hyperframes-player-DzJbtQVO.js → hyperframes-player-C-9N8nk0.js} +1 -1
  2. package/dist/assets/{index-n5FUn1xv.js → index-BoUM6Xqb.js} +1 -1
  3. package/dist/assets/{index-DRhF3v4Z.js → index-CAvZ0AA-.js} +204 -204
  4. package/dist/assets/{index-DgwDQK5G.js → index-CVuorIMv.js} +1 -1
  5. package/dist/assets/index-t1LH6H5H.css +1 -0
  6. package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
  7. package/dist/chunk-5EKWJZLM.js.map +1 -0
  8. package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
  9. package/dist/{chunk-BIUXP3PG.js → chunk-TXZMMOMA.js} +43 -276
  10. package/dist/chunk-TXZMMOMA.js.map +1 -0
  11. package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-D7VYFJJD.js} +3 -5
  12. package/dist/index.d.ts +46 -22
  13. package/dist/index.html +2 -2
  14. package/dist/index.js +3899 -3487
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +2 -2
  17. package/dist/styles.css +1 -1
  18. package/package.json +10 -8
  19. package/src/App.tsx +84 -86
  20. package/src/components/EditorShell.tsx +3 -3
  21. package/src/components/LinkedSelectionToggle.tsx +3 -2
  22. package/src/components/StudioHeader.tsx +4 -4
  23. package/src/components/StudioLeftPanels.tsx +3 -3
  24. package/src/components/StudioOverlays.tsx +4 -4
  25. package/src/components/StudioRightPanels.tsx +14 -9
  26. package/src/components/StudioToast.tsx +1 -1
  27. package/src/components/TimelineToolbar.tsx +3 -3
  28. package/src/components/dock/Dock.tsx +5 -7
  29. package/src/components/editor/CanvasContextMenu.tsx +3 -2
  30. package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
  31. package/src/components/editor/DomEditCropHandles.tsx +7 -4
  32. package/src/components/editor/DomEditOverlay.tsx +5 -1
  33. package/src/components/editor/FileTreeNodes.tsx +13 -10
  34. package/src/components/editor/InlineTextToolbar.tsx +11 -27
  35. package/src/components/editor/MotionPathOverlay.tsx +7 -2
  36. package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
  37. package/src/components/editor/SnapToolbar.test.tsx +100 -1
  38. package/src/components/editor/SnapToolbar.tsx +43 -5
  39. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
  40. package/src/components/editor/audioFxTelemetry.ts +2 -3
  41. package/src/components/editor/colorBrowserCanvas.test.ts +116 -0
  42. package/src/components/editor/colorValue.test.ts +16 -0
  43. package/src/components/editor/colorValue.ts +100 -24
  44. package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
  45. package/src/components/editor/cropResize.test.ts +11 -0
  46. package/src/components/editor/cropResize.ts +6 -6
  47. package/src/components/editor/domEditingDom.ts +4 -0
  48. package/src/components/editor/domEditingLayers.ts +1 -5
  49. package/src/components/editor/gradientValue.ts +28 -36
  50. package/src/components/editor/gsapAnimatesProperty.ts +0 -54
  51. package/src/components/editor/inlineTextStyleRange.test.ts +31 -1
  52. package/src/components/editor/inlineTextStyleRange.ts +5 -2
  53. package/src/components/editor/inlineTextStyleRead.ts +9 -0
  54. package/src/components/editor/manualEdits.ts +1 -0
  55. package/src/components/editor/manualEditsDom.ts +2 -2
  56. package/src/components/editor/manualEditsParsing.ts +7 -0
  57. package/src/components/editor/manualEditsSeekReapply.ts +2 -2
  58. package/src/components/editor/marqueeCommit.test.tsx +106 -37
  59. package/src/components/editor/marqueeCommit.ts +24 -8
  60. package/src/components/editor/motionPathCommit.test.ts +29 -1
  61. package/src/components/editor/motionPathCommit.ts +21 -9
  62. package/src/components/editor/probeSourceElement.test.ts +45 -0
  63. package/src/components/editor/probeSourceElement.ts +39 -15
  64. package/src/components/editor/propertyPanelColor.tsx +1 -1
  65. package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
  66. package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
  67. package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
  68. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
  69. package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
  70. package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
  71. package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
  72. package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
  73. package/src/components/editor/propertyPanelFxSection.tsx +30 -28
  74. package/src/components/editor/propertyPanelTypes.ts +6 -3
  75. package/src/components/editor/reapplyPositionEditsAfterSeek.test.ts +25 -0
  76. package/src/components/editor/useDomEditNudge.test.tsx +37 -0
  77. package/src/components/editor/useDomEditNudge.ts +2 -0
  78. package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
  79. package/src/components/editor/useDomEditZOrder.ts +12 -2
  80. package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
  81. package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
  82. package/src/components/sidebar/AssetContextMenu.tsx +4 -4
  83. package/src/components/ui/menuStyle.test.ts +61 -0
  84. package/src/components/ui/menuStyle.ts +10 -0
  85. package/src/contexts/DomEditContext.tsx +1 -1
  86. package/src/contexts/TimelineEditContext.tsx +1 -0
  87. package/src/domEditExports.test.tsx +9 -1
  88. package/src/hooks/domEditCommitRunner.ts +1 -1
  89. package/src/hooks/elementOffsetStager.ts +5 -4
  90. package/src/hooks/gsapDragCommit.ts +2 -20
  91. package/src/hooks/gsapDragPositionCommit.ts +18 -3
  92. package/src/hooks/gsapGestureOutcome.test.ts +161 -0
  93. package/src/hooks/gsapGestureOutcome.ts +64 -0
  94. package/src/hooks/gsapKeyframeCommit.ts +3 -12
  95. package/src/hooks/gsapResizeIntercept.test.ts +33 -0
  96. package/src/hooks/gsapResizeIntercept.ts +1 -0
  97. package/src/hooks/gsapRuntimeKeyframes.ts +1 -1
  98. package/src/hooks/gsapRuntimeReaders.test.ts +48 -0
  99. package/src/hooks/gsapRuntimeReaders.ts +3 -90
  100. package/src/hooks/gsapScriptCommitTypes.ts +5 -0
  101. package/src/hooks/gsapShared.writeTarget.test.ts +20 -0
  102. package/src/hooks/keyframeCacheAstLoad.ts +6 -5
  103. package/src/hooks/plainRotation.ts +3 -2
  104. package/src/hooks/previewPromotionSelection.test.tsx +1 -1
  105. package/src/hooks/refreshTimelineRowText.test.ts +65 -0
  106. package/src/hooks/refreshTimelineRowText.ts +29 -0
  107. package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
  108. package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
  109. package/src/hooks/useAppHotkeys.ts +11 -5
  110. package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
  111. package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
  112. package/src/hooks/useDomEditTextCommits.test.tsx +18 -0
  113. package/src/hooks/useDomEditTextCommits.ts +5 -1
  114. package/src/hooks/useDomEditWiring.ts +4 -2
  115. package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
  116. package/src/hooks/useDomGeometryCommit.ts +7 -4
  117. package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
  118. package/src/hooks/useDomGeometryCommits.ts +3 -2
  119. package/src/hooks/useDomSelection.test.ts +26 -0
  120. package/src/hooks/useDomSelection.ts +10 -3
  121. package/src/hooks/useDomSelectionTypes.ts +6 -1
  122. package/src/hooks/useEnableKeyframes.test.ts +2 -2
  123. package/src/hooks/useEnableKeyframes.ts +5 -3
  124. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +102 -0
  125. package/src/hooks/useExternalFileChangeCoordinator.ts +31 -6
  126. package/src/hooks/useFrameCapture.ts +15 -8
  127. package/src/hooks/useGestureCommit.test.tsx +225 -55
  128. package/src/hooks/useGestureCommit.ts +90 -56
  129. package/src/hooks/useGestureTrailOverlay.tsx +33 -0
  130. package/src/hooks/useGsapAwareEditing.test.tsx +11 -6
  131. package/src/hooks/useGsapAwareEditing.ts +96 -195
  132. package/src/hooks/useGsapAwareGroupMove.ts +179 -0
  133. package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
  134. package/src/hooks/useGsapKeyframeOps.ts +36 -10
  135. package/src/hooks/useGsapScriptCommits.test.tsx +57 -0
  136. package/src/hooks/useGsapScriptCommits.ts +3 -0
  137. package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
  138. package/src/hooks/useGsapSelectionHandlers.ts +24 -18
  139. package/src/hooks/useGsapTweenCache.ts +18 -12
  140. package/src/hooks/usePanelLayout.ts +5 -1
  141. package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
  142. package/src/hooks/usePreviewInteraction.test.ts +21 -0
  143. package/src/hooks/usePreviewInteraction.ts +2 -0
  144. package/src/hooks/useRenderClipContent.test.ts +18 -0
  145. package/src/hooks/useRenderClipContent.ts +4 -5
  146. package/src/hooks/useShellSlices.ts +53 -0
  147. package/src/hooks/useStableHandlers.test.tsx +77 -0
  148. package/src/hooks/useStableHandlers.ts +44 -0
  149. package/src/hooks/useStudioSdkSessions.ts +2 -1
  150. package/src/hooks/useStudioSidePanels.test.tsx +44 -0
  151. package/src/hooks/useStudioSidePanels.tsx +23 -0
  152. package/src/hooks/useTimelineAddAtPlayhead.ts +16 -8
  153. package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
  154. package/src/hooks/useTimelineDeleteOps.ts +21 -27
  155. package/src/hooks/useTimelineEditing.test.tsx +1 -1
  156. package/src/hooks/useTimelineEditing.ts +33 -27
  157. package/src/player/components/AutomationSelectionMenu.tsx +3 -2
  158. package/src/player/components/ClipContextMenu.test.tsx +73 -0
  159. package/src/player/components/ClipContextMenu.tsx +33 -31
  160. package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
  161. package/src/player/components/OutOfSyncBadge.tsx +5 -5
  162. package/src/player/components/TextClipContent.test.ts +19 -0
  163. package/src/player/components/TextClipContent.tsx +27 -0
  164. package/src/player/components/TrackGapContextMenu.tsx +3 -6
  165. package/src/player/components/clipMenuToolItems.test.tsx +23 -0
  166. package/src/player/components/clipMenuToolItems.tsx +13 -9
  167. package/src/player/components/timelineCallbacks.ts +1 -0
  168. package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
  169. package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
  170. package/src/player/components/timelineClipGestureHandlers.ts +20 -2
  171. package/src/player/components/timelineEditing.ts +3 -1
  172. package/src/player/components/timelineLinkSelection.ts +35 -1
  173. package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
  174. package/src/player/components/useTimelineClipDrag.ts +6 -7
  175. package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
  176. package/src/player/hooks/timelinePlayerSync.ts +6 -1
  177. package/src/player/lib/timelineElementHelpers.test.ts +87 -1
  178. package/src/player/lib/timelineElementHelpers.ts +6 -15
  179. package/src/player/lib/timelineText.ts +40 -0
  180. package/src/player/store/timelineElement.ts +12 -0
  181. package/src/styles/theme.css +3 -0
  182. package/src/utils/frameCapture.ts +3 -1
  183. package/src/utils/gsapUndoRestore.browser.test.ts +4 -2
  184. package/src/utils/keyframeUsage.ts +79 -0
  185. package/src/utils/previewFeatureUsage.test.ts +32 -0
  186. package/src/utils/previewFeatureUsage.ts +46 -0
  187. package/src/utils/studioTelemetry.test.ts +107 -0
  188. package/src/utils/studioTelemetry.ts +49 -4
  189. package/src/webmcp/StudioAgentTools.tsx +2 -2
  190. package/dist/assets/index-DbfKVTId.css +0 -1
  191. package/dist/chunk-BIUXP3PG.js.map +0 -1
  192. package/dist/chunk-K47E2ZJA.js.map +0 -1
  193. /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
  194. /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-D7VYFJJD.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.113",
3
+ "version": "0.8.115",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,12 +60,12 @@
60
60
  "dockview-react": "8.3.1",
61
61
  "gsap": "^3.13.0",
62
62
  "mediabunny": "^1.45.3",
63
- "@hyperframes/core": "0.8.113",
64
- "@hyperframes/player": "0.8.113",
65
- "@hyperframes/sdk": "0.8.113",
66
- "@hyperframes/parsers": "0.8.113",
67
- "@hyperframes/studio-server": "0.8.113",
68
- "@hyperframes/shader-transitions": "0.8.113"
63
+ "@hyperframes/core": "0.8.115",
64
+ "@hyperframes/player": "0.8.115",
65
+ "@hyperframes/studio-server": "0.8.115",
66
+ "@hyperframes/sdk": "0.8.115",
67
+ "@hyperframes/shader-transitions": "0.8.115",
68
+ "@hyperframes/parsers": "0.8.115"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@tailwindcss/vite": "^4.3.3",
@@ -81,7 +81,7 @@
81
81
  "vite": "^6.4.2",
82
82
  "vitest": "^4.1.11",
83
83
  "zustand": "^5.0.0",
84
- "@hyperframes/producer": "0.8.113"
84
+ "@hyperframes/producer": "0.8.115"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "react": "19",
@@ -97,8 +97,10 @@
97
97
  "test:filmstrip-controls": "node tests/e2e/filmstrip-controls.mjs",
98
98
  "test:webmcp-edit-loop": "node tests/e2e/webmcp-edit-loop.mjs",
99
99
  "test:thumbnail-save": "node tests/e2e/thumbnail-save.mjs",
100
+ "bench:timeline-bulk-actions": "node tests/e2e/timeline-bulk-actions.mjs",
100
101
  "test:timeline-virtualization": "TIMELINE_ROW_VIRTUALIZATION=on TIMELINE_ELEMENT_COUNT=50000 node tests/e2e/timeline-virtualization.mjs",
101
102
  "test:watch": "vitest",
103
+ "test:colors:browser": "node tests/e2e/native-css-colors.mjs",
102
104
  "report:sdk-cutover": "bun src/utils/sdkCutoverPolicy.report.ts",
103
105
  "test:timeline-default": "bun run test:timeline-virtualization"
104
106
  }
package/src/App.tsx CHANGED
@@ -1,6 +1,8 @@
1
1
  import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore";
2
2
  import { buildProjectApiPath } from "./utils/projectRouting";
3
- import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
3
+ import { useState, useCallback, useRef, useMemo } from "react";
4
+ import { useStableHandlers } from "./hooks/useStableHandlers";
5
+ import { useHistoryFlags, useToolbarSession } from "./hooks/useShellSlices";
4
6
  import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents";
5
7
  import { useRenderQueue } from "./components/renders/useRenderQueue";
6
8
  import { usePlayerStore } from "./player";
@@ -46,10 +48,9 @@ import {
46
48
  import type { DomEditSelection } from "./components/editor/domEditing";
47
49
  import { StudioHeader } from "./components/StudioHeader";
48
50
  import { useGestureCommit } from "./hooks/useGestureCommit";
49
- import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
50
- import { StudioLeftPanels } from "./components/StudioLeftPanels";
51
+ import { useGestureTrailOverlay } from "./hooks/useGestureTrailOverlay";
51
52
  import { EditorShell } from "./components/EditorShell";
52
- import { StudioRightPanels } from "./components/StudioRightPanels";
53
+ import { useStudioSidePanels } from "./hooks/useStudioSidePanels";
53
54
  import { TimelineToolbar } from "./components/TimelineToolbar";
54
55
  import { StudioPlaybackProvider, StudioShellProvider } from "./contexts/StudioContext";
55
56
  import { PanelLayoutProvider } from "./contexts/PanelLayoutContext";
@@ -99,17 +100,18 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
99
100
  rightCollapsed: initialUrlStateRef.current.rightCollapsed,
100
101
  rightPanelTab: initialUrlStateRef.current.rightPanelTab,
101
102
  });
102
- const editHistory = usePersistentEditHistory({ projectId });
103
+ const editHistory = useStableHandlers(usePersistentEditHistory({ projectId }), projectId);
103
104
  const handleDomZIndexReorderCommitRef = useRef<TimelineZIndexReorderCommit | null>(null);
104
105
  const pendingTimelineEditPathRef = useRef(new Set<string>());
105
106
  const isGestureRecordingRef = useRef(false);
106
107
  const reloadPreview = useCallback(() => setRefreshKey((k) => k + 1), []);
107
- const fileManager = useFileManager({
108
+ const fileManagerResult = useFileManager({
108
109
  projectId,
109
110
  showToast,
110
111
  recordEdit: editHistory.recordEdit,
111
112
  setRefreshKey,
112
113
  });
114
+ const fileManager = useStableHandlers(fileManagerResult, projectId);
113
115
  const masterCompPath = useMemo(
114
116
  () => resolveMasterCompositionPath(fileManager.compositions),
115
117
  [fileManager.compositions],
@@ -134,8 +136,8 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
134
136
  );
135
137
  const activeCompPathRef = useRef(activeCompPath);
136
138
  activeCompPathRef.current = activeCompPath;
137
- const renderQueue = useRenderQueue(projectId, activeCompPathRef);
138
- const previewPersistence = usePreviewPersistence({
139
+ const renderQueue = useStableHandlers(useRenderQueue(projectId, activeCompPathRef), projectId);
140
+ const previewPersistenceResult = usePreviewPersistence({
139
141
  showToast,
140
142
  readOptionalProjectFile: fileManager.readOptionalProjectFile,
141
143
  writeProjectFile: fileManager.writeProjectFile,
@@ -144,6 +146,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
144
146
  activeCompPathRef,
145
147
  reloadPreview: () => setRefreshKey((k) => k + 1),
146
148
  });
149
+ const previewPersistence = useStableHandlers(previewPersistenceResult, projectId);
147
150
  const externalFileChanges = useStudioExternalFileChanges({
148
151
  projectId,
149
152
  activeCompPath,
@@ -184,15 +187,9 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
184
187
  } = useTimelineAddAtPlayhead(
185
188
  timelineEditing.handleTimelineAssetDrop,
186
189
  timelineEditing.handleTimelineCompositionDrop,
190
+ projectId,
187
191
  );
188
- const {
189
- activeBlockParams,
190
- setActiveBlockParams,
191
- handleAddBlock,
192
- handleTimelineBlockDrop,
193
- handleAddMediaOverlay,
194
- handlePreviewBlockDrop,
195
- } = useBlockHandlers({
192
+ const blockHandlersResult = useBlockHandlers({
196
193
  projectId,
197
194
  blockCtxDeps: {
198
195
  activeCompPath,
@@ -209,6 +206,14 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
209
206
  setRightCollapsed: panelLayout.setRightCollapsed,
210
207
  setRightPanelTab: panelLayout.setRightPanelTab,
211
208
  });
209
+ const {
210
+ activeBlockParams,
211
+ setActiveBlockParams,
212
+ handleAddBlock,
213
+ handleTimelineBlockDrop,
214
+ handleAddMediaOverlay,
215
+ handlePreviewBlockDrop,
216
+ } = useStableHandlers(blockHandlersResult, projectId);
212
217
  const dismissBlockParams = useCallback(() => setActiveBlockParams(null), [setActiveBlockParams]);
213
218
  const setRightPanelTab = useDismissingTabSetter(panelLayout.setRightPanelTab, dismissBlockParams);
214
219
  const layout = useMemo(
@@ -237,6 +242,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
237
242
  waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
238
243
  });
239
244
  const appHotkeys = useAppHotkeys({
245
+ projectId,
240
246
  handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
241
247
  handleLinkEdit: timelineEditing.handleLinkEdit,
242
248
  handleTimelineElementDeleteOnly: timelineEditing.handleTimelineElementDeleteOnly,
@@ -266,10 +272,10 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
266
272
  onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
267
273
  activeCompPath,
268
274
  forceReloadSdkSession: sdkHandle.forceReload,
269
- onToggleRecording: () => handleToggleRecordingRef.current(),
275
+ onToggleRecording: () => handleToggleRecordingRef.current("keyboard"),
270
276
  readOnlyPreview,
271
277
  });
272
- const domEditSession = useDomEditSession({
278
+ const domEditSessionResult = useDomEditSession({
273
279
  projectId,
274
280
  activeCompPath,
275
281
  compIdToSrc,
@@ -309,6 +315,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
309
315
  handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
310
316
  readOnlyPreview,
311
317
  });
318
+ const domEditSession = useStableHandlers(domEditSessionResult, projectId);
312
319
  domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
313
320
  const { handleDomZIndexReorderCommit: zCommit, handleDomEditElementDelete: del } = domEditSession;
314
321
  handleDomZIndexReorderCommitRef.current = trackedStudioEdit(zCommit);
@@ -342,23 +349,25 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
342
349
  effectiveTimelineDuration,
343
350
  });
344
351
  const compositionDimensions = useCompositionDimensions(previewIframeRef);
345
- const lint = useLintModal(projectId, refreshKey);
346
- const frameCapture = useFrameCapture({
352
+ const lint = useStableHandlers(useLintModal(projectId, refreshKey), projectId);
353
+ const frameCaptureResult = useFrameCapture({
347
354
  projectId,
348
355
  activeCompPath,
349
356
  showToast,
350
357
  waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
351
358
  });
359
+ const frameCapture = useStableHandlers(frameCaptureResult, projectId);
352
360
  const {
353
361
  consoleErrors,
354
362
  setConsoleErrors,
355
363
  resetErrors: resetConsoleErrors,
356
364
  } = useConsoleErrorCapture(previewIframe);
357
365
  const fileDrop = useGlobalFileDrop(timelineEditing.handleTimelineFileDrop);
358
- const handleToggleRecordingRef = useRef<() => void>(() => {});
366
+ const handleToggleRecordingRef = useRef<(method?: "button" | "keyboard") => void>(() => {});
359
367
  const domEditSessionRef = useRef(domEditSession);
360
368
  domEditSessionRef.current = domEditSession;
361
369
  const { gestureState, gestureRecording, handleToggleRecording } = useGestureCommit({
370
+ projectId,
362
371
  domEditSessionRef,
363
372
  previewIframeRef,
364
373
  showToast,
@@ -366,14 +375,11 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
366
375
  readOnlyPreview,
367
376
  });
368
377
  handleToggleRecordingRef.current = handleToggleRecording;
369
- const canvasRectRef = useRef<DOMRect | null>(null);
370
- useLayoutEffect(() => {
371
- if (gestureState !== "recording" || !previewIframe) {
372
- canvasRectRef.current = null;
373
- return;
374
- }
375
- canvasRectRef.current = previewIframe.getBoundingClientRect();
376
- }, [gestureState, previewIframe]);
378
+ const gestureOverlay = useGestureTrailOverlay(
379
+ { gestureState, gestureRecording },
380
+ previewIframe,
381
+ compositionDimensions,
382
+ );
377
383
  const handlePreviewIframeRef = useCallback(
378
384
  (iframe: HTMLIFrameElement | null) => {
379
385
  previewIframeRef.current = iframe;
@@ -415,6 +421,8 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
415
421
  setRightPanelTab,
416
422
  initialState: initialUrlStateRef.current,
417
423
  });
424
+ const historyFlags = useHistoryFlags(editHistory);
425
+ const toolbarSession = useToolbarSession(domEditSession);
418
426
  const studioCtxValue = buildStudioContextValue({
419
427
  projectId: projectId!,
420
428
  activeCompPath,
@@ -427,7 +435,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
427
435
  setRefreshKey,
428
436
  timelineElements,
429
437
  isPlaying,
430
- editHistory,
438
+ editHistory: historyFlags,
431
439
  handleUndo: appHotkeys.handleUndo,
432
440
  handleRedo: appHotkeys.handleRedo,
433
441
  renderQueue,
@@ -441,11 +449,51 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
441
449
  const timelineToolbar = useMemo(
442
450
  () => (
443
451
  <TimelineToolbar
444
- domEditSession={domEditSession}
452
+ domEditSession={toolbarSession}
445
453
  onSplitElement={timelineEditing.handleTimelineElementSplit}
446
454
  />
447
455
  ),
448
- [domEditSession, timelineEditing.handleTimelineElementSplit],
456
+ [toolbarSession, timelineEditing.handleTimelineElementSplit],
457
+ );
458
+ const handleExport = useCallback(() => {
459
+ void previewPersistence
460
+ .waitForPendingDomEditSaves()
461
+ .then(() => renderQueue.startRender(undefined));
462
+ }, [previewPersistence, renderQueue]);
463
+ const clearConsoleErrors = useCallback(() => setConsoleErrors(null), [setConsoleErrors]);
464
+ const panels = useStudioSidePanels(
465
+ {
466
+ onSelectComposition: handleSelectComposition,
467
+ onAddBlock: handleAddBlock,
468
+ onPreviewBlock: setBlockPreview,
469
+ onLint: lint.handleLint,
470
+ linting: lint.linting,
471
+ lintFindingCount: lint.lintFindingCount,
472
+ lintFindingsByFile: lint.findingsByFile,
473
+ lintHasError: lint.hasLintError,
474
+ onAddAssetToTimeline: handleAddAssetAtPlayhead,
475
+ onAddCompositionToTimeline: handleAddCompositionAtPlayhead,
476
+ },
477
+ {
478
+ activeBlockParams,
479
+ onDismissBlockParams: dismissBlockParams,
480
+ onCloseBlockParams: () => {
481
+ setActiveBlockParams(null);
482
+ panelLayout.setRightPanelTab("design");
483
+ },
484
+ recordingState: gestureState,
485
+ recordingDuration: gestureRecording.recordingDuration,
486
+ onToggleRecording: handleToggleRecording,
487
+ sdkSession: sdkHandle.session,
488
+ publishSdkSession: sdkHandle.publish,
489
+ forceReloadSdkSession: sdkHandle.forceReload,
490
+ reloadPreview,
491
+ recordEdit: editHistory.recordEdit,
492
+ onToggleElementHidden: timelineEditing.handleToggleElementHidden,
493
+ onAutoGroupCarveSources: timelineEditing.handleAutoGroupCarveSources,
494
+ onAddMediaOverlay: handleAddMediaOverlay,
495
+ },
496
+ projectId,
449
497
  );
450
498
  if (resolving || waitingForServer || !projectId)
451
499
  return <StudioSplash waiting={waitingForServer} />;
@@ -468,12 +516,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
468
516
  capturing={frameCapture.capturing}
469
517
  inspectorButtonActive={inspectorButtonActive}
470
518
  inspectorPanelActive={inspectorPanelActive}
471
- onExport={() => {
472
- void (async () => {
473
- await previewPersistence.waitForPendingDomEditSaves();
474
- await renderQueue.startRender(undefined);
475
- })();
476
- }}
519
+ onExport={handleExport}
477
520
  />
478
521
  {previewPersistence.domEditSaveQueuePaused && !externalFileChanges.blocked && (
479
522
  <SaveQueuePausedBanner
@@ -491,41 +534,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
491
534
  <EditorShell
492
535
  readOnlyPreview={readOnlyPreview}
493
536
  readOnlyPreviewReason={readOnlyPreviewReason}
494
- panels={
495
- <>
496
- <StudioLeftPanels
497
- onSelectComposition={handleSelectComposition}
498
- onAddBlock={handleAddBlock}
499
- onPreviewBlock={setBlockPreview}
500
- onLint={lint.handleLint}
501
- linting={lint.linting}
502
- lintFindingCount={lint.lintFindingCount}
503
- lintFindingsByFile={lint.findingsByFile}
504
- lintHasError={lint.hasLintError}
505
- onAddAssetToTimeline={handleAddAssetAtPlayhead}
506
- onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
507
- />
508
- <StudioRightPanels
509
- activeBlockParams={activeBlockParams}
510
- onDismissBlockParams={dismissBlockParams}
511
- onCloseBlockParams={() => {
512
- setActiveBlockParams(null);
513
- panelLayout.setRightPanelTab("design");
514
- }}
515
- recordingState={gestureState}
516
- recordingDuration={gestureRecording.recordingDuration}
517
- onToggleRecording={handleToggleRecording}
518
- sdkSession={sdkHandle.session}
519
- publishSdkSession={sdkHandle.publish}
520
- forceReloadSdkSession={sdkHandle.forceReload}
521
- reloadPreview={reloadPreview}
522
- recordEdit={editHistory.recordEdit}
523
- onToggleElementHidden={timelineEditing.handleToggleElementHidden}
524
- onAutoGroupCarveSources={timelineEditing.handleAutoGroupCarveSources}
525
- onAddMediaOverlay={handleAddMediaOverlay}
526
- />
527
- </>
528
- }
537
+ panels={panels}
529
538
  timelineToolbar={timelineToolbar}
530
539
  renderClipContent={renderClipContent}
531
540
  handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
@@ -561,18 +570,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
561
570
  recordingState={gestureState}
562
571
  onToggleRecording={handleToggleRecording}
563
572
  blockPreview={blockPreview}
564
- gestureOverlay={
565
- gestureState === "recording" && previewIframe ? (
566
- <GestureTrailOverlay
567
- samples={gestureRecording.samplesRef.current}
568
- sampleCount={gestureRecording.samplesRef.current.length}
569
- trail={gestureRecording.trailRef.current}
570
- canvasRect={canvasRectRef.current!}
571
- compositionSize={compositionDimensions ?? undefined}
572
- mode="recording"
573
- />
574
- ) : undefined
575
- }
573
+ gestureOverlay={gestureOverlay}
576
574
  />
577
575
  <StudioOverlays
578
576
  projectId={projectId}
@@ -580,7 +578,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
580
578
  lintModal={lint.lintModal}
581
579
  closeLintModal={lint.closeLintModal}
582
580
  consoleErrors={consoleErrors}
583
- clearConsoleErrors={() => setConsoleErrors(null)}
581
+ clearConsoleErrors={clearConsoleErrors}
584
582
  domEditSession={domEditSession}
585
583
  activeCompPath={activeCompPath}
586
584
  toasts={toasts}
@@ -1,4 +1,4 @@
1
- import { useCallback, type ReactNode } from "react";
1
+ import { memo, useCallback, type ReactNode } from "react";
2
2
  import { PreviewPane } from "./nle/PreviewPane";
3
3
  import { TimelinePane } from "./nle/TimelinePane";
4
4
  import { PreviewOverlays } from "./nle/PreviewOverlays";
@@ -81,7 +81,7 @@ export interface EditorShellProps extends TimelineEditCallbackDeps {
81
81
  // The dockable shell: every panel lives in one Dock, arranged by the user's
82
82
  // saved layout. Owns the shared player + composition-stack state via
83
83
  // NLEProvider so every panel shares one player.
84
- export function EditorShell({
84
+ export const EditorShell = memo(function EditorShell({
85
85
  panels,
86
86
  timelineToolbar,
87
87
  renderClipContent,
@@ -228,7 +228,7 @@ export function EditorShell({
228
228
  </div>
229
229
  </PreviewReadOnlyProvider>
230
230
  );
231
- }
231
+ });
232
232
 
233
233
  interface EditorShellBodyProps {
234
234
  panels: ReactNode;
@@ -6,6 +6,7 @@ import { useMenuKeyboardNav } from "../player/components/menuKeyboardNav";
6
6
  import { useLinkedClipPreferences } from "../utils/linkedClipPreferences";
7
7
  import { Tooltip } from "./ui";
8
8
  import { flatActive, flatIdle } from "./timelineToolbarStyles";
9
+ import { menuClasses } from "./ui/menuStyle";
9
10
 
10
11
  function SyncIndicatorMenu({ x, y, onClose }: { x: number; y: number; onClose: () => void }) {
11
12
  const menuRef = useContextMenuDismiss(onClose);
@@ -17,14 +18,14 @@ function SyncIndicatorMenu({ x, y, onClose }: { x: number; y: number; onClose: (
17
18
  ref={menuRef}
18
19
  role="menu"
19
20
  aria-label="Linked clips"
20
- className="fixed z-200 min-w-[200px] rounded-sm border border-neutral-700 bg-neutral-900 py-1 shadow-lg"
21
+ className={`${menuClasses.panel} fixed z-200 min-w-[200px]`}
21
22
  style={{ left: x, top: y }}
22
23
  >
23
24
  <button
24
25
  type="button"
25
26
  role="menuitemcheckbox"
26
27
  aria-checked={visible}
27
- className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-neutral-300 outline-hidden hover:bg-neutral-800 focus-visible:bg-neutral-800"
28
+ className={`${menuClasses.row} ${menuClasses.rowEnabled} flex items-center gap-2`}
28
29
  onClick={() => {
29
30
  setVisible(!visible);
30
31
  onClose();
@@ -1,4 +1,4 @@
1
- import { useContext, type MouseEvent } from "react";
1
+ import { memo, useContext, type MouseEvent } from "react";
2
2
  import { Camera } from "../icons/SystemIcons";
3
3
  import { useStudioShellContext } from "../contexts/StudioContext";
4
4
  import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
@@ -9,7 +9,7 @@ import { InspectorIcon } from "./icons/InspectorIcon";
9
9
  import { HyperframesLogo } from "./ui/HyperframesLogo";
10
10
  import { ShowThemeToggle, ThemeToggle } from "./ThemeToggle";
11
11
 
12
- export interface StudioHeaderProps {
12
+ interface StudioHeaderProps {
13
13
  captureFrameHref: string;
14
14
  captureFrameFilename: string;
15
15
  handleCaptureFrameClick: (event: MouseEvent<HTMLAnchorElement>) => void;
@@ -35,7 +35,7 @@ export function shouldOpenInspector(
35
35
  }
36
36
 
37
37
  // fallow-ignore-next-line complexity
38
- export function StudioHeader({
38
+ export const StudioHeader = memo(function StudioHeader({
39
39
  captureFrameHref,
40
40
  captureFrameFilename,
41
41
  handleCaptureFrameClick,
@@ -184,4 +184,4 @@ export function StudioHeader({
184
184
  </div>
185
185
  </div>
186
186
  );
187
- }
187
+ });
@@ -1,4 +1,4 @@
1
- import { useCallback, type ReactNode } from "react";
1
+ import { memo, useCallback, type ReactNode } from "react";
2
2
  import { SourceEditor } from "./editor/SourceEditor";
3
3
  import { FileTree } from "./editor/FileTree";
4
4
  import { MediaPreview } from "./MediaPreview";
@@ -36,7 +36,7 @@ function PanelColumn({ footer, children }: { footer: ReactNode; children: ReactN
36
36
  }
37
37
 
38
38
  // fallow-ignore-next-line complexity
39
- export function StudioLeftPanels({
39
+ export const StudioLeftPanels = memo(function StudioLeftPanels({
40
40
  onSelectComposition,
41
41
  onAddBlock,
42
42
  onPreviewBlock,
@@ -159,7 +159,7 @@ export function StudioLeftPanels({
159
159
  </Dock.Panel>
160
160
  </>
161
161
  );
162
- }
162
+ });
163
163
 
164
164
  function CodeBody({
165
165
  projectId,
@@ -1,4 +1,4 @@
1
- import type { ComponentProps } from "react";
1
+ import { memo, type ComponentProps } from "react";
2
2
  import { LintModal } from "./LintModal";
3
3
  import { AskAgentModal } from "./AskAgentModal";
4
4
  import { StudioToast } from "./StudioToast";
@@ -9,7 +9,7 @@ import type { useToast } from "../hooks/useToast";
9
9
 
10
10
  type LintFindings = ComponentProps<typeof LintModal>["findings"];
11
11
 
12
- export interface StudioOverlaysProps {
12
+ interface StudioOverlaysProps {
13
13
  projectId: string;
14
14
  projectDir?: string | null;
15
15
  lintModal: LintFindings | null;
@@ -28,7 +28,7 @@ export interface StudioOverlaysProps {
28
28
  * `App.tsx` to keep the shell within the studio's 600-line decomposition budget.
29
29
  */
30
30
  // fallow-ignore-next-line complexity
31
- export function StudioOverlays({
31
+ export const StudioOverlays = memo(function StudioOverlays({
32
32
  projectId,
33
33
  projectDir,
34
34
  lintModal,
@@ -91,4 +91,4 @@ export function StudioOverlays({
91
91
  </div>
92
92
  </>
93
93
  );
94
- }
94
+ });
@@ -1,4 +1,4 @@
1
- import { useCallback } from "react";
1
+ import { memo, useCallback } from "react";
2
2
  import type { StudioRightPanelsProps } from "./StudioRightPanels.types";
3
3
 
4
4
  import { PropertyPanel } from "./editor/PropertyPanel";
@@ -29,7 +29,9 @@ import { useRemoveBackground } from "../hooks/useRemoveBackground";
29
29
  import { useApplyColorGradingScope } from "../hooks/useApplyColorGradingScope";
30
30
 
31
31
  // fallow-ignore-next-line complexity
32
- export function StudioRightPanels({
32
+ const seekToTime = (t: number) => usePlayerStore.getState().requestSeek(t);
33
+
34
+ export const StudioRightPanels = memo(function StudioRightPanels({
33
35
  activeBlockParams,
34
36
  onCloseBlockParams,
35
37
  onDismissBlockParams,
@@ -166,7 +168,7 @@ export function StudioRightPanels({
166
168
  handleDomAttributeLiveCommit(attr, value, undefined, { previewOnly: true }),
167
169
  [handleDomAttributeLiveCommit],
168
170
  );
169
- const handleHideAllSelected = () => {
171
+ const handleHideAllSelected = useCallback(() => {
170
172
  // Audio has no visual to hide, and `data-hidden` on an audio element is what
171
173
  // MUTES it — preview silences it and the render drops it from the mix. The
172
174
  // timeline withholds the eye on an audio track for that reason
@@ -181,7 +183,12 @@ export function StudioRightPanels({
181
183
  const { elements } = usePlayerStore.getState();
182
184
  const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath);
183
185
  if (keys.length > 0) void onToggleElementHidden?.(keys, true);
184
- };
186
+ }, [domEditGroupSelections, showToast, activeCompPath, onToggleElementHidden]);
187
+ const convertToKeyframes = useCallback(
188
+ (animId: string, duration?: number) =>
189
+ handleGsapConvertToKeyframes(animId, undefined, duration),
190
+ [handleGsapConvertToKeyframes],
191
+ );
185
192
  const propertyPanel = (
186
193
  <DesignPanelPromoteProvider
187
194
  selection={domEditGroupSelections.length > 1 ? null : domEditSelection}
@@ -246,10 +253,8 @@ export function StudioRightPanels({
246
253
  onCommitAnimatedProperties={commitAnimatedProperties}
247
254
  onAddKeyframe={handleGsapAddKeyframe}
248
255
  onRemoveKeyframe={handleGsapRemoveKeyframe}
249
- onConvertToKeyframes={(animId, duration) =>
250
- handleGsapConvertToKeyframes(animId, undefined, duration)
251
- }
252
- onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)}
256
+ onConvertToKeyframes={convertToKeyframes}
257
+ onSeekToTime={seekToTime}
253
258
  onSetArcPath={handleSetArcPath}
254
259
  onUpdateArcSegment={handleUpdateArcSegment}
255
260
  onUnroll={handleUnroll}
@@ -307,4 +312,4 @@ export function StudioRightPanels({
307
312
  </Dock.Panel>
308
313
  </>
309
314
  );
310
- }
315
+ });
@@ -46,5 +46,5 @@ export function StudioToast({ message, tone, leaving, onDismiss }: StudioToastPr
46
46
  }
47
47
 
48
48
  export function toastSurface(isError: boolean): string {
49
- return `border shadow-popover backdrop-blur-md ${isError ? "border-danger/40 bg-raised" : "border-border bg-raised/95"}`;
49
+ return `border shadow-popover bg-raised ${isError ? "border-danger/40" : "border-border"}`;
50
50
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef } from "react";
1
+ import { memo, useEffect, useRef } from "react";
2
2
  import {
3
3
  ArrowsOutLineHorizontal,
4
4
  Image,
@@ -141,7 +141,7 @@ function useKeyframeToggle(session?: DomEditSessionSlice) {
141
141
  }
142
142
 
143
143
  // fallow-ignore-next-line complexity
144
- export function TimelineToolbar({
144
+ export const TimelineToolbar = memo(function TimelineToolbar({
145
145
  domEditSession,
146
146
  onSplitElement,
147
147
  history,
@@ -558,4 +558,4 @@ export function TimelineToolbar({
558
558
  </div>
559
559
  </div>
560
560
  );
561
- }
561
+ });
@@ -31,6 +31,7 @@ import {
31
31
  type PanelId,
32
32
  } from "./panelRegistry";
33
33
  import "./dock.css";
34
+ import { menuClasses } from "../ui/menuStyle";
34
35
 
35
36
  const PERSIST_DEBOUNCE_MS = 250;
36
37
 
@@ -288,10 +289,7 @@ function WindowMenu() {
288
289
  Window
289
290
  </button>
290
291
  {open && (
291
- <div
292
- role="menu"
293
- className="absolute right-0 top-8 z-50 w-44 rounded-md border border-neutral-800 bg-neutral-900 py-1 shadow-lg"
294
- >
292
+ <div role="menu" className={`${menuClasses.panel} absolute right-0 top-8 z-50 w-44`}>
295
293
  {panels.map((id) => (
296
294
  <button
297
295
  key={id}
@@ -299,13 +297,13 @@ function WindowMenu() {
299
297
  role="menuitemcheckbox"
300
298
  aria-checked={openPanels.has(id)}
301
299
  onClick={() => togglePanel(id)}
302
- className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-neutral-300 hover:bg-neutral-800"
300
+ className={`${menuClasses.row} ${menuClasses.rowEnabled} flex items-center gap-2`}
303
301
  >
304
302
  <span className="w-3 text-accent-ink">{openPanels.has(id) ? "✓" : ""}</span>
305
303
  {PANEL_DEFINITIONS[id].title}
306
304
  </button>
307
305
  ))}
308
- <div className="my-1 border-t border-neutral-800" />
306
+ <div className={menuClasses.divider} />
309
307
  <button
310
308
  type="button"
311
309
  role="menuitem"
@@ -313,7 +311,7 @@ function WindowMenu() {
313
311
  resetLayout();
314
312
  setOpen(false);
315
313
  }}
316
- className="w-full px-3 py-1.5 text-left text-[11px] text-neutral-300 hover:bg-neutral-800"
314
+ className={`${menuClasses.row} ${menuClasses.rowEnabled}`}
317
315
  >
318
316
  Reset layout
319
317
  </button>
@@ -33,6 +33,7 @@ import {
33
33
  type ZOrderAction,
34
34
  type ZOrderPatch,
35
35
  } from "./canvasContextMenuZOrder";
36
+ import { menuClasses } from "../ui/menuStyle";
36
37
 
37
38
  interface CanvasContextMenuProps {
38
39
  /** Viewport x of the right-click event. */
@@ -209,7 +210,7 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
209
210
  return createPortal(
210
211
  <div
211
212
  ref={menuRef}
212
- className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
213
+ className={`${menuClasses.panel} fixed z-200 min-w-[180px]`}
213
214
  style={{ left: adjustedX, top: adjustedY }}
214
215
  onPointerDown={stopBubble}
215
216
  onMouseDown={stopBubble}
@@ -252,7 +253,7 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
252
253
  );
253
254
  })}
254
255
 
255
- {hasDivider && <div className="my-1 border-t border-neutral-700/60" />}
256
+ {hasDivider && <div className={menuClasses.divider} />}
256
257
 
257
258
  {hasDelete && (
258
259
  <button