@hyperframes/studio 0.5.0-alpha.14 → 0.5.0-alpha.15
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-vibA20NC.js → hyperframes-player-Cd8vYWxP.js} +2 -2
- package/dist/assets/index-DFLVGWTx.js +106 -0
- package/dist/assets/index-mXJ-UH9F.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +785 -377
- 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/parser.test.ts +14 -0
- package/src/captions/parser.ts +1 -0
- package/src/components/editor/DomEditOverlay.test.ts +241 -0
- package/src/components/editor/DomEditOverlay.tsx +970 -115
- package/src/components/editor/PropertyPanel.tsx +91 -83
- package/src/components/editor/domEditing.test.ts +161 -29
- package/src/components/editor/domEditing.ts +84 -113
- package/src/components/editor/manualEdits.test.ts +945 -0
- package/src/components/editor/manualEdits.ts +1397 -0
- package/src/components/editor/manualOffsetDrag.test.ts +140 -0
- package/src/components/editor/manualOffsetDrag.ts +307 -0
- package/src/components/renders/RenderQueue.tsx +10 -3
- package/src/hooks/usePersistentEditHistory.test.ts +1 -0
- package/src/hooks/usePersistentEditHistory.ts +3 -2
- package/src/player/components/CompositionThumbnail.test.ts +1 -1
- package/src/player/components/CompositionThumbnail.tsx +1 -1
- package/src/player/components/Player.tsx +54 -9
- package/src/player/hooks/useTimelinePlayer.test.ts +1 -0
- package/src/utils/clipboard.test.ts +1 -0
- package/src/utils/frameCapture.ts +3 -1
- package/src/utils/projectRouting.test.ts +87 -0
- package/src/utils/projectRouting.ts +27 -0
- package/dist/assets/index-JhhmFie-.js +0 -105
- package/dist/assets/index-KioPDrX6.css +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildProjectApiPath } from "./projectRouting";
|
|
2
|
+
|
|
1
3
|
export interface FrameCaptureRequest {
|
|
2
4
|
projectId: string;
|
|
3
5
|
compositionPath: string | null;
|
|
@@ -17,7 +19,7 @@ export function buildFrameCaptureUrl({
|
|
|
17
19
|
}: FrameCaptureRequest): string {
|
|
18
20
|
const compPath = normalizeCompositionPath(compositionPath);
|
|
19
21
|
const url = new URL(
|
|
20
|
-
|
|
22
|
+
buildProjectApiPath(projectId, `/thumbnail/${encodeURIComponent(compPath)}`),
|
|
21
23
|
origin,
|
|
22
24
|
);
|
|
23
25
|
url.searchParams.set("t", Math.max(0, currentTime).toFixed(3));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { buildFrameCaptureUrl } from "./frameCapture";
|
|
3
|
+
import {
|
|
4
|
+
buildProjectApiPath,
|
|
5
|
+
buildProjectHash,
|
|
6
|
+
encodeProjectId,
|
|
7
|
+
parseProjectIdFromHash,
|
|
8
|
+
} from "./projectRouting";
|
|
9
|
+
|
|
10
|
+
describe("project routing utilities", () => {
|
|
11
|
+
it("decodes project ids from hash routes before building capture URLs", () => {
|
|
12
|
+
vi.useFakeTimers();
|
|
13
|
+
vi.setSystemTime(new Date("2026-05-01T12:00:00Z"));
|
|
14
|
+
|
|
15
|
+
const projectId = parseProjectIdFromHash("#project/Notion%20Showcase");
|
|
16
|
+
|
|
17
|
+
expect(projectId).toBe("Notion Showcase");
|
|
18
|
+
expect(
|
|
19
|
+
buildFrameCaptureUrl({
|
|
20
|
+
projectId: projectId ?? "",
|
|
21
|
+
compositionPath: null,
|
|
22
|
+
currentTime: 1.809,
|
|
23
|
+
origin: "http://localhost:3002",
|
|
24
|
+
}),
|
|
25
|
+
).toBe(
|
|
26
|
+
"http://localhost:3002/api/projects/Notion%20Showcase/thumbnail/index.html?t=1.809&format=png&v=1777636800000",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
vi.useRealTimers();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("accepts legacy raw-space hash routes", () => {
|
|
33
|
+
expect(parseProjectIdFromHash("#project/Notion Showcase")).toBe("Notion Showcase");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("decodes reserved characters when the hash route is encoded", () => {
|
|
37
|
+
expect(parseProjectIdFromHash("#project/Launch%20%231%3F%20v2")).toBe("Launch #1? v2");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("does not throw on malformed percent escapes in hash routes", () => {
|
|
41
|
+
expect(parseProjectIdFromHash("#project/Broken%ZZName")).toBe("Broken%ZZName");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("ignores non-project hash routes", () => {
|
|
45
|
+
expect(parseProjectIdFromHash("")).toBeNull();
|
|
46
|
+
expect(parseProjectIdFromHash("#settings")).toBeNull();
|
|
47
|
+
expect(parseProjectIdFromHash("#project/")).toBeNull();
|
|
48
|
+
expect(parseProjectIdFromHash("#project/foo/bar")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("encodes project ids when writing hash routes", () => {
|
|
52
|
+
expect(buildProjectHash("Notion Showcase")).toBe("#project/Notion%20Showcase");
|
|
53
|
+
expect(buildProjectHash("Notion%20Showcase")).toBe("#project/Notion%2520Showcase");
|
|
54
|
+
expect(buildProjectHash("Launch #1? v2")).toBe("#project/Launch%20%231%3F%20v2");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("round-trips unicode project ids through hash routes", () => {
|
|
58
|
+
const hash = buildProjectHash("Mañana demo");
|
|
59
|
+
|
|
60
|
+
expect(hash).toBe("#project/Ma%C3%B1ana%20demo");
|
|
61
|
+
expect(parseProjectIdFromHash(hash)).toBe("Mañana demo");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("encodes project ids as one API path segment", () => {
|
|
65
|
+
expect(encodeProjectId("Notion Showcase")).toBe("Notion%20Showcase");
|
|
66
|
+
expect(encodeProjectId("Notion%20Showcase")).toBe("Notion%2520Showcase");
|
|
67
|
+
expect(encodeProjectId("Launch #1? v2")).toBe("Launch%20%231%3F%20v2");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("builds API paths without double encoding decoded project ids", () => {
|
|
71
|
+
expect(buildProjectApiPath("Notion Showcase", "/thumbnail/index.html")).toBe(
|
|
72
|
+
"/api/projects/Notion%20Showcase/thumbnail/index.html",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("keeps literal percent signs safe in API paths", () => {
|
|
77
|
+
expect(buildProjectApiPath("Percent%20Name", "/preview")).toBe(
|
|
78
|
+
"/api/projects/Percent%2520Name/preview",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("keeps unicode project ids safe in API paths", () => {
|
|
83
|
+
expect(buildProjectApiPath("Mañana demo", "/preview")).toBe(
|
|
84
|
+
"/api/projects/Ma%C3%B1ana%20demo/preview",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const PROJECT_HASH_PREFIX = "#project/";
|
|
2
|
+
|
|
3
|
+
export function encodeProjectId(projectId: string): string {
|
|
4
|
+
return encodeURIComponent(projectId);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildProjectHash(projectId: string): string {
|
|
8
|
+
return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseProjectIdFromHash(hash: string): string | null {
|
|
12
|
+
if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;
|
|
13
|
+
|
|
14
|
+
const encodedProjectId = hash.slice(PROJECT_HASH_PREFIX.length);
|
|
15
|
+
if (!encodedProjectId || encodedProjectId.includes("/")) return null;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
return decodeURIComponent(encodedProjectId);
|
|
19
|
+
} catch {
|
|
20
|
+
return encodedProjectId;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildProjectApiPath(projectId: string, suffix = ""): string {
|
|
25
|
+
const normalizedSuffix = suffix && !suffix.startsWith("/") ? `/${suffix}` : suffix;
|
|
26
|
+
return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;
|
|
27
|
+
}
|