@hyperframes/studio 0.6.109 → 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 (66) hide show
  1. package/dist/assets/hyperframes-player-BRjQ5a5_.js +418 -0
  2. package/dist/assets/index-B7S86vK1.js +370 -0
  3. package/dist/assets/index-DP8pPIk2.css +1 -0
  4. package/dist/assets/{index-SlMJloLR.js → index-_IV-vm9l.js} +1 -1
  5. package/dist/assets/{index-CFCe0Xvn.js → index-x0c2-zQN.js} +1 -1
  6. package/dist/index.html +2 -2
  7. package/package.json +8 -5
  8. package/src/App.tsx +133 -141
  9. package/src/components/StudioHeader.tsx +44 -0
  10. package/src/components/StudioOverlays.tsx +70 -0
  11. package/src/components/editor/SourceEditor.tsx +19 -16
  12. package/src/components/editor/gsapAnimationConstants.ts +24 -0
  13. package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
  14. package/src/components/editor/manualEditingAvailability.ts +25 -7
  15. package/src/components/storyboard/FramePoster.tsx +56 -0
  16. package/src/components/storyboard/StoryboardDirection.tsx +45 -0
  17. package/src/components/storyboard/StoryboardFrameFocus.tsx +262 -0
  18. package/src/components/storyboard/StoryboardFrameTile.tsx +88 -0
  19. package/src/components/storyboard/StoryboardGrid.tsx +33 -0
  20. package/src/components/storyboard/StoryboardLoaded.tsx +136 -0
  21. package/src/components/storyboard/StoryboardScriptPanel.tsx +26 -0
  22. package/src/components/storyboard/StoryboardSourceEditor.tsx +231 -0
  23. package/src/components/storyboard/StoryboardStatusLegend.tsx +21 -0
  24. package/src/components/storyboard/StoryboardView.tsx +78 -0
  25. package/src/components/storyboard/frameStatus.ts +36 -0
  26. package/src/contexts/ViewModeContext.tsx +98 -0
  27. package/src/hooks/gsapScriptCommitTypes.ts +4 -7
  28. package/src/hooks/sdkSelfWriteRegistry.test.ts +67 -0
  29. package/src/hooks/sdkSelfWriteRegistry.ts +77 -0
  30. package/src/hooks/timelineEditingHelpers.ts +2 -0
  31. package/src/hooks/useAppHotkeys.ts +20 -0
  32. package/src/hooks/useDomEditCommits.ts +43 -14
  33. package/src/hooks/useDomEditSession.test.ts +41 -0
  34. package/src/hooks/useDomEditSession.ts +38 -6
  35. package/src/hooks/useDomGeometryCommits.ts +5 -9
  36. package/src/hooks/useElementLifecycleOps.ts +30 -0
  37. package/src/hooks/useGsapAnimationOps.ts +78 -51
  38. package/src/hooks/useGsapKeyframeOps.ts +127 -43
  39. package/src/hooks/useGsapPropertyDebounce.test.ts +70 -0
  40. package/src/hooks/useGsapPropertyDebounce.ts +201 -23
  41. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +85 -0
  42. package/src/hooks/useGsapScriptCommits.ts +95 -40
  43. package/src/hooks/useGsapTweenCache.ts +0 -27
  44. package/src/hooks/useRazorSplit.ts +4 -10
  45. package/src/hooks/useSdkSession.test.ts +12 -0
  46. package/src/hooks/useSdkSession.ts +91 -25
  47. package/src/hooks/useStoryboard.ts +80 -0
  48. package/src/hooks/useTimelineEditing.ts +163 -68
  49. package/src/utils/gsapSoftReload.ts +14 -0
  50. package/src/utils/sdkCutover.gate.test.ts +61 -0
  51. package/src/utils/sdkCutover.test.ts +839 -0
  52. package/src/utils/sdkCutover.ts +465 -0
  53. package/src/utils/sdkOpMapping.ts +43 -0
  54. package/src/utils/sdkResolverShadow.test.ts +366 -0
  55. package/src/utils/sdkResolverShadow.ts +313 -0
  56. package/src/utils/timelineElementSplit.test.ts +61 -1
  57. package/src/utils/timelineElementSplit.ts +18 -0
  58. package/dist/assets/hyperframes-player-67pq7USK.js +0 -418
  59. package/dist/assets/index-BVqybwMG.css +0 -1
  60. package/dist/assets/index-ho_f4axK.js +0 -296
  61. package/src/utils/sdkShadow.test.ts +0 -606
  62. package/src/utils/sdkShadow.ts +0 -517
  63. package/src/utils/sdkShadowGsapFidelity.ts +0 -296
  64. package/src/utils/sdkShadowGsapKeyframe.test.ts +0 -265
  65. package/src/utils/sdkShadowGsapKeyframe.ts +0 -257
  66. package/src/utils/sdkShadowNumeric.ts +0 -11
@@ -1,257 +0,0 @@
1
- /**
2
- * GSAP keyframe-op shadow (serialize round-trip diff). New module for the Stage 7
3
- * shadow-parity push — kept out of sdkShadow.ts / sdkShadowGsapFidelity.ts so the
4
- * shared files stay untouched (only additive imports) and the studio 600-line cap
5
- * holds.
6
- *
7
- * Unlike tweens, the SDK exposes NO keyframe reader on ElementSnapshot, so there
8
- * is no existence-parity path here. Instead we compare the two writers' output:
9
- * open a fresh SDK doc from the server's pre-op file, dispatch the equivalent
10
- * keyframe op, serialize, and diff the SDK's GSAP script against the server's
11
- * resulting script.
12
- *
13
- * gsapFidelityMismatches (reused) matches tweens by resolved target element +
14
- * method + position and diffs tween-level fields — but it does NOT look inside a
15
- * tween's `keyframes` array. Keyframe drift therefore needs a dedicated diff,
16
- * layered on top of the reused tween-level diff, matched by the GSAP animation id.
17
- *
18
- * SDK mapping (main, pre PR #1498 percentage-variant):
19
- * add → addGsapKeyframe{animationId, position: percentage, value: properties}
20
- * remove → removeGsapKeyframe{animationId, keyframeIndex} — the studio op is
21
- * percentage-based, so we resolve percentage → index against the pre-op
22
- * script (KF_PERCENT_TOLERANCE, aligned with the writer ~0.001) and
23
- * no-op on ambiguity (duplicate-percentage keyframes can't be told
24
- * apart by percentage — landmine from PR #1498).
25
- */
26
-
27
- import { openComposition } from "@hyperframes/sdk";
28
- import type { EditOp } from "@hyperframes/sdk";
29
- import { parseGsapScriptAcorn } from "@hyperframes/core/gsap-parser-acorn";
30
- import type { GsapPercentageKeyframe } from "@hyperframes/core/gsap-parser";
31
- import { STUDIO_SDK_SHADOW_ENABLED } from "../components/editor/manualEditingAvailability";
32
- import { trackStudioEvent } from "./studioTelemetry";
33
- import type { SdkShadowMismatch } from "./sdkShadow";
34
- import {
35
- extractGsapScript,
36
- gsapFidelityMismatches,
37
- makeSelectorResolver,
38
- } from "./sdkShadowGsapFidelity";
39
-
40
- // Match the GSAP writer's percentage equality tolerance so a remove resolves to
41
- // the same keyframe the server would pick (writer rounds to ~3 decimals).
42
- const KF_PERCENT_TOLERANCE = 0.001;
43
-
44
- export type ShadowKeyframeOp =
45
- | {
46
- kind: "add";
47
- animationId: string;
48
- percentage: number;
49
- properties: Record<string, number | string>;
50
- }
51
- | { kind: "remove"; animationId: string; percentage: number };
52
-
53
- // ─── percentage → SDK op mapping ──────────────────────────────────────────────
54
-
55
- function findAnimationKeyframes(
56
- script: string,
57
- animationId: string,
58
- ): GsapPercentageKeyframe[] | null {
59
- const parsed = parseGsapScriptAcorn(script);
60
- // Match the writer's locateAnimationWithFallback (gsapParser.ts): a from/fromTo
61
- // tween's derived id may be normalized to "-to-" on write, so fall back to the
62
- // converted id when the exact one isn't found — otherwise the keyframe diff
63
- // goes blind (both scripts resolve null → falsely "clean") on converted tweens.
64
- const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
65
- const anim =
66
- parsed.animations.find((a) => a.id === animationId) ??
67
- parsed.animations.find((a) => a.id === convertedId);
68
- return anim?.keyframes?.keyframes ?? null;
69
- }
70
-
71
- export interface KeyframeRemoveResolution {
72
- /** Resolved 0-based index, or null when it can't be safely resolved. */
73
- keyframeIndex: number | null;
74
- /** Why no index — for telemetry when keyframeIndex is null. */
75
- reason?: "no_keyframes" | "not_found" | "ambiguous";
76
- }
77
-
78
- /**
79
- * Resolve a percentage-based remove to a keyframe index against the pre-op
80
- * script. Returns null index (with a reason) when there are no keyframes, the
81
- * percentage matches none, or — per the PR #1498 landmine — more than one
82
- * keyframe shares the percentage (can't be disambiguated by percentage alone).
83
- * Pure + exported so the mapping is unit-testable without an SDK session.
84
- */
85
- export function resolveKeyframeIndexByPercentage(
86
- script: string | null | undefined,
87
- animationId: string,
88
- percentage: number,
89
- ): KeyframeRemoveResolution {
90
- if (!script) return { keyframeIndex: null, reason: "no_keyframes" };
91
- const kfs = findAnimationKeyframes(script, animationId);
92
- if (!kfs || kfs.length === 0) return { keyframeIndex: null, reason: "no_keyframes" };
93
- const matches: number[] = [];
94
- for (let i = 0; i < kfs.length; i++) {
95
- if (Math.abs(kfs[i]?.percentage - percentage) <= KF_PERCENT_TOLERANCE) matches.push(i);
96
- }
97
- if (matches.length === 0) return { keyframeIndex: null, reason: "not_found" };
98
- if (matches.length > 1) return { keyframeIndex: null, reason: "ambiguous" };
99
- return { keyframeIndex: matches[0] };
100
- }
101
-
102
- /**
103
- * Map a studio keyframe op to the SDK EditOp. For a remove this needs the pre-op
104
- * script to resolve percentage → index; returns null (with a reason) when the
105
- * index can't be safely resolved so the caller can emit a no-op-with-reason
106
- * event instead of dispatching the wrong keyframe.
107
- */
108
- export function keyframeOpToEditOp(
109
- op: ShadowKeyframeOp,
110
- beforeScript: string | null | undefined,
111
- ): { op: EditOp } | { op: null; reason: string } {
112
- if (op.kind === "add") {
113
- return {
114
- op: {
115
- type: "addGsapKeyframe",
116
- animationId: op.animationId,
117
- position: op.percentage,
118
- value: op.properties,
119
- },
120
- };
121
- }
122
- const resolved = resolveKeyframeIndexByPercentage(beforeScript, op.animationId, op.percentage);
123
- if (resolved.keyframeIndex === null) {
124
- return { op: null, reason: resolved.reason ?? "unresolved" };
125
- }
126
- return {
127
- op: {
128
- type: "removeGsapKeyframe",
129
- animationId: op.animationId,
130
- keyframeIndex: resolved.keyframeIndex,
131
- },
132
- };
133
- }
134
-
135
- // ─── Keyframe-aware fidelity diff ─────────────────────────────────────────────
136
-
137
- function canonicalKeyframe(kf: GsapPercentageKeyframe): string {
138
- const props: Record<string, unknown> = {};
139
- for (const key of Object.keys(kf.properties).sort()) {
140
- const v = kf.properties[key];
141
- props[key] =
142
- typeof v === "string" && v.trim() !== "" && !Number.isNaN(Number(v)) ? Number(v) : v;
143
- }
144
- return JSON.stringify({ pct: Math.round(kf.percentage * 1000) / 1000, ease: kf.ease, props });
145
- }
146
-
147
- function canonicalKeyframes(kfs: GsapPercentageKeyframe[] | null): string {
148
- if (!kfs) return "[]";
149
- return JSON.stringify(
150
- [...kfs].sort((a, b) => a.percentage - b.percentage).map(canonicalKeyframe),
151
- );
152
- }
153
-
154
- /**
155
- * Diff two GSAP scripts for a keyframe op: the reused tween-level diff PLUS a
156
- * keyframe-array comparison for the targeted animation (which the tween-level
157
- * diff doesn't inspect). Reports a `keyframes` value_mismatch when the SDK and
158
- * server keyframe arrays diverge canonically.
159
- */
160
- export function gsapKeyframeFidelityMismatches(
161
- sdkScript: string,
162
- serverScript: string,
163
- animationId: string,
164
- resolveSelector?: (sel: string) => string,
165
- ): SdkShadowMismatch[] {
166
- const mismatches = gsapFidelityMismatches(sdkScript, serverScript, resolveSelector);
167
- const sdkKfs = findAnimationKeyframes(sdkScript, animationId);
168
- const serverKfs = findAnimationKeyframes(serverScript, animationId);
169
- const sdkCanon = canonicalKeyframes(sdkKfs);
170
- const serverCanon = canonicalKeyframes(serverKfs);
171
- if (sdkCanon !== serverCanon) {
172
- mismatches.push({
173
- kind: "value_mismatch",
174
- hfId: animationId,
175
- property: "keyframes",
176
- expected: serverCanon,
177
- actual: sdkCanon,
178
- });
179
- }
180
- return mismatches;
181
- }
182
-
183
- // ─── Telemetry runner ─────────────────────────────────────────────────────────
184
-
185
- /**
186
- * Shadow a GSAP keyframe op: open a fresh SDK doc from the server's pre-op file,
187
- * apply the equivalent keyframe op, serialize, and diff against the server's
188
- * resulting script. Emits sdk_shadow_dispatch op: "gsap_keyframe". Async,
189
- * fire-and-forget; server stays authoritative. No-op when shadow is disabled.
190
- */
191
- export async function runShadowGsapKeyframeFidelity(
192
- beforeHtml: string | null | undefined,
193
- op: ShadowKeyframeOp,
194
- serverScript: string | null | undefined,
195
- ): Promise<void> {
196
- if (!STUDIO_SDK_SHADOW_ENABLED) return;
197
- // No server script to diff against → skip the (costly) openComposition.
198
- if (!serverScript || !beforeHtml) return;
199
- const beforeScript = extractGsapScript(beforeHtml);
200
- const mapped = keyframeOpToEditOp(op, beforeScript);
201
- if (mapped.op === null) {
202
- // Ambiguous / not-found percentage: don't dispatch the wrong keyframe.
203
- trackStudioEvent("sdk_shadow_dispatch", {
204
- op: "gsap_keyframe",
205
- dispatched: false,
206
- reason: mapped.reason,
207
- mismatchCount: 0,
208
- });
209
- return;
210
- }
211
- const editOp = mapped.op;
212
- try {
213
- const session = await openComposition(beforeHtml);
214
- const verdict = session.can(editOp);
215
- if (!verdict.ok) {
216
- trackStudioEvent("sdk_shadow_dispatch", {
217
- op: "gsap_keyframe",
218
- dispatched: false,
219
- reason: "cannot_dispatch",
220
- code: verdict.code,
221
- mismatchCount: 0,
222
- });
223
- return;
224
- }
225
- session.batch(() => session.dispatch(editOp));
226
- const sdkScript = extractGsapScript(session.serialize());
227
- if (sdkScript == null) {
228
- trackStudioEvent("sdk_shadow_dispatch", {
229
- op: "gsap_keyframe",
230
- dispatched: false,
231
- reason: "no_sdk_script",
232
- mismatchCount: 0,
233
- });
234
- return;
235
- }
236
- const mismatches = gsapKeyframeFidelityMismatches(
237
- sdkScript,
238
- serverScript,
239
- op.animationId,
240
- makeSelectorResolver(beforeHtml),
241
- );
242
- trackStudioEvent("sdk_shadow_dispatch", {
243
- op: "gsap_keyframe",
244
- dispatched: true,
245
- mismatchCount: mismatches.length,
246
- mismatches: JSON.stringify(mismatches),
247
- });
248
- } catch (err) {
249
- trackStudioEvent("sdk_shadow_dispatch", {
250
- op: "gsap_keyframe",
251
- dispatched: false,
252
- reason: "fidelity_error",
253
- error: String(err),
254
- mismatchCount: 0,
255
- });
256
- }
257
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * Relative-epsilon numeric equality shared by the shadow diffs (timing parity +
3
- * GSAP value fidelity). Both writers round-trip durations/positions through JS
4
- * number formatting, so a value like 3.1 can read back as 3.0999999999999996.
5
- * Treat values within 1e-6 * max(1, |a|, |b|) as equal — tight enough that a
6
- * real 2 vs 1 (or 0.5 vs 0.49) still flags, loose enough to absorb float noise.
7
- */
8
- export function relEqual(a: number, b: number): boolean {
9
- if (a === b) return true;
10
- return Math.abs(a - b) <= 1e-6 * Math.max(1, Math.abs(a), Math.abs(b));
11
- }