hyperframes 0.8.45 → 0.8.47

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,4 +1,4 @@
1
- import{n as Qi}from"./index-C_TAjvrq.js";/*!
1
+ import{n as Qi}from"./index-j-4hCrZU.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
@@ -4,6 +4,7 @@ import { ReactNode, RefObject } from 'react';
4
4
  import { RuntimeTimelineClipIdentity } from '@hyperframes/core';
5
5
  import { GsapAnimation, PropertyGroupName } from '@hyperframes/core/gsap-parser';
6
6
  import * as zustand from 'zustand';
7
+ import { StoreApi } from 'zustand';
7
8
  import { MusicBeatAnalysis } from '@hyperframes/core/beats';
8
9
  import { GsapAnimation as GsapAnimation$1, ArcPathSegment } from '@hyperframes/parsers/gsap-parser';
9
10
 
@@ -250,6 +251,20 @@ interface ThumbnailSlice {
250
251
  bumpThumbnailContentRevision: () => void;
251
252
  }
252
253
 
254
+ /** Gates `timelineReady` on the composition's declared readiness inputs
255
+ * (media, for now) instead of just a known duration. The generation counter
256
+ * is module-scope, not store state: it guards an in-flight settlement, not
257
+ * something a component reads, so bumping it shouldn't trigger a render. */
258
+
259
+ interface PlaybackReadinessSlice {
260
+ timelineReady: boolean;
261
+ setTimelineReady: (ready: boolean) => void;
262
+ /** Sets timelineReady once doc's readiness inputs settle, or immediately
263
+ * if doc is null. A wait a later call supersedes never wins the race. */
264
+ requestTimelineReady: (doc: Document | null) => void;
265
+ }
266
+ declare function createPlaybackReadinessSlice(set: StoreApi<PlaybackReadinessSlice>["setState"]): PlaybackReadinessSlice;
267
+
253
268
  type TimeListener = (time: number) => void;
254
269
  declare const liveTime: {
255
270
  notify: (time: number) => void;
@@ -382,11 +397,11 @@ type TimelineTool = "select" | "razor";
382
397
  interface SelectElementOptions {
383
398
  preserveSet?: boolean;
384
399
  }
385
- interface PlayerState extends KeyframeSlice, AutomationSelectionSlice, ThumbnailSlice, EditingModeSlice {
400
+ type PlayerStoreSlices = KeyframeSlice & AutomationSelectionSlice & ThumbnailSlice & EditingModeSlice & ReturnType<typeof createPlaybackReadinessSlice>;
401
+ interface PlayerState extends PlayerStoreSlices {
386
402
  isPlaying: boolean;
387
403
  currentTime: number;
388
404
  duration: number;
389
- timelineReady: boolean;
390
405
  /** Increments exactly once when the Studio switches to a different project. */
391
406
  timelineSessionEpoch: number;
392
407
  /** Project owning the current timeline session; null outside a project-scoped reset. */
@@ -452,7 +467,6 @@ interface PlayerState extends KeyframeSlice, AutomationSelectionSlice, Thumbnail
452
467
  setAudioMuted: (muted: boolean) => void;
453
468
  setAudioVolume: (volume: number) => void;
454
469
  setLoopEnabled: (enabled: boolean) => void;
455
- setTimelineReady: (ready: boolean) => void;
456
470
  setBeatDragging: (dragging: boolean) => void;
457
471
  setElements: (elements: TimelineElement[]) => void;
458
472
  setSelectedElementId: (id: string | null, options?: SelectElementOptions) => void;
@@ -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-C_TAjvrq.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-j-4hCrZU.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-DnRfAiK2.css">
10
10
  </head>
11
11
  <body>
@@ -1594,6 +1594,30 @@ function createThumbnailSlice(set) {
1594
1594
  };
1595
1595
  }
1596
1596
 
1597
+ // src/player/store/readinessSlice.ts
1598
+ import { settleCompositionReadiness } from "@hyperframes/core/composition-readiness";
1599
+ var timelineReadyGeneration = 0;
1600
+ function resetPlaybackReadinessState() {
1601
+ timelineReadyGeneration++;
1602
+ return { timelineReady: false };
1603
+ }
1604
+ function createPlaybackReadinessSlice(set) {
1605
+ return {
1606
+ timelineReady: false,
1607
+ setTimelineReady: (ready) => {
1608
+ timelineReadyGeneration++;
1609
+ set({ timelineReady: ready });
1610
+ },
1611
+ requestTimelineReady: (doc) => {
1612
+ const generation = ++timelineReadyGeneration;
1613
+ if (!doc) return set({ timelineReady: true });
1614
+ settleCompositionReadiness(doc, () => {
1615
+ if (generation === timelineReadyGeneration) set({ timelineReady: true });
1616
+ });
1617
+ }
1618
+ };
1619
+ }
1620
+
1597
1621
  // src/player/store/liveTime.ts
1598
1622
  var timeListeners = /* @__PURE__ */ new Set();
1599
1623
  var liveTime = {
@@ -1623,7 +1647,7 @@ function createTimelineResetState() {
1623
1647
  isPlaying: false,
1624
1648
  currentTime: 0,
1625
1649
  duration: 0,
1626
- timelineReady: false,
1650
+ ...resetPlaybackReadinessState(),
1627
1651
  beatDragging: false,
1628
1652
  elements: [],
1629
1653
  selectedElementId: null,
@@ -1666,7 +1690,6 @@ var usePlayerStore = create2((set, get) => ({
1666
1690
  isPlaying: false,
1667
1691
  currentTime: 0,
1668
1692
  duration: 0,
1669
- timelineReady: false,
1670
1693
  timelineSessionEpoch: 0,
1671
1694
  timelineProjectId: null,
1672
1695
  beatDragging: false,
@@ -1692,6 +1715,7 @@ var usePlayerStore = create2((set, get) => ({
1692
1715
  ...createThumbnailSlice(set),
1693
1716
  ...createAutomationSelectionSlice(set),
1694
1717
  ...createEditingModeSlice(set),
1718
+ ...createPlaybackReadinessSlice(set),
1695
1719
  activeKeyframePct: null,
1696
1720
  setActiveKeyframePct: (pct) => set({ activeKeyframePct: pct }),
1697
1721
  selectedElementIds: /* @__PURE__ */ new Set(),
@@ -1856,7 +1880,6 @@ var usePlayerStore = create2((set, get) => ({
1856
1880
  bumpZEditVersion: () => set((state) => ({ zEditVersion: state.zEditVersion + 1 })),
1857
1881
  setCurrentTime: (time) => set({ currentTime: Number.isFinite(time) ? time : 0 }),
1858
1882
  setDuration: (duration) => set({ duration: Number.isFinite(duration) ? duration : 0 }),
1859
- setTimelineReady: (ready) => set({ timelineReady: ready }),
1860
1883
  setBeatDragging: (dragging) => set({ beatDragging: dragging }),
1861
1884
  setElements: (elements) => set({ elements }),
1862
1885
  // A genuine single selection: always collapse the set to just this element. User
@@ -25080,7 +25103,7 @@ function useTimelineSyncCallbacks({
25080
25103
  syncTimelineElements,
25081
25104
  setDuration,
25082
25105
  setCurrentTime,
25083
- setTimelineReady,
25106
+ requestTimelineReady,
25084
25107
  setIsPlaying,
25085
25108
  attachIframeShortcutListeners,
25086
25109
  applyPreviewAudioState
@@ -25152,7 +25175,7 @@ function useTimelineSyncCallbacks({
25152
25175
  syncAdapterDuration(adapter, setDuration);
25153
25176
  setCurrentTime(startTime);
25154
25177
  if (!isRefreshingRef.current) {
25155
- setTimelineReady(true);
25178
+ requestTimelineReady(safeContentDocument(iframeRef.current));
25156
25179
  }
25157
25180
  isRefreshingRef.current = false;
25158
25181
  setIsPlaying(false);
@@ -25170,7 +25193,7 @@ function useTimelineSyncCallbacks({
25170
25193
  getAdapter,
25171
25194
  setDuration,
25172
25195
  setCurrentTime,
25173
- setTimelineReady,
25196
+ requestTimelineReady,
25174
25197
  setIsPlaying,
25175
25198
  processTimelineMessage,
25176
25199
  enrichMissingCompositions,
@@ -25641,7 +25664,7 @@ function useTimelinePlayer() {
25641
25664
  const lastTimelineMessageRef = useRef44(0);
25642
25665
  const staticSeekAdapterRef = useRef44(null);
25643
25666
  const staticSeekWarnedRef = useRef44(false);
25644
- const { setIsPlaying, setCurrentTime, setDuration, setTimelineReady, setElements } = usePlayerStore.getState();
25667
+ const { setIsPlaying, setCurrentTime, setDuration, requestTimelineReady, setElements } = usePlayerStore.getState();
25645
25668
  const syncTimelineElements = useCallback36(
25646
25669
  // The lease guard adds one deliberate branch at the shared synchronization boundary.
25647
25670
  // fallow-ignore-next-line complexity
@@ -25667,7 +25690,7 @@ function useTimelinePlayer() {
25667
25690
  setDuration(nextDuration ?? 0);
25668
25691
  }
25669
25692
  if (!state.timelineReady) {
25670
- setTimelineReady(true);
25693
+ requestTimelineReady(safeContentDocument(iframeRef.current));
25671
25694
  }
25672
25695
  void probeMissingSourceDurations(
25673
25696
  mergedElements,
@@ -25683,7 +25706,7 @@ function useTimelinePlayer() {
25683
25706
  }
25684
25707
  );
25685
25708
  },
25686
- [setElements, setTimelineReady, setDuration]
25709
+ [setElements, requestTimelineReady, setDuration]
25687
25710
  );
25688
25711
  const getAdapter = useCallback36(() => {
25689
25712
  try {
@@ -25953,7 +25976,7 @@ function useTimelinePlayer() {
25953
25976
  syncTimelineElements,
25954
25977
  setDuration,
25955
25978
  setCurrentTime,
25956
- setTimelineReady,
25979
+ requestTimelineReady,
25957
25980
  setIsPlaying,
25958
25981
  attachIframeShortcutListeners,
25959
25982
  applyPreviewAudioState
@@ -38233,7 +38256,7 @@ var STUDIO_SDK_OPERATION_FAMILIES = [
38233
38256
  function resolveEnabledSdkFamilies(env2, masterEnabled) {
38234
38257
  if (!masterEnabled) return /* @__PURE__ */ new Set();
38235
38258
  const raw = env2["VITE_STUDIO_SDK_CUTOVER_FAMILIES"];
38236
- if (typeof raw !== "string" || raw.trim() === "") return /* @__PURE__ */ new Set();
38259
+ if (typeof raw !== "string" || raw.trim() === "") return new Set(STUDIO_SDK_OPERATION_FAMILIES);
38237
38260
  const requested = raw.split(",").map((family) => family.trim()).filter(Boolean);
38238
38261
  const known = new Set(STUDIO_SDK_OPERATION_FAMILIES);
38239
38262
  const unknown = requested.filter((family) => !known.has(family));
@@ -38266,7 +38289,7 @@ var env = { ...import.meta.env ?? {}, ...runtimeEnv };
38266
38289
  var STUDIO_SDK_CUTOVER_ENABLED = resolveStudioBooleanEnvFlag(
38267
38290
  env,
38268
38291
  ["VITE_STUDIO_SDK_CUTOVER_ENABLED"],
38269
- false
38292
+ true
38270
38293
  );
38271
38294
  var STUDIO_SDK_CUTOVER_FAMILIES = resolveEnabledSdkFamilies(
38272
38295
  env,
@@ -38759,6 +38782,13 @@ function shouldDeclineTextCutoverForTarget(target, ops) {
38759
38782
  const tag = elementTag(children[0]);
38760
38783
  return tag !== null && NON_HTML_CHILD_TAGS.has(tag);
38761
38784
  }
38785
+ function isResolverDisagreement(session, hfId) {
38786
+ try {
38787
+ return resolveSnapshot(session, hfId) !== null;
38788
+ } catch {
38789
+ return false;
38790
+ }
38791
+ }
38762
38792
  function shouldUseSdkCutover(flagEnabled, hasSession, hfId, ops) {
38763
38793
  return flagEnabled && hasSession && !!hfId && ops.length > 0 && ops.every((o) => CUTOVER_OP_TYPES.has(o.type)) && // SDK edit ops target only the element hfId; child-scoped patch ops need the server path.
38764
38794
  !hasChildScopedOp(ops) && !ops.some(mapsToReservedAttr) && !hasUnsafeHtmlAttributeOp(ops);
@@ -38800,7 +38830,12 @@ function isSelfWriteEcho(path, content, now2 = Date.now()) {
38800
38830
  }
38801
38831
 
38802
38832
  // src/utils/sdkEditTransaction.ts
38803
- function declinedCutover(reason) {
38833
+ function declinedCutover(reason, family, resolverDisagreement) {
38834
+ trackStudioEvent("sdk_cutover_declined", {
38835
+ reason,
38836
+ family: family ?? null,
38837
+ ...resolverDisagreement ? { resolverDisagreement: true } : {}
38838
+ });
38804
38839
  return { status: "declined", reason };
38805
38840
  }
38806
38841
  function asCutoverError(error) {
@@ -38967,15 +39002,17 @@ function gsapReadSource(deps, targetPath) {
38967
39002
  }
38968
39003
  async function sdkCutoverPersist(selection, ops, originalContent, targetPath, sdkSession, deps, options) {
38969
39004
  if (!shouldUseSdkCutover(sdkFamilyEnabled("dom"), !!sdkSession, selection.hfId, ops))
38970
- return declinedCutover("ineligible_operation");
38971
- if (!sdkSession) return declinedCutover("session_unavailable");
39005
+ return declinedCutover("ineligible_operation", "dom");
39006
+ if (!sdkSession) return declinedCutover("session_unavailable", "dom");
38972
39007
  const hfId = selection.hfId;
38973
- if (!hfId) return declinedCutover("target_unaddressable");
39008
+ if (!hfId) return declinedCutover("target_unaddressable", "dom");
38974
39009
  const target = sdkSession.getElement(hfId);
38975
- if (!target) return declinedCutover("target_not_found");
39010
+ if (!target)
39011
+ return declinedCutover("target_not_found", "dom", isResolverDisagreement(sdkSession, hfId));
38976
39012
  if (shouldDeclineTextCutoverForTarget(target, ops))
38977
- return declinedCutover("unsupported_text_target");
38978
- if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
39013
+ return declinedCutover("unsupported_text_target", "dom");
39014
+ if (wrongCompositionFile(deps, targetPath))
39015
+ return declinedCutover("wrong_composition_file", "dom");
38979
39016
  const result = await persistSdkCandidateMutation(
38980
39017
  sdkSession,
38981
39018
  targetPath,
@@ -38998,10 +39035,12 @@ async function sdkTimingPersist(hfId, targetPath, timingUpdate, sdkSession, deps
38998
39035
  timingSrc ? () => timingSrc(targetPath) : void 0,
38999
39036
  { targetPath, compositionPath: deps.compositionPath }
39000
39037
  );
39001
- if (!sdkFamilyEnabled("timing")) return declinedCutover("feature_disabled");
39002
- if (!sdkSession) return declinedCutover("session_unavailable");
39003
- if (!sdkSession.getElement(hfId)) return declinedCutover("target_not_found");
39004
- if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
39038
+ if (!sdkFamilyEnabled("timing")) return declinedCutover("feature_disabled", "timing");
39039
+ if (!sdkSession) return declinedCutover("session_unavailable", "timing");
39040
+ if (!sdkSession.getElement(hfId))
39041
+ return declinedCutover("target_not_found", "timing", isResolverDisagreement(sdkSession, hfId));
39042
+ if (wrongCompositionFile(deps, targetPath))
39043
+ return declinedCutover("wrong_composition_file", "timing");
39005
39044
  try {
39006
39045
  const serializedBefore = sdkSession.serialize();
39007
39046
  const result = await persistSdkCandidateMutation(
@@ -39032,11 +39071,17 @@ async function sdkTimingBatchPersist(changes, targetPath, sdkSession, deps, opti
39032
39071
  { targetPath, compositionPath: deps.compositionPath }
39033
39072
  );
39034
39073
  }
39035
- if (!sdkFamilyEnabled("timing")) return declinedCutover("feature_disabled");
39036
- if (!sdkSession) return declinedCutover("session_unavailable");
39037
- if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
39038
- if (changes.some((change) => !sdkSession.getElement(change.hfId)))
39039
- return declinedCutover("target_not_found");
39074
+ if (!sdkFamilyEnabled("timing")) return declinedCutover("feature_disabled", "timing");
39075
+ if (!sdkSession) return declinedCutover("session_unavailable", "timing");
39076
+ if (wrongCompositionFile(deps, targetPath))
39077
+ return declinedCutover("wrong_composition_file", "timing");
39078
+ const unresolved = changes.find((change) => !sdkSession.getElement(change.hfId));
39079
+ if (unresolved)
39080
+ return declinedCutover(
39081
+ "target_not_found",
39082
+ "timing",
39083
+ isResolverDisagreement(sdkSession, unresolved.hfId)
39084
+ );
39040
39085
  try {
39041
39086
  const serializedBefore = sdkSession.serialize();
39042
39087
  const result = await persistSdkCandidateMutation(
@@ -39091,9 +39136,9 @@ function sdkGsapTweenPersist(targetPath, op, sdkSession, deps, options) {
39091
39136
  );
39092
39137
  }
39093
39138
  if (!sdkFamilyEnabled("gsap-animation"))
39094
- return Promise.resolve(declinedCutover("feature_disabled"));
39139
+ return Promise.resolve(declinedCutover("feature_disabled", "gsap-animation"));
39095
39140
  if (op.kind === "add" && sdkSession && !sdkSession.getElement(op.target))
39096
- return Promise.resolve(declinedCutover("target_not_found"));
39141
+ return Promise.resolve(declinedCutover("target_not_found", "gsap-animation"));
39097
39142
  return dispatchGsapOpAndPersist("gsap-animation", targetPath, sdkSession, deps, options, (s) => {
39098
39143
  s.batch(() => {
39099
39144
  if (op.kind === "add") {
@@ -39116,9 +39161,10 @@ async function dispatchGsapOpAndPersist(family, targetPath, sdkSession, deps, op
39116
39161
  { targetPath, compositionPath: deps.compositionPath }
39117
39162
  );
39118
39163
  }
39119
- if (!sdkFamilyEnabled(family)) return declinedCutover("feature_disabled");
39120
- if (!sdkSession) return declinedCutover("session_unavailable");
39121
- if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
39164
+ if (!sdkFamilyEnabled(family)) return declinedCutover("feature_disabled", family);
39165
+ if (!sdkSession) return declinedCutover("session_unavailable", family);
39166
+ if (wrongCompositionFile(deps, targetPath))
39167
+ return declinedCutover("wrong_composition_file", family);
39122
39168
  const session = sdkSession;
39123
39169
  try {
39124
39170
  const serializedBefore = session.serialize();
@@ -39271,10 +39317,16 @@ async function sdkDeletePersist(hfId, originalContent, targetPath, sdkSession, d
39271
39317
  () => Promise.resolve(originalContent),
39272
39318
  { targetPath, compositionPath: deps.compositionPath }
39273
39319
  );
39274
- if (!sdkFamilyEnabled("lifecycle")) return declinedCutover("feature_disabled");
39275
- if (!sdkSession) return declinedCutover("session_unavailable");
39276
- if (!sdkSession.getElement(hfId)) return declinedCutover("target_not_found");
39277
- if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
39320
+ if (!sdkFamilyEnabled("lifecycle")) return declinedCutover("feature_disabled", "lifecycle");
39321
+ if (!sdkSession) return declinedCutover("session_unavailable", "lifecycle");
39322
+ if (!sdkSession.getElement(hfId))
39323
+ return declinedCutover(
39324
+ "target_not_found",
39325
+ "lifecycle",
39326
+ isResolverDisagreement(sdkSession, hfId)
39327
+ );
39328
+ if (wrongCompositionFile(deps, targetPath))
39329
+ return declinedCutover("wrong_composition_file", "lifecycle");
39278
39330
  const result = await persistSdkCandidateMutation(
39279
39331
  sdkSession,
39280
39332
  targetPath,
@@ -74679,7 +74731,11 @@ function useSdkSession(projectId, activeCompPath) {
74679
74731
  generation
74680
74732
  };
74681
74733
  readProjectFileOptional(projectId, activeCompPath).then(async (content) => {
74682
- if (cancelled || typeof content !== "string") return;
74734
+ if (cancelled) return;
74735
+ if (typeof content !== "string") {
74736
+ trackStudioEvent("sdk_session_unavailable", { stage: "read" });
74737
+ return;
74738
+ }
74683
74739
  const comp = await openComposition3(content, { history: false });
74684
74740
  if (cancelled) {
74685
74741
  disposeSdkSession(comp);
@@ -74693,6 +74749,7 @@ function useSdkSession(projectId, activeCompPath) {
74693
74749
  reloadTokenRef.current
74694
74750
  )) {
74695
74751
  disposeSdkSession(comp);
74752
+ trackStudioEvent("sdk_session_unavailable", { stage: "ownership" });
74696
74753
  return;
74697
74754
  }
74698
74755
  const displaced = ownedSessionRef.current;
@@ -74701,8 +74758,14 @@ function useSdkSession(projectId, activeCompPath) {
74701
74758
  ownedSessionRef.current = installed;
74702
74759
  setOwnedSession(installed);
74703
74760
  if (displaced && displaced.session !== comp) disposeSdkSession(displaced.session);
74704
- }).catch(() => {
74705
- if (!cancelled && generationRef.current === generation) setOwnedSession(null);
74761
+ }).catch((error) => {
74762
+ if (!cancelled && generationRef.current === generation) {
74763
+ setOwnedSession(null);
74764
+ trackStudioEvent("sdk_session_unavailable", {
74765
+ stage: "open",
74766
+ error: error instanceof Error ? error.message : String(error)
74767
+ });
74768
+ }
74706
74769
  });
74707
74770
  return () => {
74708
74771
  cancelled = true;