@hyperframes/studio 0.8.144 → 0.8.146

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 (119) hide show
  1. package/dist/assets/{hyperframes-player-Bw_jIaSX.js → hyperframes-player-tkxyWf5H.js} +3 -3
  2. package/dist/assets/{index-Cj7W_c7p.js → index-BrdqOkqP.js} +1 -1
  3. package/dist/assets/{index-BVM9NuWl.js → index-Cg48BxAo.js} +145 -145
  4. package/dist/assets/{index-DyH9dT7b.js → index-Dllxg4aU.js} +1 -1
  5. package/dist/{chunk-EEXSYY5F.js → chunk-C5BDT7NG.js} +0 -1
  6. package/dist/{chunk-PEAKCOUW.js → chunk-HZDCMUG4.js} +2 -100
  7. package/dist/{chunk-PZ5AY32C.js → chunk-MLKGABMK.js} +0 -1
  8. package/dist/{chunk-G3EDKRZH.js → chunk-PHGBG7OG.js} +100 -5
  9. package/dist/{chunk-Y2OHHNM2.js → chunk-PVFBY7KR.js} +0 -1
  10. package/dist/{chunk-3GHTHPZD.js → chunk-T43DM7Q6.js} +101 -3
  11. package/dist/{domEditingLayers-PYST4G45.js → domEditingLayers-SMVA3FEW.js} +3 -4
  12. package/dist/index.css +0 -1
  13. package/dist/index.d.ts +8 -5
  14. package/dist/index.html +1 -1
  15. package/dist/index.js +509 -529
  16. package/dist/player.js +4 -5
  17. package/dist/styles/tailwind-preset.js +1 -2
  18. package/dist/ui.js +3 -4
  19. package/package.json +8 -8
  20. package/src/captions/components/CaptionOverlay.tsx +1 -1
  21. package/src/captions/hooks/useCaptionSync.ts +1 -1
  22. package/src/components/LinkedSelectionToggle.tsx +2 -2
  23. package/src/components/LintModal.tsx +1 -5
  24. package/src/components/MediaPreview.tsx +2 -2
  25. package/src/components/ProjectUnreachableBanner.tsx +1 -1
  26. package/src/components/StudioLeftPanels.tsx +2 -2
  27. package/src/components/StudioRightPanels.tsx +1 -1
  28. package/src/components/TimelineToolbar.test.tsx +2 -2
  29. package/src/components/TimelineToolbar.tsx +6 -6
  30. package/src/components/editor/AnimationCard.tsx +3 -3
  31. package/src/components/editor/AnimationCardParts.tsx +1 -1
  32. package/src/components/editor/BlockParamsPanel.tsx +3 -3
  33. package/src/components/editor/ComputedTweenNotice.tsx +2 -2
  34. package/src/components/editor/FileTree.tsx +1 -1
  35. package/src/components/editor/GsapAnimationList.tsx +1 -1
  36. package/src/components/editor/GsapAnimationSection.tsx +2 -2
  37. package/src/components/editor/LayersPanel.test.ts +4 -4
  38. package/src/components/editor/LayersPanel.tsx +2 -2
  39. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  40. package/src/components/editor/PromotableControl.tsx +1 -1
  41. package/src/components/editor/PropertyPanel.tsx +2 -2
  42. package/src/components/editor/SnapToolbar.tsx +2 -2
  43. package/src/components/editor/TopologyLens.test.tsx +5 -4
  44. package/src/components/editor/Transform3DCube.tsx +1 -1
  45. package/src/components/editor/gsapAnimationConstants.ts +2 -2
  46. package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
  47. package/src/components/editor/gsapAnimationHelpers.ts +1 -1
  48. package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
  49. package/src/components/editor/propertyPanelFill.tsx +1 -1
  50. package/src/components/editor/propertyPanelFlatMotionSection.tsx +3 -3
  51. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
  52. package/src/components/editor/propertyPanelFlatSelectRow.tsx +1 -1
  53. package/src/components/editor/propertyPanelFlatTextSection.tsx +1 -1
  54. package/src/components/editor/propertyPanelFont.tsx +3 -3
  55. package/src/components/editor/propertyPanelFxBandRuler.tsx +2 -2
  56. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +2 -2
  57. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  58. package/src/components/editor/propertyPanelTransformCommit.ts +3 -3
  59. package/src/components/editor/useColorGradingController.ts +1 -1
  60. package/src/components/panels/SlideshowSubPanels.tsx +2 -2
  61. package/src/components/panels/VariablesBindElement.tsx +1 -1
  62. package/src/components/panels/VariablesPanel.tsx +3 -3
  63. package/src/components/renders/GpuEncodingToggle.tsx +28 -0
  64. package/src/components/renders/RenderQueue.test.tsx +20 -2
  65. package/src/components/renders/RenderQueue.tsx +15 -3
  66. package/src/components/renders/RenderQueuePanel.tsx +2 -1
  67. package/src/components/renders/renderSettings.ts +5 -2
  68. package/src/components/renders/useRenderQueue.ts +6 -3
  69. package/src/components/renders/useRenderQueueComposition.test.tsx +12 -0
  70. package/src/components/sidebar/AssetCard.tsx +2 -2
  71. package/src/components/sidebar/AudioRow.tsx +1 -1
  72. package/src/components/sidebar/BlocksTab.tsx +2 -2
  73. package/src/components/sidebar/CompositionsTab.tsx +2 -2
  74. package/src/components/sidebar/PromptPreviewModal.tsx +1 -1
  75. package/src/hooks/timelineAudioGroupCreate.ts +1 -1
  76. package/src/hooks/timelineEditingHelpers.ts +1 -1
  77. package/src/hooks/useBlockHandlers.ts +1 -1
  78. package/src/hooks/useConsoleErrorCapture.test.tsx +21 -2
  79. package/src/hooks/useConsoleErrorCapture.ts +3 -2
  80. package/src/hooks/useDomEditSession.ts +1 -1
  81. package/src/hooks/useEditorSave.ts +1 -1
  82. package/src/hooks/useFrameCapture.ts +1 -1
  83. package/src/hooks/useGestureCommit.ts +4 -4
  84. package/src/hooks/useGsapScriptCommits.ts +1 -1
  85. package/src/index.ts +1 -0
  86. package/src/player/components/AudioGainDialog.test.tsx +1 -1
  87. package/src/player/components/AudioGainDialog.tsx +2 -1
  88. package/src/player/components/EditModal.tsx +2 -2
  89. package/src/player/components/KeyframeDiamondContextMenu.tsx +1 -1
  90. package/src/player/components/OutOfSyncBadge.tsx +1 -1
  91. package/src/player/components/Player.test.ts +44 -0
  92. package/src/player/components/Player.tsx +12 -1
  93. package/src/player/components/ShortcutsPanel.tsx +2 -2
  94. package/src/player/components/Timeline.virtualization.test.tsx +1 -1
  95. package/src/player/components/TimelineAutomationLaneSlot.tsx +1 -1
  96. package/src/player/components/TimelineClip.tsx +1 -1
  97. package/src/player/components/TimelineFxButton.tsx +2 -2
  98. package/src/player/components/TimelineTrackHeader.test.tsx +3 -3
  99. package/src/player/components/timelineClipDragCommit.ts +2 -2
  100. package/src/player/components/trackHeaderLabelRows.tsx +1 -1
  101. package/src/player/lib/playbackAdapter.ts +1 -1
  102. package/src/player/lib/playbackTypes.ts +1 -0
  103. package/src/player/sceneSwap.test.ts +27 -1
  104. package/src/player/sceneSwap.ts +16 -2
  105. package/src/utils/hmrStableContext.ts +1 -1
  106. package/src/utils/razorSplitTransaction.ts +0 -2
  107. package/src/utils/sdkCutover.linkedTiming.test.ts +1 -1
  108. package/dist/chunk-3GHTHPZD.js.map +0 -1
  109. package/dist/chunk-EEXSYY5F.js.map +0 -1
  110. package/dist/chunk-G3EDKRZH.js.map +0 -1
  111. package/dist/chunk-PEAKCOUW.js.map +0 -1
  112. package/dist/chunk-PZ5AY32C.js.map +0 -1
  113. package/dist/chunk-Y2OHHNM2.js.map +0 -1
  114. package/dist/domEditingLayers-PYST4G45.js.map +0 -1
  115. package/dist/index.css.map +0 -1
  116. package/dist/index.js.map +0 -1
  117. package/dist/player.js.map +0 -1
  118. package/dist/styles/tailwind-preset.js.map +0 -1
  119. package/dist/ui.js.map +0 -1
package/dist/player.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import {
2
2
  Player
3
- } from "./chunk-G3EDKRZH.js";
4
- import "./chunk-Y2OHHNM2.js";
5
- import "./chunk-3GHTHPZD.js";
6
- import "./chunk-PZ5AY32C.js";
3
+ } from "./chunk-PHGBG7OG.js";
4
+ import "./chunk-PVFBY7KR.js";
5
+ import "./chunk-T43DM7Q6.js";
6
+ import "./chunk-MLKGABMK.js";
7
7
  export {
8
8
  Player
9
9
  };
10
- //# sourceMappingURL=player.js.map
@@ -1,4 +1,4 @@
1
- import "../chunk-PZ5AY32C.js";
1
+ import "../chunk-MLKGABMK.js";
2
2
 
3
3
  // src/styles/tailwind-preset.shared.js
4
4
  var studioPreset = {
@@ -47,4 +47,3 @@ var tailwind_preset_default = tailwind_preset_shared_default;
47
47
  export {
48
48
  tailwind_preset_default as default
49
49
  };
50
- //# sourceMappingURL=tailwind-preset.js.map
package/dist/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HyperframesLogo
3
- } from "./chunk-EEXSYY5F.js";
3
+ } from "./chunk-C5BDT7NG.js";
4
4
  import {
5
5
  Button,
6
6
  ContextMenu,
@@ -30,8 +30,8 @@ import {
30
30
  fieldBase,
31
31
  fieldText,
32
32
  popupSurface
33
- } from "./chunk-Y2OHHNM2.js";
34
- import "./chunk-PZ5AY32C.js";
33
+ } from "./chunk-PVFBY7KR.js";
34
+ import "./chunk-MLKGABMK.js";
35
35
  export {
36
36
  Button,
37
37
  ContextMenu,
@@ -63,4 +63,3 @@ export {
63
63
  fieldText,
64
64
  popupSurface
65
65
  };
66
- //# sourceMappingURL=ui.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.144",
3
+ "version": "0.8.146",
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.144",
64
- "@hyperframes/parsers": "0.8.144",
65
- "@hyperframes/sdk": "0.8.144",
66
- "@hyperframes/player": "0.8.144",
67
- "@hyperframes/shader-transitions": "0.8.144",
68
- "@hyperframes/studio-server": "0.8.144"
63
+ "@hyperframes/core": "0.8.146",
64
+ "@hyperframes/parsers": "0.8.146",
65
+ "@hyperframes/player": "0.8.146",
66
+ "@hyperframes/sdk": "0.8.146",
67
+ "@hyperframes/studio-server": "0.8.146",
68
+ "@hyperframes/shader-transitions": "0.8.146"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@tailwindcss/vite": "^4.3.3",
@@ -82,7 +82,7 @@
82
82
  "vite": "^6.4.2",
83
83
  "vitest": "^4.1.11",
84
84
  "zustand": "^5.0.0",
85
- "@hyperframes/producer": "0.8.144"
85
+ "@hyperframes/producer": "0.8.146"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "react": "19",
@@ -408,7 +408,7 @@ export const CaptionOverlay = memo(function CaptionOverlay({ iframeRef }: Captio
408
408
  {wordBoxes.length === 0 && model && model.segments.size > 0 && (
409
409
  <div className="absolute inset-x-0 top-3 flex justify-center pointer-events-none">
410
410
  <span className="px-2.5 py-1 rounded-full bg-black/60 border border-neutral-700 text-2xs text-neutral-300">
411
- No captions visible at this frame — scrub to a caption, or select one in the track below
411
+ No captions visible at this frame. Scrub to a caption, or select one in the track below
412
412
  </span>
413
413
  </div>
414
414
  )}
@@ -235,7 +235,7 @@ export function useCaptionSync(projectId: string | null) {
235
235
  // File exists but is unreadable — previous edits would silently not load.
236
236
  useCaptionStore
237
237
  .getState()
238
- .setSyncError("caption-overrides.json is corrupt — earlier caption edits didn't load");
238
+ .setSyncError("caption-overrides.json is corrupt, so earlier caption edits didn't load");
239
239
  }
240
240
  }, []);
241
241
 
@@ -50,8 +50,8 @@ export function LinkedSelectionToggle() {
50
50
  <Tooltip
51
51
  label={
52
52
  linkedSelection
53
- ? "Linked Selection on — a click selects both halves (⌥-click for one)"
54
- : "Linked Selection off — a click selects one clip"
53
+ ? "Linked Selection on: a click selects both halves (⌥-click for one)"
54
+ : "Linked Selection off: a click selects one clip"
55
55
  }
56
56
  >
57
57
  <button
@@ -113,11 +113,7 @@ export function LintModal({
113
113
  : "bg-accent hover:bg-accent-hover text-on-accent"
114
114
  }`}
115
115
  >
116
- {copied
117
- ? "Copied!"
118
- : copyFailed
119
- ? "Copy failed — check permissions"
120
- : "Copy to Agent"}
116
+ {copied ? "Copied!" : copyFailed ? "Copy failed. Check permissions" : "Copy to Agent"}
121
117
  </button>
122
118
  </div>
123
119
  )}
@@ -23,7 +23,7 @@ function MediaErrorPanel({ name, filePath }: { name: string; filePath: string })
23
23
  <span className="text-sm text-neutral-400 font-medium">{name}</span>
24
24
  <span className="text-[11px] text-neutral-600 font-mono">{filePath}</span>
25
25
  <span className="text-[10px] text-neutral-500">
26
- Couldn't load this file — it may be missing or corrupt
26
+ Couldn't load this file. It may be missing or corrupt
27
27
  </span>
28
28
  </div>
29
29
  );
@@ -110,7 +110,7 @@ export function MediaPreview({ projectId, filePath }: { projectId: string; fileP
110
110
  </svg>
111
111
  <span className="text-sm text-neutral-400 font-medium">{name}</span>
112
112
  <span className="text-[11px] text-neutral-600 font-mono">{filePath}</span>
113
- <span className="text-[10px] text-neutral-600">Binary file — preview not available</span>
113
+ <span className="text-[10px] text-neutral-600">Binary file: preview not available</span>
114
114
  </div>
115
115
  );
116
116
  }
@@ -82,7 +82,7 @@ export function ProjectUnreachableBanner({ projectId }: ProjectUnreachableBanner
82
82
  </button>
83
83
  </>
84
84
  ) : (
85
- <span>Couldn&apos;t open this project — it may have been renamed, moved, or deleted.</span>
85
+ <span>Couldn&apos;t open this project. It may have been renamed, moved, or deleted.</span>
86
86
  )}
87
87
  </div>
88
88
  );
@@ -75,8 +75,8 @@ export const StudioLeftPanels = memo(function StudioLeftPanels({
75
75
  return;
76
76
  }
77
77
  await waitForPendingDomEditSaves();
78
- const { format, quality, fps } = getPersistedRenderSettings();
79
- await renderQueue.startRender({ composition: comp, format, quality, fps });
78
+ const { format, quality, fps, gpu } = getPersistedRenderSettings();
79
+ await renderQueue.startRender({ composition: comp, format, quality, fps, gpu });
80
80
  },
81
81
  [renderQueue, waitForPendingDomEditSaves],
82
82
  );
@@ -174,7 +174,7 @@ export const StudioRightPanels = memo(function StudioRightPanels({
174
174
  // timeline offers that write as a mute. Checked here as well as in the panel
175
175
  // because the button is not the only caller.
176
176
  if (!canHideSelections(domEditGroupSelections)) {
177
- showToast("Audio can't be hidden — use the group's own controls", "info");
177
+ showToast("Audio can't be hidden. Use the group's own controls", "info");
178
178
  return;
179
179
  }
180
180
  const { elements } = usePlayerStore.getState();
@@ -82,7 +82,7 @@ describe("TimelineToolbar — adaptive thumbnails", () => {
82
82
  it("keeps a user-controlled hidden mode as the rollback path", () => {
83
83
  const { host, root } = renderToolbar();
84
84
  const button = host.querySelector<HTMLButtonElement>(
85
- 'button[aria-label="Hide thumbnails — labels only"]',
85
+ 'button[aria-label="Hide thumbnails: labels only"]',
86
86
  );
87
87
  if (!button) throw new Error("thumbnail toggle not rendered");
88
88
 
@@ -90,7 +90,7 @@ describe("TimelineToolbar — adaptive thumbnails", () => {
90
90
 
91
91
  expect(usePlayerStore.getState().thumbnailMode).toBe("hidden");
92
92
  expect(button.getAttribute("aria-label")).toBe(
93
- "Show thumbnails — posters stay visible; richer previews appear on interaction",
93
+ "Show thumbnails: posters stay visible; richer previews appear on interaction",
94
94
  );
95
95
  act(() => root.unmount());
96
96
  });
@@ -235,8 +235,8 @@ export const TimelineToolbar = memo(function TimelineToolbar({
235
235
  <Tooltip
236
236
  label={
237
237
  rippleEditEnabled
238
- ? "Ripple on — keeps the main track gapless"
239
- : "Ripple off — deleting a main-track clip leaves a gap"
238
+ ? "Ripple on: keeps the main track gapless"
239
+ : "Ripple off: deleting a main-track clip leaves a gap"
240
240
  }
241
241
  >
242
242
  <button
@@ -465,16 +465,16 @@ export const TimelineToolbar = memo(function TimelineToolbar({
465
465
  <Tooltip
466
466
  label={
467
467
  thumbnailsVisible
468
- ? "Hide thumbnails — labels only"
469
- : "Show thumbnails — posters stay visible; richer previews appear on interaction"
468
+ ? "Hide thumbnails: labels only"
469
+ : "Show thumbnails: posters stay visible; richer previews appear on interaction"
470
470
  }
471
471
  >
472
472
  <button
473
473
  type="button"
474
474
  aria-label={
475
475
  thumbnailsVisible
476
- ? "Hide thumbnails — labels only"
477
- : "Show thumbnails — posters stay visible; richer previews appear on interaction"
476
+ ? "Hide thumbnails: labels only"
477
+ : "Show thumbnails: posters stay visible; richer previews appear on interaction"
478
478
  }
479
479
  aria-pressed={thumbnailsVisible}
480
480
  onClick={() => setThumbnailMode(thumbnailsVisible ? "hidden" : "adaptive")}
@@ -217,7 +217,7 @@ export const AnimationCard = memo(function AnimationCard({
217
217
  {typeof animation.position === "number"
218
218
  ? `${parseFloat(animation.position.toFixed(3))}s`
219
219
  : animation.position}{" "}
220
- – {typeof endTime === "number" ? `${parseFloat(endTime.toFixed(3))}s` : endTime}
220
+ to {typeof endTime === "number" ? `${parseFloat(endTime.toFixed(3))}s` : endTime}
221
221
  </span>
222
222
  <span
223
223
  className={`ml-auto text-[10px] ${flat ? "text-panel-text-3" : "text-neutral-500"}`}
@@ -256,7 +256,7 @@ export const AnimationCard = memo(function AnimationCard({
256
256
  clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
257
257
  }}
258
258
  />
259
- Keyframed — click a segment below to edit its curve
259
+ Keyframed: click a segment below to edit its curve
260
260
  </p>
261
261
  )}
262
262
  {runEase && runEase !== "none" && (
@@ -273,7 +273,7 @@ export const AnimationCard = memo(function AnimationCard({
273
273
  setTimeout(() => setCopied(false), 1500);
274
274
  }}
275
275
  className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
276
- title="Copy description to clipboard — paste into agent prompts"
276
+ title="Copy description for agent prompts"
277
277
  >
278
278
  {copied ? "Copied" : "Copy"}
279
279
  </button>
@@ -93,7 +93,7 @@ export function PropertyRow({
93
93
  onClick={() => onCommit(isVisible ? "hidden" : "visible")}
94
94
  className="shrink-0 rounded-full transition-colors duration-200 relative"
95
95
  style={{ width: 28, height: 16, background: isVisible ? P.accent : P.borderInput }}
96
- title={isVisible ? "Visible — click to hide" : "Hidden — click to show"}
96
+ title={isVisible ? "Visible: click to hide" : "Hidden: click to show"}
97
97
  >
98
98
  <span
99
99
  className="absolute top-[2px] left-0 rounded-full transition-transform duration-200"
@@ -58,7 +58,7 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
58
58
  if (matches === 0) {
59
59
  setCommitState({
60
60
  tone: "error",
61
- message: `Couldn't find the current value in ${compositionPath} — it may have been edited by hand.`,
61
+ message: `Couldn't find the current value in ${compositionPath}. It may have been edited by hand.`,
62
62
  });
63
63
  trackBlockParamCommit({ tone: "error", blockName, key });
64
64
  return;
@@ -73,7 +73,7 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
73
73
  if (matches > 1) {
74
74
  setCommitState({
75
75
  tone: "error",
76
- message: `"${previous}" appears ${matches}× in ${compositionPath} — the panel can't tell which one belongs to this parameter, so it won't risk changing unrelated content. Edit the file directly to disambiguate.`,
76
+ message: `"${previous}" appears ${matches}× in ${compositionPath}. The panel can't tell which one belongs to this parameter, so it won't risk changing unrelated content. Edit the file directly to disambiguate.`,
77
77
  });
78
78
  trackBlockParamCommit({ tone: "error", blockName, key });
79
79
  return;
@@ -171,7 +171,7 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
171
171
  )}
172
172
  {!fileManager && params.length > 0 && (
173
173
  <div className="text-[10px] text-warning-ink">
174
- Block params can't be edited here — no project file access.
174
+ Block params can't be edited here: no project file access.
175
175
  </div>
176
176
  )}
177
177
  {params.map((param) => (
@@ -17,14 +17,14 @@ export function ComputedTweenNotice({
17
17
  if (editability === "source") {
18
18
  return (
19
19
  <div className="rounded-md border border-neutral-800 bg-neutral-900/50 px-2 py-1.5 text-[9px] text-neutral-400">
20
- Computed value — edit it in the Code tab.
20
+ Computed value. Edit it in the Code tab.
21
21
  </div>
22
22
  );
23
23
  }
24
24
  const source = provenance?.fn ? `${provenance.fn}()` : "a loop";
25
25
  return (
26
26
  <div className="flex items-center justify-between gap-2 rounded-md border border-neutral-800 bg-neutral-900/50 px-2 py-1.5 text-[9px] text-neutral-400">
27
- <span>Generated by {source} — not directly editable.</span>
27
+ <span>Generated by {source}, not directly editable.</span>
28
28
  {onUnroll && (
29
29
  <button
30
30
  type="button"
@@ -293,7 +293,7 @@ export const FileTree = memo(function FileTree({
293
293
  )}
294
294
  {children.length === 0 && !inlineInput && (
295
295
  <div className="px-3 py-4 text-center text-[11px] text-neutral-600">
296
- No files yet{hasFileOps ? " — use + above to create one" : ""}.
296
+ No files yet{hasFileOps ? ". Use + above to create one" : ""}.
297
297
  </div>
298
298
  )}
299
299
  {children.map((child) =>
@@ -63,7 +63,7 @@ export function GsapAnimationList({
63
63
  <div className="space-y-2">
64
64
  {animations.length === 0 && (
65
65
  <p className="text-[11px] leading-4 text-neutral-500">
66
- No animations on this element yet — add an effect below to animate it.
66
+ No animations on this element yet. Add an effect below to animate it.
67
67
  </p>
68
68
  )}
69
69
  {animations.map((animation, index) => (
@@ -25,8 +25,8 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
25
25
  <Section title="Animation" icon={<Film size={15} />}>
26
26
  {multipleTimelines && (
27
27
  <p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-warning-ink">
28
- This file has multiple GSAP timelines. Animation editing is disabled to prevent data loss
29
- — consolidate into a single timeline to enable editing.
28
+ This file has multiple GSAP timelines. Animation editing is disabled to prevent data loss.
29
+ Consolidate into a single timeline to enable editing.
30
30
  </p>
31
31
  )}
32
32
  {unsupportedTimelinePattern && (
@@ -9,7 +9,7 @@ import type { DomEditLayerItem } from "./domEditingTypes";
9
9
  import { createRafThrottle, LayersPanel, sortLayersByZIndex } from "./LayersPanel";
10
10
  import { isLayerDraggable } from "./useLayerDrag";
11
11
  import { liveTime } from "../../player";
12
- import { sceneSwapFor } from "../../player/sceneSwap";
12
+ import { announcePreviewDocumentLoaded, sceneSwapFor } from "../../player/sceneSwap";
13
13
 
14
14
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
15
15
 
@@ -284,12 +284,12 @@ describe("LayersPanel preview promotion", () => {
284
284
  mocks.collect.mockClear();
285
285
 
286
286
  act(() => {
287
- a.dispatchEvent(new Event("load"));
287
+ announcePreviewDocumentLoaded(a);
288
288
  });
289
289
  expect(mocks.collect).not.toHaveBeenCalled();
290
290
 
291
291
  act(() => {
292
- b.dispatchEvent(new Event("load"));
292
+ announcePreviewDocumentLoaded(b);
293
293
  });
294
294
  expect(collectedRootIds()).toEqual(["b"]);
295
295
  });
@@ -325,7 +325,7 @@ describe("LayersPanel preview promotion", () => {
325
325
  mocks.previewIframeRef.current = iframe;
326
326
  usePreviewIframeStore.setState({ iframe });
327
327
  act(() => root.render(createElement(LayersPanel)));
328
- act(() => void iframe.dispatchEvent(new Event("load")));
328
+ act(() => void announcePreviewDocumentLoaded(iframe));
329
329
 
330
330
  const rows = [...host.querySelectorAll("[data-layer-index]")];
331
331
  expect(rows).toHaveLength(2);
@@ -89,8 +89,8 @@ interface CollapsedState {
89
89
  [key: string]: boolean;
90
90
  }
91
91
 
92
- /** Studio state the panel reads; passed by hosts outside Studio's providers. Keep previewIframeRef stable,
93
- * and bump refreshKey when the preview document is rebuilt without an iframe load. */
92
+ /** Studio state the panel reads; passed by hosts outside Studio's providers. Keep previewIframeRef stable;
93
+ * a host with its own player calls announcePreviewDocumentLoaded on each new page, or bumps refreshKey. */
94
94
  export type LayersPanelHost = Pick<
95
95
  StudioShellValue,
96
96
  "previewIframeRef" | "activeCompPath" | "showToast"
@@ -127,7 +127,7 @@ export function OffCanvasIndicators({
127
127
  aria-label={`Select off-canvas element ${r.key}`}
128
128
  className="pointer-events-auto absolute inset-0 border-2 border-dashed border-studio-accent/10 rounded-md cursor-pointer hover:border-studio-accent hover:bg-studio-accent/10 transition-colors"
129
129
  style={clipOutside ? { clipPath: clipOutside } : undefined}
130
- title={`Off-canvas: ${r.key} — click to select`}
130
+ title={`Off-canvas: ${r.key}. Click to select`}
131
131
  onClick={handleClick}
132
132
  onKeyDown={(e) => {
133
133
  if (e.key === "Enter" || e.key === " ") {
@@ -50,7 +50,7 @@ export function PromotableControl({
50
50
  useEffect(() => {
51
51
  if (danglingId != null) {
52
52
  console.warn(
53
- `[hyperframes] Control is bound to variable "${danglingId}", but no such declaration exists. The element still carries the binding on disk — re-declare the variable or unbind the element.`,
53
+ `[hyperframes] Control is bound to variable "${danglingId}", but no such declaration exists. The element still carries the binding on disk. Re-declare the variable or unbind the element.`,
54
54
  );
55
55
  }
56
56
  }, [danglingId]);
@@ -240,13 +240,13 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
240
240
  navigator.clipboard
241
241
  .writeText(text)
242
242
  .then(() => {
243
- showToast(`Copied element info for ${element.label} — paste into any AI agent`, "info");
243
+ showToast(`Copied element info for ${element.label}. Paste it into any AI agent`, "info");
244
244
  setClipboardCopied(true);
245
245
  clearTimeout(clipboardTimerRef.current);
246
246
  clipboardTimerRef.current = setTimeout(() => setClipboardCopied(false), 1500);
247
247
  })
248
248
  .catch(() => {
249
- showToast("Couldn't copy to the clipboard — check browser permissions", "error");
249
+ showToast("Couldn't copy to the clipboard. Check browser permissions", "error");
250
250
  });
251
251
  };
252
252
 
@@ -160,8 +160,8 @@ export const SnapToolbar = memo(function SnapToolbar() {
160
160
  }}
161
161
  title={
162
162
  prefs.gridVisible
163
- ? "Grid visible (G) — right-click for spacing options"
164
- : "Grid hidden (G) — right-click for spacing options"
163
+ ? "Grid visible (G). Right-click for spacing options"
164
+ : "Grid hidden (G). Right-click for spacing options"
165
165
  }
166
166
  aria-label="Toggle grid"
167
167
  >
@@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { studioEditLifecycle, type StudioWriteResult } from "../../webmcp/writeCoordinator";
7
7
  import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
8
+ import { announcePreviewDocumentLoaded } from "../../player/sceneSwap";
8
9
  import { TopologyLens } from "./TopologyLens";
9
10
 
10
11
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
@@ -259,7 +260,7 @@ describe("TopologyLens", () => {
259
260
  mount();
260
261
  begin();
261
262
 
262
- act(() => iframe?.dispatchEvent(new Event("load")));
263
+ act(() => announcePreviewDocumentLoaded(iframe!));
263
264
  expect(host?.querySelector('[data-topology-lens="hidden"]')).not.toBeNull();
264
265
 
265
266
  begin();
@@ -286,7 +287,7 @@ describe("TopologyLens", () => {
286
287
  root = null;
287
288
 
288
289
  expect(vi.getTimerCount()).toBe(1);
289
- expect(removeListener).toHaveBeenCalledWith("load", expect.any(Function));
290
+ expect(removeListener).toHaveBeenCalledWith("hf-preview-document-loaded", expect.any(Function));
290
291
  expect(studioEditLifecycle.getSnapshot()).toMatchObject({ phase: "dispatching" });
291
292
  act(() => vi.advanceTimersByTime(0));
292
293
  expect(vi.getTimerCount()).toBe(0);
@@ -312,10 +313,10 @@ describe("TopologyLens", () => {
312
313
  ref.current = b;
313
314
  usePreviewIframeStore.getState().setIframe(b);
314
315
  });
315
- act(() => a.dispatchEvent(new Event("load")));
316
+ act(() => announcePreviewDocumentLoaded(a));
316
317
  expect(host.querySelector('[data-topology-lens="hidden"]')).toBeNull();
317
318
 
318
- act(() => b.dispatchEvent(new Event("load")));
319
+ act(() => announcePreviewDocumentLoaded(b));
319
320
  expect(host.querySelector('[data-topology-lens="hidden"]')).not.toBeNull();
320
321
  usePreviewIframeStore.setState({ iframe: null });
321
322
  });
@@ -308,7 +308,7 @@ export function Transform3DCube({
308
308
  onClick={onKeyframe}
309
309
  title={
310
310
  keyframed
311
- ? "3D transform is keyframed — click a field diamond to add keyframes"
311
+ ? "3D transform is keyframed. Click a field diamond to add keyframes"
312
312
  : "Keyframe the 3D transform (animate it over time)"
313
313
  }
314
314
  aria-label="Keyframe 3D transform"
@@ -8,7 +8,7 @@ export const METHOD_LABELS: Record<string, string> = {
8
8
  };
9
9
 
10
10
  export const METHOD_TOOLTIPS: Record<string, string> = {
11
- set: "Instantly snap to these values — no transition",
11
+ set: "Instantly snap to these values, no transition",
12
12
  to: "Smoothly animate the element to these target values",
13
13
  from: "Element starts at these values and transitions to its normal state",
14
14
  fromTo: "Animate from one state to another",
@@ -83,7 +83,7 @@ export const PROP_TOOLTIPS: Record<string, string> = {
83
83
  perspective:
84
84
  "3D depth context for child elements; set it on a parent when rotating children in 3D",
85
85
  transformPerspective:
86
- "3D depth for THIS element's own X/Y rotation — lower = stronger perspective (try 600–1000)",
86
+ "3D depth for THIS element's own X/Y rotation: lower = stronger perspective (try 600 to 1000)",
87
87
  transformOrigin: "Pivot point for transforms, for example center center or 50% 50%",
88
88
  width: "Element width",
89
89
  height: "Element height",
@@ -109,7 +109,7 @@ describe("buildTweenSummary", () => {
109
109
  const s = buildTweenSummary(
110
110
  anim({ method: "fromTo", fromProperties: {}, properties: { scale: 2 } }),
111
111
  );
112
- expect(s).toContain("from [—]");
112
+ expect(s).toContain("from [none]");
113
113
  });
114
114
 
115
115
  it("handles no properties", () => {
@@ -64,7 +64,7 @@ export function buildTweenSummary(animation: GsapAnimation): string {
64
64
  const label = (PROP_LABELS[p] ?? p).toLowerCase();
65
65
  return `${label} ${formatPropValue(p, v)}`;
66
66
  });
67
- const fromText = fromDescs.length > 0 ? fromDescs.join(", ") : "—";
67
+ const fromText = fromDescs.length > 0 ? fromDescs.join(", ") : "none";
68
68
  return `Starting at ${pos}s, over ${dur}s, ${target} animates from [${fromText}] to [${propText}] using a ${ease.toLowerCase()} curve.`;
69
69
  }
70
70
  return `Starting at ${pos}s, over ${dur}s, animate ${target}'s ${propText} using a ${ease.toLowerCase()} curve.`;
@@ -423,7 +423,7 @@ export function ColorGradingControls({
423
423
  void actions
424
424
  .importLut(files, onImportAssets, () => track("button", "Import LUT"))
425
425
  .catch(() =>
426
- setLutImportError("LUT import failed — check the .cube file and try again."),
426
+ setLutImportError("LUT import failed. Check the .cube file and try again."),
427
427
  )
428
428
  .finally(() => setLutImporting(false));
429
429
  event.currentTarget.value = "";
@@ -135,7 +135,7 @@ export function ImageFillField({
135
135
  commitImage(toProjectRootAssetPath(nextImage), true);
136
136
  }
137
137
  } catch {
138
- setUploadError("Upload failed — check the file and try again.");
138
+ setUploadError("Upload failed. Check the file and try again.");
139
139
  } finally {
140
140
  setUploading(false);
141
141
  }
@@ -100,7 +100,7 @@ export function FlatTimingRow({
100
100
  {cell("Duration", formatTimingValue(duration), commitDuration)}
101
101
  {derived && (
102
102
  <p className="col-span-3 mt-1 text-[10px] leading-snug text-panel-text-3">
103
- Inferred from this element's animation — edit to pin an explicit clip range.
103
+ Inferred from this element's animation. Edit to pin an explicit clip range.
104
104
  </p>
105
105
  )}
106
106
  </div>
@@ -150,7 +150,7 @@ export function FlatMotionSection({
150
150
  {multipleTimelines && (
151
151
  <p className="rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-warning-ink">
152
152
  This file has multiple GSAP timelines. Animation editing is disabled to prevent data
153
- loss — consolidate into a single timeline to enable editing.
153
+ loss. Consolidate into a single timeline to enable editing.
154
154
  </p>
155
155
  )}
156
156
  {unsupportedTimelinePattern && (
@@ -195,7 +195,7 @@ export function motionSectionLabel(args: {
195
195
  if (args.timingOnly) {
196
196
  return {
197
197
  title: "Timing",
198
- summary: `${formatTimingValue(args.start)} – ${formatTimingValue(args.start + args.duration)}`,
198
+ summary: `${formatTimingValue(args.start)} to ${formatTimingValue(args.start + args.duration)}`,
199
199
  };
200
200
  }
201
201
  return {
@@ -72,7 +72,7 @@ export function FlatRow({
72
72
  <button
73
73
  type="button"
74
74
  data-flat-row-reset="true"
75
- title="Remove — fall back to default"
75
+ title="Remove and fall back to default"
76
76
  onClick={() => {
77
77
  track("button", `Reset ${label}`);
78
78
  onReset();
@@ -556,7 +556,7 @@ export function FlatSlider({
556
556
  <button
557
557
  type="button"
558
558
  data-flat-slider-reset="true"
559
- title="Remove — fall back to default"
559
+ title="Remove and fall back to default"
560
560
  disabled={disabled}
561
561
  onClick={() => {
562
562
  track("button", `Reset ${label}`);
@@ -90,7 +90,7 @@ export function FlatSelectRow({
90
90
  <button
91
91
  type="button"
92
92
  data-flat-select-reset="true"
93
- title="Remove — fall back to default"
93
+ title="Remove and fall back to default"
94
94
  disabled={disabled}
95
95
  onClick={() => {
96
96
  track("button", `Reset ${trackName}`);
@@ -35,7 +35,7 @@ const ALIGN_OPTIONS = [
35
35
  ];
36
36
 
37
37
  const CASE_OPTIONS = [
38
- { key: "none", label: "none", node: "–" },
38
+ { key: "none", label: "none", node: "None" },
39
39
  { key: "uppercase", label: "uppercase", node: "AG" },
40
40
  { key: "lowercase", label: "lowercase", node: "ag" },
41
41
  { key: "capitalize", label: "capitalize", node: "Ag" },