@hyperframes/studio 0.5.0-alpha.8 → 0.5.0
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/assets/hyperframes-player-CoI5h1xv.js +353 -0
- package/dist/assets/index-BKjcNNNd.css +1 -0
- package/dist/assets/index-CqiisJmo.js +93 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +208 -1436
- package/src/captions/components/CaptionOverlay.tsx +2 -1
- package/src/captions/generator.test.ts +19 -0
- package/src/captions/generator.ts +9 -2
- package/src/captions/hooks/useCaptionSync.ts +6 -1
- package/src/captions/keyboard.test.ts +38 -0
- package/src/captions/keyboard.ts +8 -0
- package/src/captions/parser.test.ts +14 -0
- package/src/captions/parser.ts +1 -0
- package/src/components/LintModal.tsx +4 -3
- package/src/components/editor/PropertyPanel.tsx +206 -2462
- package/src/components/nle/NLELayout.tsx +47 -17
- package/src/components/nle/NLEPreview.tsx +9 -50
- package/src/components/sidebar/AssetsTab.tsx +4 -3
- package/src/components/sidebar/CompositionsTab.test.ts +1 -16
- package/src/components/sidebar/CompositionsTab.tsx +45 -117
- package/src/components/sidebar/LeftSidebar.tsx +55 -34
- package/src/components/ui/HyperframesLoader.tsx +104 -0
- package/src/components/ui/index.ts +2 -0
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/CompositionThumbnail.tsx +10 -42
- package/src/player/components/EditModal.tsx +20 -5
- package/src/player/components/Player.tsx +129 -28
- package/src/player/components/PlayerControls.tsx +117 -49
- package/src/player/components/Timeline.test.ts +0 -12
- package/src/player/components/Timeline.tsx +25 -52
- package/src/player/components/TimelineClip.tsx +9 -21
- package/src/player/components/timelineEditing.test.ts +4 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineTheme.test.ts +19 -0
- package/src/player/components/timelineTheme.ts +8 -4
- package/src/player/hooks/useTimelinePlayer.test.ts +219 -1
- package/src/player/hooks/useTimelinePlayer.ts +487 -106
- package/src/player/lib/time.test.ts +29 -1
- package/src/player/lib/time.ts +26 -0
- package/src/player/store/playerStore.test.ts +11 -1
- package/src/player/store/playerStore.ts +6 -1
- package/src/styles/studio.css +112 -0
- package/src/utils/frameCapture.test.ts +26 -0
- package/src/utils/frameCapture.ts +40 -0
- package/src/utils/mediaTypes.ts +1 -1
- package/src/utils/projectRouting.test.ts +87 -0
- package/src/utils/projectRouting.ts +27 -0
- package/src/utils/sourcePatcher.test.ts +1 -128
- package/src/utils/sourcePatcher.ts +18 -130
- package/src/utils/timelineAssetDrop.test.ts +11 -31
- package/src/utils/timelineAssetDrop.ts +2 -22
- package/dist/assets/hyperframes-player-vibA20NC.js +0 -198
- package/dist/assets/index-0Zt0t13W.css +0 -1
- package/dist/assets/index-C9f5eif8.js +0 -105
- package/src/components/editor/DomEditOverlay.tsx +0 -442
- package/src/components/editor/colorValue.test.ts +0 -82
- package/src/components/editor/colorValue.ts +0 -175
- package/src/components/editor/domEditing.test.ts +0 -537
- package/src/components/editor/domEditing.ts +0 -762
- package/src/components/editor/floatingPanel.test.ts +0 -34
- package/src/components/editor/floatingPanel.ts +0 -54
- package/src/components/editor/fontAssets.ts +0 -32
- package/src/components/editor/fontCatalog.ts +0 -126
- package/src/components/editor/gradientValue.test.ts +0 -89
- package/src/components/editor/gradientValue.ts +0 -445
- package/src/player/components/CompositionThumbnail.test.ts +0 -19
- package/src/utils/clipboard.test.ts +0 -88
- package/src/utils/clipboard.ts +0 -57
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { memo, useState, useCallback, useRef } from "react";
|
|
2
2
|
import { useCaptionStore } from "../store";
|
|
3
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
|
+
import { shouldHandleCaptionNudgeKey } from "../keyboard";
|
|
4
5
|
|
|
5
6
|
interface CaptionOverlayProps {
|
|
6
7
|
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
@@ -329,7 +330,7 @@ export const CaptionOverlay = memo(function CaptionOverlay({ iframeRef }: Captio
|
|
|
329
330
|
const { selectedSegmentIds: sel, model: m } = useCaptionStore.getState();
|
|
330
331
|
if (sel.size === 0 || !m) return;
|
|
331
332
|
const arrow = e.key;
|
|
332
|
-
if (!
|
|
333
|
+
if (!shouldHandleCaptionNudgeKey(e)) return;
|
|
333
334
|
|
|
334
335
|
e.preventDefault();
|
|
335
336
|
const step = e.shiftKey ? 10 : 1;
|
|
@@ -137,6 +137,25 @@ describe("generateCaptionHtml", () => {
|
|
|
137
137
|
expect(html).toContain('"end": 2.7');
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it("includes stable word ids in the transcript and generated word spans", () => {
|
|
141
|
+
const transcript: TranscriptWord[] = [
|
|
142
|
+
{ id: "word-a", text: "Hello", start: 0, end: 0.4 },
|
|
143
|
+
{ id: "word-b", text: "world", start: 0.5, end: 1 },
|
|
144
|
+
];
|
|
145
|
+
const model = buildCaptionModel(transcript, {
|
|
146
|
+
width: 1920,
|
|
147
|
+
height: 1080,
|
|
148
|
+
duration: 2,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const html = generateCaptionHtml(model);
|
|
152
|
+
|
|
153
|
+
expect(html).toContain('"id": "word-a"');
|
|
154
|
+
expect(html).toContain('"id": "word-b"');
|
|
155
|
+
expect(html).toContain('w_segment_0.id = "word-a";');
|
|
156
|
+
expect(html).toContain('w_segment_1.id = "word-b";');
|
|
157
|
+
});
|
|
158
|
+
|
|
140
159
|
it("TRANSCRIPT contains all 7 words from the sample", () => {
|
|
141
160
|
const model = buildTestModel();
|
|
142
161
|
const html = generateCaptionHtml(model);
|
|
@@ -261,14 +261,19 @@ function hexToRgba(color: string, opacity: number): string {
|
|
|
261
261
|
|
|
262
262
|
function generateJs(model: CaptionModel): string {
|
|
263
263
|
// Collect all segments across all groups in order
|
|
264
|
-
const allSegments: Array<{ text: string; start: number; end: number }> = [];
|
|
264
|
+
const allSegments: Array<{ id?: string; text: string; start: number; end: number }> = [];
|
|
265
265
|
for (const groupId of model.groupOrder) {
|
|
266
266
|
const group = model.groups.get(groupId);
|
|
267
267
|
if (!group) continue;
|
|
268
268
|
for (const segId of group.segmentIds) {
|
|
269
269
|
const seg = model.segments.get(segId);
|
|
270
270
|
if (!seg) continue;
|
|
271
|
-
allSegments.push({
|
|
271
|
+
allSegments.push({
|
|
272
|
+
...(seg.wordId ? { id: seg.wordId } : {}),
|
|
273
|
+
text: seg.text,
|
|
274
|
+
start: seg.start,
|
|
275
|
+
end: seg.end,
|
|
276
|
+
});
|
|
272
277
|
}
|
|
273
278
|
}
|
|
274
279
|
|
|
@@ -300,9 +305,11 @@ function generateJs(model: CaptionModel): string {
|
|
|
300
305
|
const wordLines: string[] = groupSegments.map((seg) => {
|
|
301
306
|
const escaped = seg.text.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
302
307
|
const segVar = `w_${seg.id.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
308
|
+
const idLine = seg.wordId ? `\n ${segVar}.id = ${JSON.stringify(seg.wordId)};` : "";
|
|
303
309
|
return (
|
|
304
310
|
` const ${segVar} = document.createElement('span');` +
|
|
305
311
|
`\n ${segVar}.className = 'word clip';` +
|
|
312
|
+
idLine +
|
|
306
313
|
`\n ${segVar}.textContent = '${escaped}';` +
|
|
307
314
|
`\n ${segVar}.dataset.start = '${seg.start}';` +
|
|
308
315
|
`\n ${segVar}.dataset.end = '${seg.end}';` +
|
|
@@ -124,17 +124,22 @@ export function useCaptionSync(projectId: string | null) {
|
|
|
124
124
|
|
|
125
125
|
const model = state.model;
|
|
126
126
|
const allSegIds: string[] = [];
|
|
127
|
+
const segIdByWordId = new Map<string, string>();
|
|
127
128
|
for (const groupId of model.groupOrder) {
|
|
128
129
|
const group = model.groups.get(groupId);
|
|
129
130
|
if (!group) continue;
|
|
130
131
|
for (const segId of group.segmentIds) {
|
|
131
132
|
allSegIds.push(segId);
|
|
133
|
+
const seg = model.segments.get(segId);
|
|
134
|
+
if (seg?.wordId) segIdByWordId.set(seg.wordId, segId);
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
const newSegments = new Map(model.segments);
|
|
136
139
|
for (const override of overrides) {
|
|
137
|
-
const segId =
|
|
140
|
+
const segId =
|
|
141
|
+
(override.wordId ? segIdByWordId.get(override.wordId) : undefined) ??
|
|
142
|
+
allSegIds[override.wordIndex];
|
|
138
143
|
if (!segId) continue;
|
|
139
144
|
const seg = newSegments.get(segId);
|
|
140
145
|
if (!seg) continue;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { shouldHandleCaptionNudgeKey } from "./keyboard";
|
|
3
|
+
|
|
4
|
+
function mockKeyboardEvent(
|
|
5
|
+
key: string,
|
|
6
|
+
overrides: Partial<Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey">> = {},
|
|
7
|
+
): Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "key"> {
|
|
8
|
+
return {
|
|
9
|
+
altKey: false,
|
|
10
|
+
ctrlKey: false,
|
|
11
|
+
metaKey: false,
|
|
12
|
+
key,
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("shouldHandleCaptionNudgeKey", () => {
|
|
18
|
+
it("handles plain and Shift-modified arrow keys for caption nudging", () => {
|
|
19
|
+
expect(shouldHandleCaptionNudgeKey(mockKeyboardEvent("ArrowLeft"))).toBe(true);
|
|
20
|
+
expect(shouldHandleCaptionNudgeKey(mockKeyboardEvent("ArrowRight"))).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("ignores browser and app shortcut chords", () => {
|
|
24
|
+
expect(shouldHandleCaptionNudgeKey(mockKeyboardEvent("ArrowLeft", { altKey: true }))).toBe(
|
|
25
|
+
false,
|
|
26
|
+
);
|
|
27
|
+
expect(shouldHandleCaptionNudgeKey(mockKeyboardEvent("ArrowRight", { ctrlKey: true }))).toBe(
|
|
28
|
+
false,
|
|
29
|
+
);
|
|
30
|
+
expect(shouldHandleCaptionNudgeKey(mockKeyboardEvent("ArrowRight", { metaKey: true }))).toBe(
|
|
31
|
+
false,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("ignores non-arrow keys", () => {
|
|
36
|
+
expect(shouldHandleCaptionNudgeKey(mockKeyboardEvent("KeyL"))).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const CAPTION_NUDGE_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]);
|
|
2
|
+
|
|
3
|
+
type CaptionNudgeKeyEvent = Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "key">;
|
|
4
|
+
|
|
5
|
+
export function shouldHandleCaptionNudgeKey(event: CaptionNudgeKeyEvent): boolean {
|
|
6
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
|
7
|
+
return CAPTION_NUDGE_KEYS.has(event.key);
|
|
8
|
+
}
|
|
@@ -102,6 +102,20 @@ describe("extractTranscript", () => {
|
|
|
102
102
|
expect(words).toHaveLength(1);
|
|
103
103
|
expect(words[0]).toEqual({ text: "Hello", start: 0.0, end: 0.5 });
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
it("preserves stable word ids when present", () => {
|
|
107
|
+
const words = extractTranscript(`
|
|
108
|
+
const TRANSCRIPT = [
|
|
109
|
+
{ id: "word-a", text: "Hello", start: 0, end: 0.4 },
|
|
110
|
+
{ id: "word-b", text: "world", start: 0.5, end: 1 },
|
|
111
|
+
];
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
expect(words).toEqual([
|
|
115
|
+
{ id: "word-a", text: "Hello", start: 0, end: 0.4 },
|
|
116
|
+
{ id: "word-b", text: "world", start: 0.5, end: 1 },
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
105
119
|
});
|
|
106
120
|
|
|
107
121
|
describe("script variable name", () => {
|
package/src/captions/parser.ts
CHANGED
|
@@ -303,6 +303,7 @@ function parseTranscriptArray(arrayLiteral: string): TranscriptWord[] {
|
|
|
303
303
|
) {
|
|
304
304
|
const entry = item as Record<string, unknown>;
|
|
305
305
|
words.push({
|
|
306
|
+
...(typeof entry.id === "string" ? { id: entry.id } : {}),
|
|
306
307
|
text: entry.text as string,
|
|
307
308
|
start: entry.start as number,
|
|
308
309
|
end: entry.end as number,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { XIcon, WarningIcon, CheckCircleIcon, CaretRightIcon } from "@phosphor-icons/react";
|
|
3
|
-
import { copyTextToClipboard } from "../utils/clipboard";
|
|
4
3
|
|
|
5
4
|
export interface LintFinding {
|
|
6
5
|
severity: "error" | "warning";
|
|
@@ -31,10 +30,12 @@ export function LintModal({
|
|
|
31
30
|
return line;
|
|
32
31
|
});
|
|
33
32
|
const text = `Fix these HyperFrames lint issues for project "${projectId}":\n\nProject path: ${window.location.href}\n\n${lines.join("\n\n")}`;
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
try {
|
|
34
|
+
await navigator.clipboard.writeText(text);
|
|
36
35
|
setCopied(true);
|
|
37
36
|
setTimeout(() => setCopied(false), 2000);
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
41
|
|