@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.
Files changed (40) hide show
  1. package/dist/assets/hyperframes-player-CqON2Rzo.js +464 -0
  2. package/dist/assets/{index-B3-orIjW.js → index-BGovDksE.js} +1 -1
  3. package/dist/assets/{index-C9gJKpqQ.js → index-Bgmx2h7W.js} +135 -135
  4. package/dist/assets/{index-D_j-0PD3.js → index-mIgSsKqs.js} +1 -1
  5. package/dist/index.d.ts +4 -3
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +443 -303
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/hooks/useBlockHandlers.ts +7 -2
  11. package/src/hooks/useTimelineAssetDropOps.test.tsx +15 -0
  12. package/src/hooks/useTimelineAssetDropOps.ts +4 -2
  13. package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +215 -3
  14. package/src/hooks/useTimelineSelectionPreviewSync.ts +63 -32
  15. package/src/player/components/timelineClipDragCommit.test.ts +114 -0
  16. package/src/player/components/timelineClipDragCommit.ts +18 -49
  17. package/src/player/components/timelineClipDragPreview.test.ts +173 -1
  18. package/src/player/components/timelineClipDragPreview.ts +8 -1
  19. package/src/player/components/timelineCollision.test.ts +23 -0
  20. package/src/player/components/timelineCollision.ts +27 -0
  21. package/src/player/components/timelineDragLanding.ts +108 -0
  22. package/src/player/components/timelineDropReveal.test.ts +34 -0
  23. package/src/player/components/timelineDropReveal.ts +12 -0
  24. package/src/player/components/useTimelineFocusCoordinator.test.tsx +66 -0
  25. package/src/player/components/useTimelineFocusCoordinator.ts +43 -3
  26. package/src/player/components/useTimelineGeometry.test.tsx +117 -0
  27. package/src/player/components/useTimelineGeometry.ts +7 -2
  28. package/src/player/components/useTimelineGeometryFocusOrder.test.tsx +184 -0
  29. package/src/player/components/useTimelineLogicalFocus.ts +1 -0
  30. package/src/player/lib/timelineElementHelpers.ts +8 -0
  31. package/src/player/store/readinessSlice.ts +4 -3
  32. package/src/utils/blockInstaller.test.ts +4 -0
  33. package/src/utils/blockInstaller.ts +9 -3
  34. package/src/utils/retryBudget.ts +20 -0
  35. package/src/utils/sdkCutover.familyGate.test.ts +5 -1
  36. package/src/utils/sdkCutover.ts +7 -4
  37. package/src/utils/sdkCutoverEligibility.test.ts +96 -1
  38. package/src/utils/sdkCutoverEligibility.ts +34 -11
  39. package/src/utils/timelineCompositionInsert.ts +2 -1
  40. 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
- return (
133
- flagEnabled &&
134
- hasSession &&
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(`${input.targetPath}#${result.hostId}`);
80
+ input.selectHost(deriveTimelineStoreKeyForDomId(result.hostId, input.targetPath));
80
81
  try {
81
82
  input.resync?.();
82
83
  } catch (error) {