@hyperframes/studio 0.8.96 → 0.8.97

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 (69) hide show
  1. package/dist/assets/{hyperframes-player-ClfxdXeC.js → hyperframes-player-Biu5_W0l.js} +1 -1
  2. package/dist/assets/{index-D8ztHPd3.js → index-B5Z-1RW9.js} +1 -1
  3. package/dist/assets/{index-CSHEHeOo.js → index-BdV5T-QI.js} +200 -194
  4. package/dist/assets/{index-BNQLP5_v.js → index-CWuozokW.js} +1 -1
  5. package/dist/assets/{index-Dx79V1Se.css → index-xB5O0hPw.css} +1 -1
  6. package/dist/{chunk-QZ6HNNFG.js → chunk-6GAO75IU.js} +16 -4
  7. package/dist/chunk-6GAO75IU.js.map +1 -0
  8. package/dist/{chunk-PQFEGZWM.js → chunk-MTAVNAGW.js} +2 -2
  9. package/dist/chunk-MTAVNAGW.js.map +1 -0
  10. package/dist/{chunk-QPSOTK6M.js → chunk-U3EE4ACX.js} +5 -5
  11. package/dist/chunk-U3EE4ACX.js.map +1 -0
  12. package/dist/{domEditingLayers-3BAP6NNO.js → domEditingLayers-NU6Z5QWA.js} +2 -2
  13. package/dist/index.d.ts +7 -2
  14. package/dist/index.html +2 -2
  15. package/dist/index.js +1981 -1779
  16. package/dist/index.js.map +1 -1
  17. package/dist/player.js +2 -2
  18. package/dist/styles.css +1 -1
  19. package/dist/ui.d.ts +3 -1
  20. package/dist/ui.js +1 -1
  21. package/package.json +8 -8
  22. package/src/components/TimelineToolPicker.tsx +19 -15
  23. package/src/components/TimelineToolbar.test.tsx +62 -0
  24. package/src/components/TimelineToolbar.tsx +3 -1
  25. package/src/components/editor/domEditing.test.ts +9 -1
  26. package/src/components/editor/domEditingDom.ts +2 -1
  27. package/src/components/editor/fontAssets.test.ts +69 -0
  28. package/src/components/editor/fontAssets.ts +11 -2
  29. package/src/components/ui/Menu.test.tsx +21 -0
  30. package/src/components/ui/Popover.tsx +21 -1
  31. package/src/hooks/domSelectionTimelineMirror.ts +5 -4
  32. package/src/hooks/domStyleCommit.fonts.test.ts +55 -0
  33. package/src/hooks/useAppHotkeys.test.ts +7 -5
  34. package/src/hooks/useClipboard.ts +25 -7
  35. package/src/hooks/useDomSelection.test.ts +56 -32
  36. package/src/hooks/useDomSelection.ts +4 -3
  37. package/src/player/components/BeatStrip.tsx +6 -3
  38. package/src/player/components/Timeline.readOnly.test.tsx +670 -0
  39. package/src/player/components/Timeline.rulerSeek.test.tsx +1 -25
  40. package/src/player/components/TimelineAutomationLane.test.tsx +23 -0
  41. package/src/player/components/TimelineClipDiamonds.tsx +7 -2
  42. package/src/player/components/TimelineFxButton.tsx +3 -0
  43. package/src/player/components/TimelineLanes.tsx +3 -2
  44. package/src/player/components/TimelineProvider.tsx +23 -7
  45. package/src/player/components/TimelineTypes.ts +2 -0
  46. package/src/player/components/studioShortcuts.ts +1 -1
  47. package/src/player/components/timelineClipGestureHandlers.ts +3 -1
  48. package/src/player/components/timelineDragDrop.ts +11 -1
  49. package/src/player/components/timelineEditCapabilities.ts +1 -0
  50. package/src/player/components/timelineMountTestEnv.ts +24 -0
  51. package/src/player/components/timelineReadOnly.ts +64 -0
  52. package/src/player/components/useAutomationLaneGestures.ts +13 -2
  53. package/src/player/components/useTimelineKeyframeHandlers.ts +7 -0
  54. package/src/player/components/useTimelineProviderState.tsx +42 -14
  55. package/src/player/hooks/useShadowPreviewReload.ts +2 -2
  56. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +69 -0
  57. package/src/player/hooks/useTimelinePlayer.ts +6 -0
  58. package/src/player/lib/timelineDOM.test.ts +25 -0
  59. package/src/player/lib/timelineDOM.ts +4 -2
  60. package/src/player/lib/timelineIframeHelpers.test.ts +15 -0
  61. package/src/player/store/playerStore.ts +4 -4
  62. package/src/utils/clipboardLook.test.ts +130 -0
  63. package/src/utils/clipboardLook.ts +82 -0
  64. package/src/utils/clipboardPayload.ts +8 -2
  65. package/src/utils/studioFontHelpers.ts +6 -3
  66. package/dist/chunk-PQFEGZWM.js.map +0 -1
  67. package/dist/chunk-QPSOTK6M.js.map +0 -1
  68. package/dist/chunk-QZ6HNNFG.js.map +0 -1
  69. /package/dist/{domEditingLayers-3BAP6NNO.js.map → domEditingLayers-NU6Z5QWA.js.map} +0 -0
@@ -29,9 +29,13 @@ function renderHarness(
29
29
  cleanup: () => void;
30
30
  timeline: TimelineSpies;
31
31
  } {
32
+ // Reads back what was last published, as the timeline store does.
33
+ let publishedSet: ReadonlySet<string> = new Set();
32
34
  const timeline: TimelineSpies = {
33
35
  setSelectedTimelineElementId: vi.fn(),
34
- setTimelineSelectionSet: vi.fn(),
36
+ setTimelineSelectionSet: vi.fn((ids: Set<string>) => {
37
+ publishedSet = ids;
38
+ }),
35
39
  };
36
40
  const host = document.createElement("div");
37
41
  document.body.append(host);
@@ -47,7 +51,7 @@ function renderHarness(
47
51
  captionEditMode: false,
48
52
  previewIframeRef: { current: null },
49
53
  timelineElements: options.timelineElements ?? [],
50
- getTimelineSelectionSet: () => new Set(),
54
+ getTimelineSelectionSet: () => publishedSet,
51
55
  setSelectedTimelineElementId: timeline.setSelectedTimelineElementId,
52
56
  setTimelineSelectionSet: timeline.setTimelineSelectionSet,
53
57
  setRightCollapsed: vi.fn(),
@@ -107,6 +111,17 @@ function timelineElement(domId: string): TimelineElement {
107
111
  } as TimelineElement;
108
112
  }
109
113
 
114
+ function renderCardAndChip() {
115
+ const card = makeSelection("Card", Object.assign(document.createElement("div"), { id: "card" }));
116
+ const chip = makeSelection("Chip", Object.assign(document.createElement("div"), { id: "chip" }));
117
+ document.body.append(card.element, chip.element);
118
+ const harness = renderHarness(
119
+ { activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
120
+ { timelineElements: [timelineElement("card"), timelineElement("chip")] },
121
+ );
122
+ return { harness, card, chip };
123
+ }
124
+
110
125
  /**
111
126
  * A marquee builds the group correctly and then used to lose it: it announced only
112
127
  * the primary to the timeline, the timeline is the source of truth for what is
@@ -116,24 +131,9 @@ function timelineElement(domId: string): TimelineElement {
116
131
  */
117
132
  describe("useDomSelection marquee", () => {
118
133
  it("announces every marquee'd element to the timeline, anchored on the primary", () => {
119
- const first = document.createElement("div");
120
- first.id = "card";
121
- const second = document.createElement("div");
122
- second.id = "chip";
123
- document.body.append(first, second);
124
- const harness = renderHarness(
125
- { activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
126
- { timelineElements: [timelineElement("card"), timelineElement("chip")] },
127
- );
134
+ const { harness, card, chip } = renderCardAndChip();
128
135
 
129
- act(() =>
130
- harness
131
- .current()
132
- .applyMarqueeSelection(
133
- [makeSelection("Card", first), makeSelection("Chip", second)],
134
- false,
135
- ),
136
- );
136
+ act(() => harness.current().applyMarqueeSelection([card, chip], false));
137
137
 
138
138
  expect(harness.current().domEditGroupSelections).toHaveLength(2);
139
139
  expect(harness.timeline.setTimelineSelectionSet).toHaveBeenCalledWith(
@@ -181,20 +181,10 @@ describe("useDomSelection marquee", () => {
181
181
  */
182
182
  describe("useDomSelection additive", () => {
183
183
  it("announces both members when a second element joins the selection", () => {
184
- const first = document.createElement("div");
185
- first.id = "card";
186
- const second = document.createElement("div");
187
- second.id = "chip";
188
- document.body.append(first, second);
189
- const harness = renderHarness(
190
- { activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
191
- { timelineElements: [timelineElement("card"), timelineElement("chip")] },
192
- );
184
+ const { harness, card, chip } = renderCardAndChip();
193
185
 
194
- act(() => harness.current().applyDomSelection(makeSelection("Card", first)));
195
- act(() =>
196
- harness.current().applyDomSelection(makeSelection("Chip", second), { additive: true }),
197
- );
186
+ act(() => harness.current().applyDomSelection(card));
187
+ act(() => harness.current().applyDomSelection(chip, { additive: true }));
198
188
 
199
189
  expect(harness.current().domEditGroupSelections).toHaveLength(2);
200
190
  expect(harness.timeline.setTimelineSelectionSet).toHaveBeenLastCalledWith(
@@ -205,6 +195,40 @@ describe("useDomSelection additive", () => {
205
195
  });
206
196
  harness.cleanup();
207
197
  });
198
+
199
+ // The timeline syncs its set back onto the canvas, so a set left at two re-adds the member.
200
+ it.each([
201
+ ["shift+click removes a member", "toggle"],
202
+ ["a marquee catches one member of a live group", "marquee"],
203
+ ] as const)("publishes the smaller set when %s", (_, shrink) => {
204
+ const { harness, card, chip } = renderCardAndChip();
205
+
206
+ act(() => harness.current().applyMarqueeSelection([card, chip], false));
207
+ act(() =>
208
+ shrink === "toggle"
209
+ ? harness.current().applyDomSelection(chip, { additive: true })
210
+ : harness.current().applyMarqueeSelection([card], false),
211
+ );
212
+
213
+ expect(harness.current().domEditGroupSelections).toHaveLength(1);
214
+ expect(harness.timeline.setTimelineSelectionSet).toHaveBeenLastCalledWith(new Set(["card"]));
215
+ expect(harness.timeline.setSelectedTimelineElementId).toHaveBeenLastCalledWith("card", {
216
+ preserveSet: true,
217
+ });
218
+ harness.cleanup();
219
+ });
220
+
221
+ it("keeps the live set when a single member is re-announced", () => {
222
+ const { harness, card, chip } = renderCardAndChip();
223
+
224
+ act(() => harness.current().applyMarqueeSelection([card, chip], false));
225
+ act(() => harness.current().applyDomSelection(chip));
226
+
227
+ expect(harness.timeline.setTimelineSelectionSet).toHaveBeenLastCalledWith(
228
+ new Set(["card", "chip"]),
229
+ );
230
+ harness.cleanup();
231
+ });
208
232
  });
209
233
 
210
234
  describe("useDomSelection", () => {
@@ -83,7 +83,7 @@ export function useDomSelection({
83
83
  // ── Callbacks ──
84
84
 
85
85
  const announceTimelineSelection = useCallback(
86
- (group: DomEditSelection[], primary: DomEditSelection | null) =>
86
+ (group: DomEditSelection[], primary: DomEditSelection | null, replaceSet?: boolean) =>
87
87
  announceSelectionToTimeline(
88
88
  {
89
89
  timelineElements,
@@ -93,6 +93,7 @@ export function useDomSelection({
93
93
  },
94
94
  group,
95
95
  primary,
96
+ replaceSet,
96
97
  ),
97
98
  [
98
99
  getTimelineSelectionSet,
@@ -179,7 +180,7 @@ export function useDomSelection({
179
180
  setRightPanelTab("design");
180
181
  }
181
182
  }
182
- announceTimelineSelection(nextGroup, nextSelection);
183
+ announceTimelineSelection(nextGroup, nextSelection, isAdditiveSelection);
183
184
  return;
184
185
  }
185
186
 
@@ -510,7 +511,7 @@ export function useDomSelection({
510
511
  domEditGroupSelectionsRef.current = nextGroup;
511
512
  setDomEditSelection(nextSelection);
512
513
  setDomEditGroupSelections(nextGroup);
513
- announceTimelineSelection(nextGroup, nextSelection);
514
+ announceTimelineSelection(nextGroup, nextSelection, true);
514
515
  },
515
516
  [applyDomSelection, announceTimelineSelection],
516
517
  );
@@ -12,6 +12,7 @@ import {
12
12
  resolveTimelineAutoScrollLoopAction,
13
13
  } from "./timelineEditing";
14
14
  import { getTimelineElementIndexes } from "../lib/timelineElementIndexes";
15
+ import { useTimelineReadOnlyPress } from "./timelineReadOnly";
15
16
 
16
17
  export const BEAT_BAND_H = 14; // dark band height at top of track
17
18
  const BEAT_HIT_W = 24; // grab width per beat (px) — ≥24px pointer target
@@ -88,7 +89,7 @@ function claimBeatDrag(pointerId?: number): BeatDragActor | null {
88
89
  return actor;
89
90
  }
90
91
 
91
- function cancelBeatDrag(): void {
92
+ export function cancelBeatDrag(): void {
92
93
  claimBeatDrag();
93
94
  }
94
95
 
@@ -377,6 +378,7 @@ export const BeatStrip = memo(function BeatStrip({
377
378
  );
378
379
  const sessionEpoch = usePlayerStore((state) => state.timelineSessionEpoch);
379
380
  const projectId = usePlayerStore((state) => state.timelineProjectId);
381
+ const readOnlyPress = useTimelineReadOnlyPress();
380
382
 
381
383
  if (!beatTimes || beatsTooDense(beatTimes, pps)) return null;
382
384
  const activeBeatIndex = activeActor
@@ -420,7 +422,7 @@ export const BeatStrip = memo(function BeatStrip({
420
422
  top: 0,
421
423
  width: BEAT_HIT_W,
422
424
  height: BEAT_BAND_H,
423
- cursor: "ew-resize",
425
+ cursor: readOnlyPress ? "default" : "ew-resize",
424
426
  pointerEvents: "auto",
425
427
  touchAction: "none",
426
428
  }}
@@ -429,13 +431,14 @@ export const BeatStrip = memo(function BeatStrip({
429
431
  // selection (which otherwise "selects" the whole panel mid-drag).
430
432
  e.preventDefault();
431
433
  e.stopPropagation();
434
+ if (readOnlyPress) return readOnlyPress();
432
435
  // ⌥ starts no drag: the delete lands on release below, so a
433
436
  // slipped ⌥-drag abandons instead of destroying the beat.
434
437
  if (e.altKey) return;
435
438
  beginBeatDrag(e, t, pps);
436
439
  }}
437
440
  onClick={(e) => {
438
- if (!e.altKey) return;
441
+ if (!e.altKey || readOnlyPress) return;
439
442
  // ⌥-click deletes. Deliberately NOT double-click: a stuttered drag
440
443
  // attempt reads as a double-click and would destroy the beat.
441
444
  e.stopPropagation();