@hyperframes/studio 0.8.48 → 0.8.49
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-CqON2Rzo.js +464 -0
- package/dist/assets/{index-B3-orIjW.js → index-BGovDksE.js} +1 -1
- package/dist/assets/{index-C9gJKpqQ.js → index-Bgmx2h7W.js} +135 -135
- package/dist/assets/{index-D_j-0PD3.js → index-mIgSsKqs.js} +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.html +1 -1
- package/dist/index.js +443 -303
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/hooks/useBlockHandlers.ts +7 -2
- package/src/hooks/useTimelineAssetDropOps.test.tsx +15 -0
- package/src/hooks/useTimelineAssetDropOps.ts +4 -2
- package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +215 -3
- package/src/hooks/useTimelineSelectionPreviewSync.ts +63 -32
- package/src/player/components/timelineClipDragCommit.test.ts +114 -0
- package/src/player/components/timelineClipDragCommit.ts +18 -49
- package/src/player/components/timelineClipDragPreview.test.ts +173 -1
- package/src/player/components/timelineClipDragPreview.ts +8 -1
- package/src/player/components/timelineCollision.test.ts +23 -0
- package/src/player/components/timelineCollision.ts +27 -0
- package/src/player/components/timelineDragLanding.ts +108 -0
- package/src/player/components/timelineDropReveal.test.ts +34 -0
- package/src/player/components/timelineDropReveal.ts +12 -0
- package/src/player/components/useTimelineFocusCoordinator.test.tsx +66 -0
- package/src/player/components/useTimelineFocusCoordinator.ts +43 -3
- package/src/player/components/useTimelineGeometry.test.tsx +117 -0
- package/src/player/components/useTimelineGeometry.ts +7 -2
- package/src/player/components/useTimelineGeometryFocusOrder.test.tsx +184 -0
- package/src/player/components/useTimelineLogicalFocus.ts +1 -0
- package/src/player/lib/timelineElementHelpers.ts +8 -0
- package/src/player/store/readinessSlice.ts +4 -3
- package/src/utils/blockInstaller.test.ts +4 -0
- package/src/utils/blockInstaller.ts +9 -3
- package/src/utils/retryBudget.ts +20 -0
- package/src/utils/sdkCutover.familyGate.test.ts +5 -1
- package/src/utils/sdkCutover.ts +7 -4
- package/src/utils/sdkCutoverEligibility.test.ts +96 -1
- package/src/utils/sdkCutoverEligibility.ts +34 -11
- package/src/utils/timelineCompositionInsert.ts +2 -1
- package/dist/assets/hyperframes-player-CtVX6jL_.js +0 -464
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { PatchOperation } from "./sourcePatcher";
|
|
3
|
-
import { shouldUseSdkCutover } from "./sdkCutoverEligibility";
|
|
3
|
+
import { shouldUseSdkCutover, sdkCutoverIneligibleReason } from "./sdkCutoverEligibility";
|
|
4
|
+
|
|
5
|
+
const styleOp = (property: string, value: string): PatchOperation => ({
|
|
6
|
+
type: "inline-style",
|
|
7
|
+
property,
|
|
8
|
+
value,
|
|
9
|
+
});
|
|
10
|
+
const textOp = (value: string): PatchOperation => ({
|
|
11
|
+
type: "text-content",
|
|
12
|
+
property: "text",
|
|
13
|
+
value,
|
|
14
|
+
});
|
|
15
|
+
const attrOp = (property: string, value: string): PatchOperation => ({
|
|
16
|
+
type: "attribute",
|
|
17
|
+
property,
|
|
18
|
+
value,
|
|
19
|
+
});
|
|
20
|
+
const htmlAttrOp = (property: string, value: string): PatchOperation => ({
|
|
21
|
+
type: "html-attribute",
|
|
22
|
+
property,
|
|
23
|
+
value,
|
|
24
|
+
});
|
|
4
25
|
|
|
5
26
|
const childStyleOp: PatchOperation = {
|
|
6
27
|
type: "inline-style",
|
|
@@ -15,3 +36,77 @@ describe("shouldUseSdkCutover child-scoped operations", () => {
|
|
|
15
36
|
expect(shouldUseSdkCutover(true, true, "hf-parent", [childStyleOp])).toBe(false);
|
|
16
37
|
});
|
|
17
38
|
});
|
|
39
|
+
|
|
40
|
+
describe("sdkCutoverIneligibleReason", () => {
|
|
41
|
+
// `ineligible_operation` was one string covering four distinct causes, which
|
|
42
|
+
// made the post-flip decline telemetry unactionable: 26 of 30 declines on
|
|
43
|
+
// v0.8.47 landed there, with no way to separate a structural edit the SDK has
|
|
44
|
+
// no vocabulary for (expected, permanent) from a reserved-attribute or
|
|
45
|
+
// unsafe-attribute decline (narrower, and possibly fixable).
|
|
46
|
+
it("names an op type the SDK cannot express", () => {
|
|
47
|
+
expect(
|
|
48
|
+
sdkCutoverIneligibleReason("hf-abc", [
|
|
49
|
+
{ type: "split", property: "x", value: "1" } as unknown as PatchOperation,
|
|
50
|
+
]),
|
|
51
|
+
).toBe("unsupported_op_type");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("names a child-scoped op", () => {
|
|
55
|
+
expect(sdkCutoverIneligibleReason("hf-parent", [childStyleOp])).toBe("child_scoped_op");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("names a reserved attribute", () => {
|
|
59
|
+
expect(sdkCutoverIneligibleReason("hf-abc", [attrOp("end", "2")])).toBe("reserved_attribute");
|
|
60
|
+
expect(sdkCutoverIneligibleReason("hf-abc", [htmlAttrOp("DATA-START", "1")])).toBe(
|
|
61
|
+
"reserved_attribute",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("names an unsafe html attribute", () => {
|
|
66
|
+
expect(sdkCutoverIneligibleReason("hf-abc", [htmlAttrOp("onclick", "alert(1)")])).toBe(
|
|
67
|
+
"unsafe_html_attribute",
|
|
68
|
+
);
|
|
69
|
+
expect(sdkCutoverIneligibleReason("hf-abc", [htmlAttrOp("href", "javascript:alert(1)")])).toBe(
|
|
70
|
+
"unsafe_html_attribute",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("names an empty batch and an unaddressable target", () => {
|
|
75
|
+
expect(sdkCutoverIneligibleReason("hf-abc", [])).toBe("no_ops");
|
|
76
|
+
expect(sdkCutoverIneligibleReason(null, [styleOp("color", "red")])).toBe(
|
|
77
|
+
"target_unaddressable",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns null when the batch is eligible", () => {
|
|
82
|
+
expect(sdkCutoverIneligibleReason("hf-abc", [styleOp("color", "red")])).toBeNull();
|
|
83
|
+
expect(
|
|
84
|
+
sdkCutoverIneligibleReason("hf-abc", [
|
|
85
|
+
styleOp("color", "red"),
|
|
86
|
+
textOp("hello"),
|
|
87
|
+
attrOp("x", "1"),
|
|
88
|
+
htmlAttrOp("class", "foo"),
|
|
89
|
+
]),
|
|
90
|
+
).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The reason function must not drift from the predicate it explains: a batch
|
|
94
|
+
// the gate accepts must have no reason, and one it declines must have one.
|
|
95
|
+
it("agrees with shouldUseSdkCutover on every case", () => {
|
|
96
|
+
const cases: PatchOperation[][] = [
|
|
97
|
+
[styleOp("color", "red")],
|
|
98
|
+
[textOp("hello")],
|
|
99
|
+
[attrOp("x", "1")],
|
|
100
|
+
[htmlAttrOp("class", "foo")],
|
|
101
|
+
[attrOp("end", "2")],
|
|
102
|
+
[htmlAttrOp("onclick", "alert(1)")],
|
|
103
|
+
[childStyleOp],
|
|
104
|
+
[{ type: "split", property: "x", value: "1" } as unknown as PatchOperation],
|
|
105
|
+
[],
|
|
106
|
+
];
|
|
107
|
+
for (const ops of cases) {
|
|
108
|
+
const eligible = shouldUseSdkCutover(true, true, "hf-abc", ops);
|
|
109
|
+
expect(sdkCutoverIneligibleReason("hf-abc", ops) === null).toBe(eligible);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -123,21 +123,44 @@ export function isResolverDisagreement(session: Composition, hfId: string): bool
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Why a batch cannot take the SDK cutover path, or null when it can.
|
|
128
|
+
*
|
|
129
|
+
* `ineligible_operation` used to be a single decline reason covering all of
|
|
130
|
+
* these, which made the post-flip telemetry unactionable — a structural edit the
|
|
131
|
+
* SDK has no vocabulary for (expected and permanent) was indistinguishable from
|
|
132
|
+
* a reserved-attribute decline (narrow, and possibly worth fixing). The order
|
|
133
|
+
* mirrors `shouldUseSdkCutover`'s conjunction, so the first failing check names
|
|
134
|
+
* the reason.
|
|
135
|
+
*/
|
|
136
|
+
export type SdkCutoverIneligibleReason =
|
|
137
|
+
| "target_unaddressable"
|
|
138
|
+
| "no_ops"
|
|
139
|
+
| "unsupported_op_type"
|
|
140
|
+
| "child_scoped_op"
|
|
141
|
+
| "reserved_attribute"
|
|
142
|
+
| "unsafe_html_attribute";
|
|
143
|
+
|
|
144
|
+
export function sdkCutoverIneligibleReason(
|
|
145
|
+
hfId: string | null | undefined,
|
|
146
|
+
ops: PatchOperation[],
|
|
147
|
+
): SdkCutoverIneligibleReason | null {
|
|
148
|
+
if (!hfId) return "target_unaddressable";
|
|
149
|
+
if (ops.length === 0) return "no_ops";
|
|
150
|
+
if (!ops.every((o) => CUTOVER_OP_TYPES.has(o.type))) return "unsupported_op_type";
|
|
151
|
+
if (hasChildScopedOp(ops)) return "child_scoped_op";
|
|
152
|
+
if (ops.some(mapsToReservedAttr)) return "reserved_attribute";
|
|
153
|
+
if (hasUnsafeHtmlAttributeOp(ops)) return "unsafe_html_attribute";
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
export function shouldUseSdkCutover(
|
|
127
158
|
flagEnabled: boolean,
|
|
128
159
|
hasSession: boolean,
|
|
129
160
|
hfId: string | null | undefined,
|
|
130
161
|
ops: PatchOperation[],
|
|
131
162
|
): boolean {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
!!hfId &&
|
|
136
|
-
ops.length > 0 &&
|
|
137
|
-
ops.every((o) => CUTOVER_OP_TYPES.has(o.type)) &&
|
|
138
|
-
// SDK edit ops target only the element hfId; child-scoped patch ops need the server path.
|
|
139
|
-
!hasChildScopedOp(ops) &&
|
|
140
|
-
!ops.some(mapsToReservedAttr) &&
|
|
141
|
-
!hasUnsafeHtmlAttributeOp(ops)
|
|
142
|
-
);
|
|
163
|
+
// Derived from the reason function so the two can never disagree about which
|
|
164
|
+
// batches are eligible.
|
|
165
|
+
return flagEnabled && hasSession && sdkCutoverIneligibleReason(hfId, ops) === null;
|
|
143
166
|
}
|
|
@@ -3,6 +3,7 @@ import { serializeStudioFileMutation } from "./studioFileMutationCoordinator";
|
|
|
3
3
|
import type { RecordEditInput } from "./studioFileHistory";
|
|
4
4
|
import { buildProjectApiPath } from "./projectRouting";
|
|
5
5
|
import { studioWriteHeaders } from "./studioFileVersion";
|
|
6
|
+
import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
|
|
6
7
|
|
|
7
8
|
interface TimelineCompositionInsertionResult {
|
|
8
9
|
path: string;
|
|
@@ -76,7 +77,7 @@ export async function commitTimelineCompositionInsertion(input: {
|
|
|
76
77
|
await input.writeFile(input.targetPath, result.before, result.after);
|
|
77
78
|
throw error;
|
|
78
79
|
}
|
|
79
|
-
input.selectHost(
|
|
80
|
+
input.selectHost(deriveTimelineStoreKeyForDomId(result.hostId, input.targetPath));
|
|
80
81
|
try {
|
|
81
82
|
input.resync?.();
|
|
82
83
|
} catch (error) {
|