@hyperframes/studio 0.4.13-alpha.1 → 0.4.13-alpha.3

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.
package/dist/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>HyperFrames Studio</title>
7
- <script type="module" crossorigin src="/assets/index-rN5doSq1.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-BNJpPMh6.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BKkR67xb.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.4.13-alpha.1",
3
+ "version": "0.4.13-alpha.3",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "@phosphor-icons/react": "^2.1.10",
33
33
  "codemirror": "^6.0.1",
34
34
  "motion": "^12.38.0",
35
- "@hyperframes/core": "0.4.13-alpha.1",
36
- "@hyperframes/player": "0.4.13-alpha.1"
35
+ "@hyperframes/core": "0.4.13-alpha.3",
36
+ "@hyperframes/player": "0.4.13-alpha.3"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.0.0",
@@ -47,7 +47,7 @@
47
47
  "vite": "^6.4.2",
48
48
  "vitest": "^3.2.4",
49
49
  "zustand": "^5.0.0",
50
- "@hyperframes/producer": "0.4.13-alpha.1"
50
+ "@hyperframes/producer": "0.4.13-alpha.3"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": "^18.0.0 || ^19.0.0",
@@ -10,6 +10,7 @@ import { formatTime } from "../lib/time";
10
10
  import { TimelineClip } from "./TimelineClip";
11
11
  import { EditPopover } from "./EditModal";
12
12
  import {
13
+ canOffsetTrimClipStart,
13
14
  resolveTimelineAutoScroll,
14
15
  resolveTimelineMove,
15
16
  resolveTimelineResize,
@@ -1109,6 +1110,7 @@ export const Timeline = memo(function Timeline({
1109
1110
  onHoverEnd={() => setHoveredClip(null)}
1110
1111
  onResizeStart={(edge, e) => {
1111
1112
  if (e.button !== 0 || e.shiftKey || !onResizeElement) return;
1113
+ if (edge === "start" && !canOffsetTrimClipStart(el)) return;
1112
1114
  e.stopPropagation();
1113
1115
  setShowPopover(false);
1114
1116
  setRangeSelection(null);
@@ -4,6 +4,7 @@ import type { TimelineTrackStyle } from "./timelineTheme";
4
4
  import { memo, type ReactNode } from "react";
5
5
  import type { TimelineElement } from "../store/playerStore";
6
6
  import { defaultTimelineTheme, getClipHandleOpacity, type TimelineTheme } from "./timelineTheme";
7
+ import { canOffsetTrimClipStart } from "./timelineEditing";
7
8
 
8
9
  interface TimelineClipProps {
9
10
  el: TimelineElement;
@@ -59,6 +60,7 @@ export const TimelineClip = memo(function TimelineClip({
59
60
  : isHovered
60
61
  ? theme.clipShadowHover
61
62
  : theme.clipShadow;
63
+ const canTrimStart = canOffsetTrimClipStart(el);
62
64
  const showHandles = handleOpacity > 0.01;
63
65
 
64
66
  return (
@@ -109,14 +111,15 @@ export const TimelineClip = memo(function TimelineClip({
109
111
  top: 0,
110
112
  bottom: 0,
111
113
  width: 18,
112
- opacity: showHandles ? 1 : 0,
113
- pointerEvents: onResizeStart ? "auto" : "none",
114
+ opacity: showHandles && canTrimStart ? 1 : 0,
115
+ pointerEvents: onResizeStart && canTrimStart ? "auto" : "none",
114
116
  zIndex: 4,
115
117
  transition: "opacity 120ms ease-out",
116
118
  cursor: "col-resize",
117
- background: showHandles
118
- ? `linear-gradient(90deg, ${trackStyle.accent}4d 0%, ${trackStyle.accent}22 42%, transparent 100%)`
119
- : "transparent",
119
+ background:
120
+ showHandles && canTrimStart
121
+ ? `linear-gradient(90deg, ${trackStyle.accent}4d 0%, ${trackStyle.accent}22 42%, transparent 100%)`
122
+ : "transparent",
120
123
  }}
121
124
  >
122
125
  <div
@@ -1,8 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
- buildTrackZIndexMap,
4
3
  buildPromptCopyText,
5
4
  buildTimelineAgentPrompt,
5
+ buildTrackZIndexMap,
6
+ canOffsetTrimClipStart,
6
7
  resolveTimelineAutoScroll,
7
8
  resolveTimelineMove,
8
9
  resolveTimelineResize,
@@ -154,13 +155,13 @@ describe("resolveTimelineMove", () => {
154
155
  });
155
156
 
156
157
  describe("buildTrackZIndexMap", () => {
157
- it("maps sorted tracks onto stable positive z-index values", () => {
158
+ it("maps visually higher tracks onto higher z-index values", () => {
158
159
  expect(buildTrackZIndexMap([-2, -1, 0, 3])).toEqual(
159
160
  new Map([
160
- [-2, 1],
161
- [-1, 2],
162
- [0, 3],
163
- [3, 4],
161
+ [-2, 4],
162
+ [-1, 3],
163
+ [0, 2],
164
+ [3, 1],
164
165
  ]),
165
166
  );
166
167
  });
@@ -168,14 +169,42 @@ describe("buildTrackZIndexMap", () => {
168
169
  it("deduplicates tracks before assigning z-index values", () => {
169
170
  expect(buildTrackZIndexMap([-1, 0, -1, 3, 3])).toEqual(
170
171
  new Map([
171
- [-1, 1],
172
+ [-1, 3],
172
173
  [0, 2],
173
- [3, 3],
174
+ [3, 1],
174
175
  ]),
175
176
  );
176
177
  });
177
178
  });
178
179
 
180
+ describe("canOffsetTrimClipStart", () => {
181
+ it("allows front trim for clips that carry playback offset metadata", () => {
182
+ expect(
183
+ canOffsetTrimClipStart({
184
+ tag: "div",
185
+ playbackStartAttr: "media-start",
186
+ }),
187
+ ).toBe(true);
188
+ });
189
+
190
+ it("allows front trim for media clips with source duration metadata", () => {
191
+ expect(
192
+ canOffsetTrimClipStart({
193
+ tag: "video",
194
+ sourceDuration: 12,
195
+ }),
196
+ ).toBe(true);
197
+ });
198
+
199
+ it("blocks front trim for generic motion clips", () => {
200
+ expect(
201
+ canOffsetTrimClipStart({
202
+ tag: "section",
203
+ }),
204
+ ).toBe(false);
205
+ });
206
+ });
207
+
179
208
  describe("resolveTimelineAutoScroll", () => {
180
209
  it("does not scroll when the pointer stays away from the edges", () => {
181
210
  expect(
@@ -116,7 +116,8 @@ export function resolveTimelineMove(
116
116
 
117
117
  export function buildTrackZIndexMap(tracks: number[]): Map<number, number> {
118
118
  const uniqueTracks = Array.from(new Set(tracks)).sort((a, b) => a - b);
119
- return new Map(uniqueTracks.map((track, index) => [track, index + 1]));
119
+ const maxZIndex = uniqueTracks.length;
120
+ return new Map(uniqueTracks.map((track, index) => [track, maxZIndex - index]));
120
121
  }
121
122
 
122
123
  export function resolveTimelineResize(
@@ -168,6 +169,23 @@ export interface TimelinePromptElement {
168
169
  track: number;
169
170
  }
170
171
 
172
+ export function canOffsetTrimClipStart(input: {
173
+ tag: string;
174
+ playbackStart?: number;
175
+ playbackStartAttr?: "media-start" | "playback-start";
176
+ sourceDuration?: number;
177
+ }): boolean {
178
+ if (input.playbackStartAttr != null) return true;
179
+ if (input.playbackStart != null) return true;
180
+ const normalizedTag = input.tag.toLowerCase();
181
+ if (!["video", "audio"].includes(normalizedTag)) return false;
182
+ return (
183
+ input.sourceDuration != null &&
184
+ Number.isFinite(input.sourceDuration) &&
185
+ input.sourceDuration > 0
186
+ );
187
+ }
188
+
171
189
  export function buildTimelineAgentPrompt({
172
190
  rangeStart,
173
191
  rangeEnd,