@hyperframes/studio 0.6.102 → 0.6.104

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.
@@ -1 +1 @@
1
- import{g as P}from"./index-BzjItfjX.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
1
+ import{g as P}from"./index-CtVcyRD2.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
@@ -1,4 +1,4 @@
1
- import{n as Qi}from"./index-BzjItfjX.js";/*!
1
+ import{n as Qi}from"./index-CtVcyRD2.js";/*!
2
2
  * Copyright (c) 2026-present, Vanilagy and contributors
3
3
  *
4
4
  * This Source Code Form is subject to the terms of the Mozilla Public
package/dist/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-BzjItfjX.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CtVcyRD2.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-BITwbxi-.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.102",
3
+ "version": "0.6.104",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,9 +33,9 @@
33
33
  "@phosphor-icons/react": "^2.1.10",
34
34
  "bpm-detective": "^2.0.5",
35
35
  "mediabunny": "^1.45.3",
36
- "@hyperframes/core": "0.6.102",
37
- "@hyperframes/player": "0.6.102",
38
- "@hyperframes/sdk": "0.6.102"
36
+ "@hyperframes/core": "0.6.104",
37
+ "@hyperframes/player": "0.6.104",
38
+ "@hyperframes/sdk": "0.6.104"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/react": "19",
@@ -49,7 +49,7 @@
49
49
  "vite": "^6.4.2",
50
50
  "vitest": "^3.2.4",
51
51
  "zustand": "^5.0.0",
52
- "@hyperframes/producer": "0.6.102"
52
+ "@hyperframes/producer": "0.6.104"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "react": "19",
package/src/App.tsx CHANGED
@@ -175,7 +175,7 @@ export function StudioApp() {
175
175
  reloadPreview: () => setRefreshKey((k) => k + 1),
176
176
  pendingTimelineEditPathRef,
177
177
  });
178
- const sdkSession = useSdkSession(projectId, activeCompPath);
178
+ const sdkSession = useSdkSession(projectId, activeCompPath ?? "index.html");
179
179
  const timelineEditing = useTimelineEditing({
180
180
  projectId,
181
181
  activeCompPath,
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useCallback, useRef, useMemo } from "react";
2
2
  import { trackStudioRenderStart } from "../../telemetry/events";
3
+ import { getAnonymousId } from "../../telemetry/config";
3
4
 
4
5
  export interface RenderJob {
5
6
  id: string;
@@ -109,10 +110,15 @@ export function useRenderQueue(projectId: string | null) {
109
110
  format: string;
110
111
  resolution?: string;
111
112
  composition?: string;
113
+ telemetryDistinctId: string;
112
114
  } = {
113
115
  fps,
114
116
  quality,
115
117
  format,
118
+ // So the server-emitted render_complete/render_error is attributed to
119
+ // this browser user (same id studio_* events use), making the render
120
+ // funnel joinable. Matches studio_render_start fired just above.
121
+ telemetryDistinctId: getAnonymousId(),
116
122
  };
117
123
  if (resolution && resolution !== "auto") body.resolution = resolution;
118
124
  if (composition) body.composition = composition;
@@ -5,7 +5,7 @@
5
5
  // fallow-ignore-file complexity
6
6
  import { useCallback, useRef } from "react";
7
7
  import type { Composition } from "@hyperframes/sdk";
8
- import { runShadowTiming } from "../utils/sdkShadow";
8
+ import { runShadowDelete, runShadowTiming } from "../utils/sdkShadow";
9
9
  import type { TimelineElement } from "../player";
10
10
  import { usePlayerStore } from "../player";
11
11
  import { useRazorSplit } from "./useRazorSplit";
@@ -288,6 +288,7 @@ export function useTimelineEditing({
288
288
  );
289
289
  usePlayerStore.getState().setSelectedElementId(null);
290
290
  reloadPreview();
291
+ if (sdkSession) runShadowDelete(sdkSession, element.hfId);
291
292
  showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
292
293
  } catch (error) {
293
294
  const message = error instanceof Error ? error.message : "Failed to delete timeline clip";
@@ -303,6 +304,7 @@ export function useTimelineEditing({
303
304
  domEditSaveTimestampRef,
304
305
  reloadPreview,
305
306
  isRecordingRef,
307
+ sdkSession,
306
308
  ],
307
309
  );
308
310
 
@@ -108,6 +108,20 @@ describe("sdkShadowDispatch (integration)", () => {
108
108
  expect(session.getElement("hf-box")?.inlineStyles.color).toBe("#00f");
109
109
  });
110
110
 
111
+ it("does NOT false-mismatch a hyphenated style property (kebab op vs camelCase snapshot)", async () => {
112
+ const { sdkShadowDispatch } = await import("./sdkShadow");
113
+ const session = await openComposition(BASE_HTML);
114
+
115
+ const ops: PatchOperation[] = [
116
+ { type: "inline-style", property: "background-color", value: "rgb(255, 79, 88)" },
117
+ ];
118
+ const result = sdkShadowDispatch(session, "hf-box", ops);
119
+
120
+ expect(result.dispatched).toBe(true);
121
+ expect(result.mismatches).toHaveLength(0); // was 1 before the kebab→camel read-back fix
122
+ expect(session.getElement("hf-box")?.inlineStyles.backgroundColor).toBe("rgb(255, 79, 88)");
123
+ });
124
+
111
125
  it("returns dispatched:false when hfId not found in session", async () => {
112
126
  const { sdkShadowDispatch } = await import("./sdkShadow");
113
127
  const session = await openComposition(BASE_HTML);
@@ -143,6 +157,21 @@ describe("sdkShadowDispatch (integration)", () => {
143
157
  expect(session.getElement("hf-box")?.attributes["data-name"]).toBe("hero");
144
158
  });
145
159
 
160
+ // fallow-ignore-next-line code-duplication
161
+ it("does NOT false-mismatch studio-internal data-hf-* marker attributes", async () => {
162
+ const { sdkShadowDispatch } = await import("./sdkShadow");
163
+ const session = await openComposition(BASE_HTML);
164
+
165
+ // path-offset drags emit these already-data-prefixed, SDK-excluded markers.
166
+ const ops: PatchOperation[] = [
167
+ { type: "attribute", property: "data-hf-studio-path-offset", value: "true" },
168
+ ];
169
+ const result = sdkShadowDispatch(session, "hf-box", ops);
170
+
171
+ expect(result.dispatched).toBe(true);
172
+ expect(result.mismatches).toHaveLength(0); // filtered, not double-prefixed + flagged
173
+ });
174
+
146
175
  it("returns dispatch_error when dispatch throws — does not propagate", async () => {
147
176
  const { sdkShadowDispatch } = await import("./sdkShadow");
148
177
  const session = await openComposition(BASE_HTML);
@@ -301,6 +330,21 @@ tl.to("[data-hf-id=\\"hf-box\\"]", { opacity: "1", duration: 0.5 }, 0);
301
330
  window.__timelines["t"] = tl;`;
302
331
  expect(gsapFidelityMismatches(numeric, stringy)).toEqual([]);
303
332
  });
333
+
334
+ it("matches the same element across different selector forms when a resolver is given", () => {
335
+ // SDK writes [data-hf-id="hf-x"], server writes .x — same element, same tween.
336
+ const sdk = `var tl = gsap.timeline({ paused: true });
337
+ tl.to("[data-hf-id=\\"hf-x\\"]", { x: 200, duration: 0.8 }, 0.5);
338
+ window.__timelines["t"] = tl;`;
339
+ const server = `var tl = gsap.timeline({ paused: true });
340
+ tl.to(".x", { x: 200, duration: 0.8 }, 0.5);
341
+ window.__timelines["t"] = tl;`;
342
+ const resolve = (sel: string) => (/hf-x|\.x/.test(sel) ? "hf-x" : sel);
343
+ // Without a resolver: selector-form divergence → present/absent mismatch.
344
+ expect(gsapFidelityMismatches(sdk, server).length).toBeGreaterThan(0);
345
+ // With a resolver: matched by element → no mismatch.
346
+ expect(gsapFidelityMismatches(sdk, server, resolve)).toEqual([]);
347
+ });
304
348
  });
305
349
 
306
350
  describe("runShadowGsapFidelity", () => {
@@ -23,6 +23,22 @@ import type { PatchOperation } from "./sourcePatcher";
23
23
  * Multiple inline-style ops are coalesced into a single setStyle (SDK batches
24
24
  * style changes naturally). One SDK op is emitted per non-style op.
25
25
  */
26
+ // "attribute" PatchOperations carry the data- attribute NAME. Studio passes
27
+ // some already prefixed (e.g. "data-hf-studio-path-offset") and some bare
28
+ // (e.g. "name"); prefix only when needed, never double-prefix.
29
+ function attrName(property: string): string {
30
+ return property.startsWith("data-") ? property : `data-${property}`;
31
+ }
32
+
33
+ // The SDK element model excludes data-hf-* attributes (document.ts skips them),
34
+ // so shadowing studio-internal markers (data-hf-studio-path-offset, etc.) can
35
+ // never match — drop those ops from the shadow instead of false-mismatching.
36
+ function isShadowableOp(op: PatchOperation): boolean {
37
+ if (op.type === "attribute") return !attrName(op.property).startsWith("data-hf-");
38
+ if (op.type === "html-attribute") return !op.property.startsWith("data-hf-");
39
+ return true;
40
+ }
41
+
26
42
  export function patchOpsToSdkEditOps(hfId: string, ops: PatchOperation[]): EditOp[] {
27
43
  const result: EditOp[] = [];
28
44
  const styles: Record<string, string | null> = {};
@@ -38,7 +54,7 @@ export function patchOpsToSdkEditOps(hfId: string, ops: PatchOperation[]): EditO
38
54
  result.push({
39
55
  type: "setAttribute",
40
56
  target: hfId,
41
- name: `data-${op.property}`,
57
+ name: attrName(op.property),
42
58
  value: op.value,
43
59
  });
44
60
  } else if (op.type === "html-attribute") {
@@ -98,17 +114,24 @@ function flattenSnapshot(snap: ElementSnapshot): FlatSnapshot {
98
114
 
99
115
  type OpFieldResolver = (op: PatchOperation, flat: FlatSnapshot) => OpFields;
100
116
 
117
+ // Snapshot inlineStyles are camelCase (CSSStyleDeclaration convention); PatchOperation
118
+ // style properties are kebab-case ("background-color"). Convert for read-back, else
119
+ // every hyphenated property false-mismatches against a null actual.
120
+ function kebabToCamel(prop: string): string {
121
+ return prop.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
122
+ }
123
+
101
124
  const OP_FIELD_RESOLVERS: Record<string, OpFieldResolver> = {
102
125
  "inline-style": (op, flat) => ({
103
126
  property: op.property,
104
127
  expected: op.value,
105
- actual: flat.styles[op.property] ?? null,
128
+ actual: flat.styles[kebabToCamel(op.property)] ?? flat.styles[op.property] ?? null,
106
129
  }),
107
130
  "text-content": (op, flat) => ({ property: "text", expected: op.value ?? "", actual: flat.text }),
108
131
  attribute: (op, flat) => ({
109
- property: `data-${op.property}`,
132
+ property: attrName(op.property),
110
133
  expected: op.value ?? null,
111
- actual: flat.attrs[`data-${op.property}`] ?? null,
134
+ actual: flat.attrs[attrName(op.property)] ?? null,
112
135
  }),
113
136
  "html-attribute": (op, flat) => ({
114
137
  property: op.property,
@@ -157,8 +180,11 @@ export function sdkShadowDispatch(
157
180
  if (!session.getElement(hfId)) {
158
181
  return { dispatched: false, mismatches: [{ kind: "element_not_found", hfId }] };
159
182
  }
183
+ // Drop studio-internal markers the SDK model can't represent (data-hf-*), so
184
+ // canvas-drag/path-offset edits don't false-mismatch on bookkeeping attrs.
185
+ const shadowable = ops.filter(isShadowableOp);
160
186
  try {
161
- const sdkOps = patchOpsToSdkEditOps(hfId, ops);
187
+ const sdkOps = patchOpsToSdkEditOps(hfId, shadowable);
162
188
  session.batch(() => {
163
189
  for (const op of sdkOps) session.dispatch(op);
164
190
  });
@@ -169,7 +195,7 @@ export function sdkShadowDispatch(
169
195
  };
170
196
  }
171
197
  const flat = flattenSnapshot(session.getElement(hfId));
172
- const mismatches = ops
198
+ const mismatches = shadowable
173
199
  .map((op) => checkOpParity(op, flat, hfId))
174
200
  .filter((m): m is SdkShadowMismatch => m !== null);
175
201
  return { dispatched: true, mismatches };
@@ -36,10 +36,35 @@ function extractGsapScript(html: string): string | null {
36
36
  return null;
37
37
  }
38
38
 
39
- function animById(script: string): Map<string, GsapAnimation> {
39
+ function posKey(position: unknown): string {
40
+ if (typeof position === "number") return String(position);
41
+ const n = Number(position);
42
+ return Number.isNaN(n) ? String(position) : String(n);
43
+ }
44
+
45
+ // Key a tween by its RESOLVED target element (not raw selector) + method +
46
+ // position. The SDK writer emits [data-hf-id="X"] selectors while the server
47
+ // emits class/other selectors for the SAME element; keying by resolved element
48
+ // matches them so the diff compares values instead of flagging present/absent.
49
+ //
50
+ // ponytail: one-tween-per-(element, method, position) assumption — coincident
51
+ // tweens (same element+method+position, different props) collapse, last wins,
52
+ // so the diff under-reports them. Props can't go in the key (a matched pair
53
+ // must share a key for the field-diff to run; raw props would split real value
54
+ // drift into present/absent). Not seen in studio-emitted templates; add a
55
+ // property-NAME hash to the key if coincident tweens show up in the wild.
56
+ function tweenKey(anim: GsapAnimation, resolveSelector?: (sel: string) => string): string {
57
+ const sel = resolveSelector ? resolveSelector(anim.targetSelector) : anim.targetSelector;
58
+ return `${sel}|${anim.method}|${posKey(anim.position)}`;
59
+ }
60
+
61
+ function animByKey(
62
+ script: string,
63
+ resolveSelector?: (sel: string) => string,
64
+ ): Map<string, GsapAnimation> {
40
65
  const map = new Map<string, GsapAnimation>();
41
66
  const parsed = parseGsapScriptAcorn(script);
42
- for (const anim of parsed.animations) map.set(anim.id, anim);
67
+ for (const anim of parsed.animations) map.set(tweenKey(anim, resolveSelector), anim);
43
68
  return map;
44
69
  }
45
70
 
@@ -73,37 +98,41 @@ function canonicalProps(obj: Record<string, unknown> | undefined): string {
73
98
  }
74
99
 
75
100
  /**
76
- * Structurally diff two GSAP scripts by tween id. Reports a tween present in
77
- * one but not the other, and per-field value drift (method, position, duration,
78
- * ease, properties, fromProperties). Comparison is canonical (see above) so
79
- * writer formatting differences do not produce false mismatches.
101
+ * Structurally diff two GSAP scripts. Tweens are matched by resolved target
102
+ * element + method + position (see tweenKey), so the SDK's [data-hf-id]
103
+ * selectors and the server's class selectors for the same element don't
104
+ * false-flag present/absent. Reports a tween present in one but not the other,
105
+ * and per-field value drift (duration, ease, properties, fromProperties).
106
+ * Comparison is canonical so writer formatting differences don't register.
107
+ *
108
+ * Pass resolveSelector (selector → canonical element id) to enable the
109
+ * element-based matching; without it, matching falls back to raw selector.
80
110
  */
81
111
  // fallow-ignore-next-line complexity
82
112
  export function gsapFidelityMismatches(
83
113
  sdkScript: string,
84
114
  serverScript: string,
115
+ resolveSelector?: (sel: string) => string,
85
116
  ): SdkShadowMismatch[] {
86
- const sdk = animById(sdkScript);
87
- const server = animById(serverScript);
117
+ const sdk = animByKey(sdkScript, resolveSelector);
118
+ const server = animByKey(serverScript, resolveSelector);
88
119
  const mismatches: SdkShadowMismatch[] = [];
89
- const ids = new Set([...sdk.keys(), ...server.keys()]);
90
- for (const id of ids) {
91
- const a = sdk.get(id);
92
- const b = server.get(id);
120
+ const keys = new Set([...sdk.keys(), ...server.keys()]);
121
+ for (const key of keys) {
122
+ const a = sdk.get(key);
123
+ const b = server.get(key);
93
124
  if (!a || !b) {
94
125
  mismatches.push({
95
126
  kind: "value_mismatch",
96
- hfId: id,
127
+ hfId: key,
97
128
  property: "tween",
98
129
  expected: b ? "present" : "absent",
99
130
  actual: a ? "present" : "absent",
100
131
  });
101
132
  continue;
102
133
  }
103
- // [property, sdk-value, server-value, equal?]
134
+ // method + position are part of the key (already equal); compare values.
104
135
  const fields: Array<[string, unknown, unknown, boolean]> = [
105
- ["method", a.method, b.method, a.method === b.method],
106
- ["position", a.position, b.position, numericEqual(a.position, b.position)],
107
136
  ["duration", a.duration, b.duration, numericEqual(a.duration, b.duration)],
108
137
  ["ease", a.ease, b.ease, a.ease === b.ease],
109
138
  [
@@ -123,7 +152,7 @@ export function gsapFidelityMismatches(
123
152
  if (!equal) {
124
153
  mismatches.push({
125
154
  kind: "value_mismatch",
126
- hfId: id,
155
+ hfId: key,
127
156
  property,
128
157
  expected: bv == null ? null : JSON.stringify(bv),
129
158
  actual: av == null ? null : JSON.stringify(av),
@@ -158,6 +187,33 @@ export function resolveGsapFidelityArgs(
158
187
  return { before, op: shadowGsapOp, serverScript };
159
188
  }
160
189
 
190
+ // Resolve a CSS selector to a canonical element id (data-hf-id) using the pre-op
191
+ // document, so tweens that target the same element via different selectors
192
+ // ([data-hf-id="X"] vs .X) match in the fidelity diff. Falls back to the raw
193
+ // selector when it can't resolve (DOMParser unavailable, no match, bad selector).
194
+ //
195
+ // ponytail: first-match heuristic — querySelector returns the FIRST match, so an
196
+ // ambiguous selector (e.g. .x shared by two elements) may map to a different id
197
+ // than the SDK side's [data-hf-id] target and still flag present/absent. Safe
198
+ // for studio templates (one tween per data-hf-id); upgrade to querySelectorAll +
199
+ // uniqueness check if ambiguous selectors appear.
200
+ function makeSelectorResolver(html: string): (sel: string) => string {
201
+ let doc: Document | null = null;
202
+ try {
203
+ doc = new DOMParser().parseFromString(html, "text/html");
204
+ } catch {
205
+ doc = null;
206
+ }
207
+ return (sel) => {
208
+ if (!doc) return sel;
209
+ try {
210
+ return doc.querySelector(sel)?.getAttribute("data-hf-id") ?? sel;
211
+ } catch {
212
+ return sel;
213
+ }
214
+ };
215
+ }
216
+
161
217
  /**
162
218
  * Shadow GSAP value fidelity: open a fresh SDK doc from the server's pre-op
163
219
  * file, apply the same tween op, serialize, and diff the SDK's GSAP script
@@ -189,7 +245,11 @@ export async function runShadowGsapFidelity(
189
245
  });
190
246
  return;
191
247
  }
192
- const mismatches = gsapFidelityMismatches(sdkScript, serverScript);
248
+ const mismatches = gsapFidelityMismatches(
249
+ sdkScript,
250
+ serverScript,
251
+ makeSelectorResolver(beforeHtml),
252
+ );
193
253
  trackStudioEvent("sdk_shadow_dispatch", {
194
254
  op: "gsap_fidelity",
195
255
  dispatched: true,