@hyperframes/studio 0.6.110 → 0.6.111

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 (58) hide show
  1. package/dist/assets/index-B7S86vK1.js +370 -0
  2. package/dist/assets/index-DP8pPIk2.css +1 -0
  3. package/dist/assets/{index-CfiyaR7G.js → index-_IV-vm9l.js} +1 -1
  4. package/dist/assets/{index-CXy_mWnP.js → index-x0c2-zQN.js} +1 -1
  5. package/dist/index.html +2 -2
  6. package/package.json +8 -5
  7. package/src/App.tsx +133 -141
  8. package/src/components/StudioHeader.tsx +44 -0
  9. package/src/components/StudioOverlays.tsx +70 -0
  10. package/src/components/editor/SourceEditor.tsx +19 -16
  11. package/src/components/editor/manualEditingAvailability.ts +25 -7
  12. package/src/components/storyboard/FramePoster.tsx +56 -0
  13. package/src/components/storyboard/StoryboardDirection.tsx +45 -0
  14. package/src/components/storyboard/StoryboardFrameFocus.tsx +262 -0
  15. package/src/components/storyboard/StoryboardFrameTile.tsx +88 -0
  16. package/src/components/storyboard/StoryboardGrid.tsx +33 -0
  17. package/src/components/storyboard/StoryboardLoaded.tsx +136 -0
  18. package/src/components/storyboard/StoryboardScriptPanel.tsx +26 -0
  19. package/src/components/storyboard/StoryboardSourceEditor.tsx +231 -0
  20. package/src/components/storyboard/StoryboardStatusLegend.tsx +21 -0
  21. package/src/components/storyboard/StoryboardView.tsx +78 -0
  22. package/src/components/storyboard/frameStatus.ts +36 -0
  23. package/src/contexts/ViewModeContext.tsx +98 -0
  24. package/src/hooks/gsapScriptCommitTypes.ts +4 -7
  25. package/src/hooks/sdkSelfWriteRegistry.test.ts +67 -0
  26. package/src/hooks/sdkSelfWriteRegistry.ts +77 -0
  27. package/src/hooks/timelineEditingHelpers.ts +2 -0
  28. package/src/hooks/useAppHotkeys.ts +20 -0
  29. package/src/hooks/useDomEditCommits.ts +43 -14
  30. package/src/hooks/useDomEditSession.test.ts +41 -0
  31. package/src/hooks/useDomEditSession.ts +38 -6
  32. package/src/hooks/useDomGeometryCommits.ts +5 -9
  33. package/src/hooks/useElementLifecycleOps.ts +30 -0
  34. package/src/hooks/useGsapAnimationOps.ts +78 -51
  35. package/src/hooks/useGsapKeyframeOps.ts +127 -43
  36. package/src/hooks/useGsapPropertyDebounce.test.ts +70 -0
  37. package/src/hooks/useGsapPropertyDebounce.ts +201 -23
  38. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +85 -0
  39. package/src/hooks/useGsapScriptCommits.ts +95 -40
  40. package/src/hooks/useSdkSession.test.ts +12 -0
  41. package/src/hooks/useSdkSession.ts +91 -25
  42. package/src/hooks/useStoryboard.ts +80 -0
  43. package/src/hooks/useTimelineEditing.ts +163 -68
  44. package/src/utils/gsapSoftReload.ts +14 -0
  45. package/src/utils/sdkCutover.gate.test.ts +61 -0
  46. package/src/utils/sdkCutover.test.ts +839 -0
  47. package/src/utils/sdkCutover.ts +465 -0
  48. package/src/utils/sdkOpMapping.ts +43 -0
  49. package/src/utils/sdkResolverShadow.test.ts +366 -0
  50. package/src/utils/sdkResolverShadow.ts +313 -0
  51. package/dist/assets/index-1C8oFiPi.css +0 -1
  52. package/dist/assets/index-D-3sGz65.js +0 -296
  53. package/src/utils/sdkShadow.test.ts +0 -606
  54. package/src/utils/sdkShadow.ts +0 -517
  55. package/src/utils/sdkShadowGsapFidelity.ts +0 -296
  56. package/src/utils/sdkShadowGsapKeyframe.test.ts +0 -265
  57. package/src/utils/sdkShadowGsapKeyframe.ts +0 -257
  58. package/src/utils/sdkShadowNumeric.ts +0 -11
@@ -1,517 +0,0 @@
1
- /**
2
- * SDK shadow dispatch utilities for Stage 7 Step 3b.
3
- *
4
- * Shadow mode keeps the server patch path authoritative while also dispatching
5
- * the equivalent op to the SDK session, then compares the result to detect
6
- * addressing gaps (blocker E: no-hf-id elements) and serialization drift
7
- * (blocker B: linkedom whole-doc serialize). Results are reported as structured
8
- * mismatches for telemetry — no user-visible change.
9
- */
10
-
11
- import type { Composition } from "@hyperframes/sdk";
12
- import type { EditOp, GsapTweenSpec } from "@hyperframes/sdk";
13
- import { STUDIO_SDK_SHADOW_ENABLED } from "../components/editor/manualEditingAvailability";
14
- import { trackStudioEvent } from "./studioTelemetry";
15
- import { relEqual } from "./sdkShadowNumeric";
16
- import type { DomEditSelection } from "../components/editor/domEditingTypes";
17
- import type { PatchOperation } from "./sourcePatcher";
18
-
19
- // ─── Op mapping ──────────────────────────────────────────────────────────────
20
-
21
- /**
22
- * Map Studio PatchOperations for a given hf-id to SDK EditOps.
23
- *
24
- * Multiple inline-style ops are coalesced into a single setStyle (SDK batches
25
- * style changes naturally). One SDK op is emitted per non-style op.
26
- */
27
- // "attribute" PatchOperations carry the data- attribute NAME. Studio passes
28
- // some already prefixed (e.g. "data-hf-studio-path-offset") and some bare
29
- // (e.g. "name"); prefix only when needed, never double-prefix.
30
- function attrName(property: string): string {
31
- return property.startsWith("data-") ? property : `data-${property}`;
32
- }
33
-
34
- // The SDK element model excludes data-hf-* attributes (document.ts skips them),
35
- // so shadowing studio-internal markers (data-hf-studio-path-offset, etc.) can
36
- // never match — drop those ops from the shadow instead of false-mismatching.
37
- function isShadowableOp(op: PatchOperation): boolean {
38
- if (op.type === "attribute") return !attrName(op.property).startsWith("data-hf-");
39
- if (op.type === "html-attribute") return !op.property.startsWith("data-hf-");
40
- return true;
41
- }
42
-
43
- // PatchOperation types patchOpsToSdkEditOps knows how to map. Used by
44
- // runShadowDispatch to flag any unmapped type as visible telemetry rather than
45
- // silently dropping it (see the unmapped_type guard there).
46
- const MAPPED_PATCH_OP_TYPES: ReadonlySet<string> = new Set([
47
- "inline-style",
48
- "text-content",
49
- "attribute",
50
- "html-attribute",
51
- ]);
52
-
53
- export function patchOpsToSdkEditOps(hfId: string, ops: PatchOperation[]): EditOp[] {
54
- const result: EditOp[] = [];
55
- const styles: Record<string, string | null> = {};
56
- let hasStyles = false;
57
-
58
- for (const op of ops) {
59
- if (op.type === "inline-style") {
60
- styles[op.property] = op.value;
61
- hasStyles = true;
62
- } else if (op.type === "text-content") {
63
- result.push({ type: "setText", target: hfId, value: op.value ?? "" });
64
- } else if (op.type === "attribute") {
65
- result.push({
66
- type: "setAttribute",
67
- target: hfId,
68
- name: attrName(op.property),
69
- value: op.value,
70
- });
71
- } else if (op.type === "html-attribute") {
72
- result.push({ type: "setAttribute", target: hfId, name: op.property, value: op.value });
73
- }
74
- // unknown op types produce no SDK op
75
- }
76
-
77
- if (hasStyles) {
78
- result.unshift({ type: "setStyle", target: hfId, styles });
79
- }
80
-
81
- return result;
82
- }
83
-
84
- // ─── Shadow result types ──────────────────────────────────────────────────────
85
-
86
- export interface SdkShadowMismatch {
87
- kind: "element_not_found" | "value_mismatch" | "dispatch_error";
88
- hfId: string;
89
- property?: string;
90
- expected?: string | null;
91
- actual?: string | null | undefined;
92
- error?: string;
93
- }
94
-
95
- export interface SdkShadowResult {
96
- /** False if the element was not found in the SDK session. */
97
- dispatched: boolean;
98
- mismatches: SdkShadowMismatch[];
99
- }
100
-
101
- // ─── Shadow dispatch ──────────────────────────────────────────────────────────
102
-
103
- type ElementSnapshot = ReturnType<Composition["getElement"]>;
104
- type OpFields = {
105
- property: string;
106
- expected: string | null | undefined;
107
- actual: string | null | undefined;
108
- };
109
-
110
- type FlatSnapshot = {
111
- styles: Record<string, string | null>;
112
- attrs: Record<string, string | null>;
113
- text: string | null;
114
- };
115
-
116
- function flattenSnapshot(snap: ElementSnapshot): FlatSnapshot {
117
- return {
118
- styles: snap?.inlineStyles ?? {},
119
- attrs: Object.fromEntries(
120
- Object.entries(snap?.attributes ?? {}).map(([k, v]) => [k, v ?? null]),
121
- ),
122
- text: snap?.text ?? null,
123
- };
124
- }
125
-
126
- type OpFieldResolver = (op: PatchOperation, flat: FlatSnapshot) => OpFields;
127
-
128
- // Snapshot inlineStyles are camelCase (CSSStyleDeclaration convention); PatchOperation
129
- // style properties are kebab-case ("background-color"). Convert for read-back, else
130
- // every hyphenated property false-mismatches against a null actual.
131
- function kebabToCamel(prop: string): string {
132
- return prop.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
133
- }
134
-
135
- // Text parity: the SDK snapshot.text is trimmed, so trim the op value too.
136
- // An empty string and absent text (null) are treated as equivalent (collapsed
137
- // to null) so "" vs null does not flag — both mean "no text content".
138
- function normalizeText(value: string | null | undefined): string | null {
139
- if (value == null) return null;
140
- const trimmed = value.trim();
141
- return trimmed === "" ? null : trimmed;
142
- }
143
-
144
- const OP_FIELD_RESOLVERS: Record<string, OpFieldResolver> = {
145
- "inline-style": (op, flat) => ({
146
- property: op.property,
147
- expected: op.value,
148
- actual: flat.styles[kebabToCamel(op.property)] ?? flat.styles[op.property] ?? null,
149
- }),
150
- // snapshot.text is already TRIMMED; trim the expected op value to match, so
151
- // trailing-whitespace differences don't flag. Empty-vs-absent ("" vs null) is
152
- // collapsed in checkOpParity. A genuinely different text value still flags.
153
- "text-content": (op, flat) => ({
154
- property: "text",
155
- expected: normalizeText(op.value),
156
- actual: normalizeText(flat.text),
157
- }),
158
- attribute: (op, flat) => ({
159
- property: attrName(op.property),
160
- expected: op.value ?? null,
161
- actual: flat.attrs[attrName(op.property)] ?? null,
162
- }),
163
- "html-attribute": (op, flat) => ({
164
- property: op.property,
165
- expected: op.value ?? null,
166
- actual: flat.attrs[op.property] ?? null,
167
- }),
168
- };
169
-
170
- function resolveOpFields(op: PatchOperation, flat: FlatSnapshot): OpFields | null {
171
- return OP_FIELD_RESOLVERS[op.type]?.(op, flat) ?? null;
172
- }
173
-
174
- function checkOpParity(
175
- op: PatchOperation,
176
- flat: FlatSnapshot,
177
- hfId: string,
178
- ): SdkShadowMismatch | null {
179
- const fields = resolveOpFields(op, flat);
180
- if (!fields || fields.actual === fields.expected) return null;
181
- return { kind: "value_mismatch", hfId, ...fields };
182
- }
183
-
184
- /**
185
- * Dispatch PatchOperations to the SDK session and return a parity report.
186
- *
187
- * If the element is not found by hfId, returns dispatched:false with a
188
- * element_not_found mismatch (signals blocker E — element has no hf-id or
189
- * SDK can't address it).
190
- *
191
- * On success, verifies that the SDK element snapshot reflects the applied
192
- * values. Value mismatches indicate serialization or normalization drift.
193
- *
194
- * **persist:error drift risk**: the HTTP adapter fires persist:error on
195
- * network failure but the SDK session is already mutated at that point. If
196
- * the server file was not updated (e.g. 503), subsequent shadow parity
197
- * comparisons here will see a diverged SDK session and produce false
198
- * positives. Before flipping STUDIO_SDK_DISPATCH_ENABLED, verify the shadow
199
- * window is clear of persist:error events.
200
- */
201
-
202
- export function sdkShadowDispatch(
203
- session: Composition,
204
- hfId: string,
205
- ops: PatchOperation[],
206
- ): SdkShadowResult {
207
- if (!session.getElement(hfId)) {
208
- return { dispatched: false, mismatches: [{ kind: "element_not_found", hfId }] };
209
- }
210
- // Drop studio-internal markers the SDK model can't represent (data-hf-*), so
211
- // canvas-drag/path-offset edits don't false-mismatch on bookkeeping attrs.
212
- const shadowable = ops.filter(isShadowableOp);
213
- try {
214
- const sdkOps = patchOpsToSdkEditOps(hfId, shadowable);
215
- session.batch(() => {
216
- for (const op of sdkOps) session.dispatch(op);
217
- });
218
- } catch (err) {
219
- return {
220
- dispatched: false,
221
- mismatches: [{ kind: "dispatch_error", hfId, error: String(err) }],
222
- };
223
- }
224
- const flat = flattenSnapshot(session.getElement(hfId));
225
- const mismatches = shadowable
226
- .map((op) => checkOpParity(op, flat, hfId))
227
- .filter((m): m is SdkShadowMismatch => m !== null);
228
- return { dispatched: true, mismatches };
229
- }
230
-
231
- // ─── Telemetry reporting ──────────────────────────────────────────────────────
232
-
233
- /**
234
- * Shadow-dispatch ops to the SDK session and emit sdk_shadow_dispatch telemetry.
235
- * Despite the telemetry focus, this function does mutate the SDK session — it
236
- * is not read-only. No-op when STUDIO_SDK_SHADOW_ENABLED is false.
237
- */
238
- // Property-path mismatches carry user content (inline-style values, edited
239
- // text) in expected/actual. Scrub before telemetry: fully redact text-content
240
- // values, length-cap the rest. The in-memory parity result keeps raw values.
241
- function redactValueForTelemetry(
242
- property: string | undefined,
243
- value: string | null | undefined,
244
- ): string | null | undefined {
245
- if (value == null) return value;
246
- if (property === "text") return `[redacted len=${value.length}]`;
247
- return value.length > 64 ? `${value.slice(0, 64)}…` : value;
248
- }
249
-
250
- function redactMismatchesForTelemetry(mismatches: SdkShadowMismatch[]): SdkShadowMismatch[] {
251
- return mismatches.map((m) => ({
252
- ...m,
253
- expected: redactValueForTelemetry(m.property, m.expected),
254
- actual: redactValueForTelemetry(m.property, m.actual),
255
- }));
256
- }
257
-
258
- export function runShadowDispatch(
259
- session: Composition,
260
- selection: DomEditSelection,
261
- ops: PatchOperation[],
262
- ): void {
263
- if (!STUDIO_SDK_SHADOW_ENABLED) return;
264
- const hfId = selection.hfId;
265
- if (!hfId) {
266
- trackStudioEvent("sdk_shadow_dispatch", {
267
- op: "property",
268
- dispatched: false,
269
- reason: "no_hf_id",
270
- mismatchCount: 0,
271
- });
272
- return;
273
- }
274
- // Defensive: patchOpsToSdkEditOps silently drops PatchOperation types it
275
- // doesn't map. PatchOperation.type is a closed union today, but emit a visible
276
- // unmapped_type event if a future type ever slips through, so the gap surfaces
277
- // in telemetry instead of vanishing.
278
- // Map to the type string before find, so a future unmapped type is read as a
279
- // plain string (no object cast; find on the closed union narrows to never).
280
- const unmappedType = ops.map((op) => op.type).find((t) => !MAPPED_PATCH_OP_TYPES.has(t));
281
- if (unmappedType !== undefined) {
282
- trackStudioEvent("sdk_shadow_dispatch", {
283
- op: "property",
284
- dispatched: false,
285
- reason: "unmapped_type",
286
- type: unmappedType,
287
- mismatchCount: 0,
288
- });
289
- return;
290
- }
291
- const result = sdkShadowDispatch(session, hfId, ops);
292
- trackStudioEvent("sdk_shadow_dispatch", {
293
- op: "property",
294
- dispatched: result.dispatched,
295
- mismatchCount: result.mismatches.length,
296
- mismatches: JSON.stringify(redactMismatchesForTelemetry(result.mismatches)),
297
- });
298
- }
299
-
300
- // ─── Shadow for non-PatchOperation ops (delete / timing / GSAP) ───────────────
301
- //
302
- // These ops never flow through persistDomEditOperations, so the property-path
303
- // shadow above never sees them. Each runner keeps the server authoritative and
304
- // only observes the SDK: can() pre-checks addressing/validity (pure, no
305
- // mutation — works even for GSAP, which has no element-snapshot value), then a
306
- // dispatch into the live session with a snapshot-based parity check.
307
- //
308
- // Parity coverage by op:
309
- // delete → getElement(id) === null (full)
310
- // timing → snapshot.start/duration/trackIndex (full)
311
- // gsap → tween id present/absent in animationIds (existence only — the
312
- // tween's property values are script-level, not in the snapshot)
313
-
314
- /**
315
- * can()-gated shadow dispatch. Emits sdk_shadow_dispatch tagged with `opLabel`.
316
- * Mutates the SDK session (not read-only); server stays authoritative.
317
- * No-op when STUDIO_SDK_SHADOW_ENABLED is false.
318
- */
319
- function runShadowEditOp(
320
- session: Composition,
321
- op: EditOp,
322
- opLabel: string,
323
- dispatchAndCheck: () => SdkShadowMismatch[],
324
- ): void {
325
- const verdict = session.can(op);
326
- if (!verdict.ok) {
327
- trackStudioEvent("sdk_shadow_dispatch", {
328
- op: opLabel,
329
- dispatched: false,
330
- reason: "cannot_dispatch",
331
- code: verdict.code,
332
- mismatchCount: 0,
333
- });
334
- return;
335
- }
336
- let mismatches: SdkShadowMismatch[];
337
- try {
338
- mismatches = dispatchAndCheck();
339
- } catch (err) {
340
- trackStudioEvent("sdk_shadow_dispatch", {
341
- op: opLabel,
342
- dispatched: false,
343
- reason: "dispatch_error",
344
- error: String(err),
345
- mismatchCount: 0,
346
- });
347
- return;
348
- }
349
- trackStudioEvent("sdk_shadow_dispatch", {
350
- op: opLabel,
351
- dispatched: true,
352
- mismatchCount: mismatches.length,
353
- mismatches: JSON.stringify(mismatches),
354
- });
355
- }
356
-
357
- /** Shadow an element delete. Parity: the element is gone from the SDK session. */
358
- export function runShadowDelete(session: Composition, hfId: string | null | undefined): void {
359
- if (!STUDIO_SDK_SHADOW_ENABLED) return;
360
- if (!hfId) {
361
- trackStudioEvent("sdk_shadow_dispatch", {
362
- op: "delete",
363
- dispatched: false,
364
- reason: "no_hf_id",
365
- mismatchCount: 0,
366
- });
367
- return;
368
- }
369
- const op: EditOp = { type: "removeElement", target: hfId };
370
- runShadowEditOp(session, op, "delete", () => {
371
- session.batch(() => session.dispatch(op));
372
- return session.getElement(hfId)
373
- ? [
374
- {
375
- kind: "value_mismatch",
376
- hfId,
377
- property: "exists",
378
- expected: "removed",
379
- actual: "present",
380
- },
381
- ]
382
- : [];
383
- });
384
- }
385
-
386
- export interface ShadowTiming {
387
- start?: number;
388
- duration?: number;
389
- trackIndex?: number;
390
- }
391
-
392
- // start/duration tolerate float-precision drift (SDK computes them
393
- // arithmetically, server stores a rounded literal) via the shared relative
394
- // epsilon; trackIndex (integer track slot) is compared exactly.
395
- function timingFieldEqual(
396
- key: keyof ShadowTiming,
397
- actual: number | null | undefined,
398
- expected: number,
399
- ): boolean {
400
- if (typeof actual === "number" && key !== "trackIndex") {
401
- return relEqual(actual, expected);
402
- }
403
- return actual === expected;
404
- }
405
-
406
- /** Shadow a timing edit. Parity: snapshot start/duration/trackIndex match. */
407
- export function runShadowTiming(
408
- session: Composition,
409
- hfId: string | null | undefined,
410
- timing: ShadowTiming,
411
- ): void {
412
- if (!STUDIO_SDK_SHADOW_ENABLED) return;
413
- if (!hfId) {
414
- trackStudioEvent("sdk_shadow_dispatch", {
415
- op: "timing",
416
- dispatched: false,
417
- reason: "no_hf_id",
418
- mismatchCount: 0,
419
- });
420
- return;
421
- }
422
- const op: EditOp = { type: "setTiming", target: hfId, ...timing };
423
- runShadowEditOp(session, op, "timing", () => {
424
- session.batch(() => session.dispatch(op));
425
- const el = session.getElement(hfId);
426
- const mismatches: SdkShadowMismatch[] = [];
427
- const fields: Array<[keyof ShadowTiming, number | null | undefined]> = [
428
- ["start", el?.start],
429
- ["duration", el?.duration],
430
- ["trackIndex", el?.trackIndex],
431
- ];
432
- for (const [key, actual] of fields) {
433
- const expected = timing[key];
434
- if (expected === undefined || timingFieldEqual(key, actual, expected)) continue;
435
- mismatches.push({
436
- kind: "value_mismatch",
437
- hfId,
438
- property: key,
439
- expected: String(expected),
440
- actual: actual == null ? null : String(actual),
441
- });
442
- }
443
- return mismatches;
444
- });
445
- }
446
-
447
- export type ShadowGsapOp =
448
- | { kind: "add"; target: string; tween: GsapTweenSpec }
449
- | { kind: "set"; animationId: string; properties: Partial<GsapTweenSpec> }
450
- | { kind: "remove"; animationId: string };
451
-
452
- /**
453
- * Shadow a GSAP tween mutation (add / set / remove). The server's animationId
454
- * shares the SDK's id-space (both derive `targetSelector-method-position` from
455
- * the same acorn parser — see sdk assignStableIds), so it is dispatchable as-is.
456
- *
457
- * Parity via the now-populated ElementSnapshot.animationIds:
458
- * add → the returned tween id is present on the target element
459
- * remove → the id is gone from every element
460
- * set → existence only (the SDK exposes no per-tween property reader; value
461
- * fidelity would need serialize()-script round-trip diffing).
462
- */
463
- export function runShadowGsapTween(session: Composition, gsapOp: ShadowGsapOp): void {
464
- if (!STUDIO_SDK_SHADOW_ENABLED) return;
465
- const op: EditOp =
466
- gsapOp.kind === "add"
467
- ? { type: "addGsapTween", target: gsapOp.target, tween: gsapOp.tween }
468
- : gsapOp.kind === "set"
469
- ? { type: "setGsapTween", animationId: gsapOp.animationId, properties: gsapOp.properties }
470
- : { type: "removeGsapTween", animationId: gsapOp.animationId };
471
- // fallow-ignore-next-line complexity
472
- runShadowEditOp(session, op, "gsap", () => {
473
- let newId: string | undefined;
474
- session.batch(() => {
475
- if (gsapOp.kind === "add") newId = session.addGsapTween(gsapOp.target, gsapOp.tween);
476
- else session.dispatch(op);
477
- });
478
- if (gsapOp.kind === "add") {
479
- const onTarget = session.getElement(gsapOp.target)?.animationIds ?? [];
480
- if (!newId || !onTarget.includes(newId)) {
481
- return [
482
- {
483
- kind: "value_mismatch",
484
- hfId: gsapOp.target,
485
- property: "animationIds",
486
- expected: newId ?? "non-empty",
487
- actual: onTarget.join(",") || null,
488
- },
489
- ];
490
- }
491
- } else if (gsapOp.kind === "remove") {
492
- const stillPresent = session
493
- .getElements()
494
- .some((el) => el.animationIds.includes(gsapOp.animationId));
495
- if (stillPresent) {
496
- return [
497
- {
498
- kind: "value_mismatch",
499
- hfId: gsapOp.animationId,
500
- property: "animationIds",
501
- expected: "removed",
502
- actual: "present",
503
- },
504
- ];
505
- }
506
- }
507
- return [];
508
- });
509
- }
510
-
511
- // GSAP value-fidelity diff lives in its own module to keep this file under the
512
- // 600-line studio cap; re-exported here so the shadow surface stays in one place.
513
- export {
514
- gsapFidelityMismatches,
515
- resolveGsapFidelityArgs,
516
- runShadowGsapFidelity,
517
- } from "./sdkShadowGsapFidelity";