@hyperframes/studio 0.7.58 → 0.7.60
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.
- package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.ts +2 -2
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +4 -1
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Browser-safe SHA-256 version matching studio-server's strong ETag format. */
|
|
2
|
+
export async function studioFileContentVersion(content: string): Promise<string> {
|
|
3
|
+
const bytes = new TextEncoder().encode(content);
|
|
4
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
5
|
+
const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
6
|
+
"",
|
|
7
|
+
);
|
|
8
|
+
return `"sha256:${hex}"`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Prefer an explicit content precondition, then the version observed during the read. */
|
|
12
|
+
export async function studioExpectedFileVersion(
|
|
13
|
+
versions: ReadonlyMap<string, string | null>,
|
|
14
|
+
path: string,
|
|
15
|
+
expectedContent?: string,
|
|
16
|
+
): Promise<string | null | undefined> {
|
|
17
|
+
if (expectedContent !== undefined) return studioFileContentVersion(expectedContent);
|
|
18
|
+
return versions.get(path);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createStudioWriteToken(): string {
|
|
22
|
+
return globalThis.crypto.randomUUID();
|
|
23
|
+
}
|
|
@@ -48,6 +48,51 @@ describe("findMatchingTimelineElementId", () => {
|
|
|
48
48
|
it("returns null for an unmatched element in index.html", () => {
|
|
49
49
|
expect(findMatchingTimelineElementId({ id: "ghost", sourceFile: "index.html" }, [])).toBe(null);
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
it("resolves the correct repeated composition host by domId, not the first host sharing the same compositionSrc", () => {
|
|
53
|
+
// Two hosts import the SAME sub-composition — only compositionSrc alone
|
|
54
|
+
// can't tell them apart, but each still has its own domId (as any
|
|
55
|
+
// element does). Selecting the SECOND host must not collapse to the
|
|
56
|
+
// first one just because an earlier, unrelated element also happens to
|
|
57
|
+
// share its compositionSrc.
|
|
58
|
+
const els = [
|
|
59
|
+
el({ id: "host-a", domId: "host-a", sourceFile: "index.html", compositionSrc: "scene.html" }),
|
|
60
|
+
el({ id: "host-b", domId: "host-b", sourceFile: "index.html", compositionSrc: "scene.html" }),
|
|
61
|
+
];
|
|
62
|
+
expect(
|
|
63
|
+
findMatchingTimelineElementId(
|
|
64
|
+
{
|
|
65
|
+
id: "host-b",
|
|
66
|
+
sourceFile: "index.html",
|
|
67
|
+
compositionSrc: "scene.html",
|
|
68
|
+
isCompositionHost: true,
|
|
69
|
+
},
|
|
70
|
+
els,
|
|
71
|
+
),
|
|
72
|
+
).toBe("host-b");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("falls back to compositionSrc-only matching when the host selection has neither a domId nor a selector", () => {
|
|
76
|
+
const els = [
|
|
77
|
+
el({
|
|
78
|
+
id: "host-only",
|
|
79
|
+
domId: undefined,
|
|
80
|
+
sourceFile: "index.html",
|
|
81
|
+
compositionSrc: "scene.html",
|
|
82
|
+
}),
|
|
83
|
+
];
|
|
84
|
+
expect(
|
|
85
|
+
findMatchingTimelineElementId(
|
|
86
|
+
{
|
|
87
|
+
id: undefined,
|
|
88
|
+
sourceFile: "index.html",
|
|
89
|
+
compositionSrc: "scene.html",
|
|
90
|
+
isCompositionHost: true,
|
|
91
|
+
},
|
|
92
|
+
els,
|
|
93
|
+
),
|
|
94
|
+
).toBe("host-only");
|
|
95
|
+
});
|
|
51
96
|
});
|
|
52
97
|
|
|
53
98
|
describe("findTimelineIdByAncestor", () => {
|
|
@@ -161,25 +161,34 @@ function matchesBySelector(selection: ElementMatchSelection, element: TimelineEl
|
|
|
161
161
|
);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
function elementMatchesSelection(
|
|
165
|
-
selection: ElementMatchSelection,
|
|
166
|
-
element: TimelineElement,
|
|
167
|
-
selectionSourceFile: string,
|
|
168
|
-
): boolean {
|
|
169
|
-
return (
|
|
170
|
-
matchesByDomId(selection, element, selectionSourceFile) ||
|
|
171
|
-
matchesByCompositionHost(selection, element) ||
|
|
172
|
-
matchesBySelector(selection, element)
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
164
|
export function findMatchingTimelineElementId(
|
|
177
165
|
selection: ElementMatchSelection,
|
|
178
166
|
elements: TimelineElement[],
|
|
179
167
|
): string | null {
|
|
180
168
|
const selectionSourceFile = selection.sourceFile || "index.html";
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
// Priority matters, not just "any of the three": a composition-host
|
|
170
|
+
// selection always carries its OWN id/selector too (computed generically
|
|
171
|
+
// for any element), so two repeated hosts sharing the same compositionSrc
|
|
172
|
+
// are still individually addressable by id/selector. Checking
|
|
173
|
+
// matchesByCompositionHost with equal priority in a single OR-per-element
|
|
174
|
+
// scan let `.find()` stop at an EARLIER, unrelated host that merely shares
|
|
175
|
+
// the compositionSrc, before the scan ever reached the correct id/selector
|
|
176
|
+
// match further down the list — collapsing every repeated host to the
|
|
177
|
+
// first one. Try id, then selector, across the WHOLE list first; only fall
|
|
178
|
+
// back to the coarser compositionSrc-only match when neither identifies a
|
|
179
|
+
// specific element.
|
|
180
|
+
const byId = selection.id
|
|
181
|
+
? elements.find((el) => matchesByDomId(selection, el, selectionSourceFile))
|
|
182
|
+
: undefined;
|
|
183
|
+
if (byId) return byId.key ?? byId.id;
|
|
184
|
+
|
|
185
|
+
const bySelector = selection.selector
|
|
186
|
+
? elements.find((el) => matchesBySelector(selection, el))
|
|
187
|
+
: undefined;
|
|
188
|
+
if (bySelector) return bySelector.key ?? bySelector.id;
|
|
189
|
+
|
|
190
|
+
const byHost = elements.find((el) => matchesByCompositionHost(selection, el));
|
|
191
|
+
if (byHost) return byHost.key ?? byHost.id;
|
|
183
192
|
|
|
184
193
|
// Child inside a sub-composition: return a qualified ID so the expansion
|
|
185
194
|
// hook can resolve the child via clipParentMap even though no timeline
|
|
@@ -237,6 +246,28 @@ export function resolveTimelineIdForSelection(
|
|
|
237
246
|
);
|
|
238
247
|
}
|
|
239
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Resolve every multi-selected element to its scope-qualified timeline key
|
|
251
|
+
* (dropping unresolvable ones). Selections carry bare DOM ids/selectors, but
|
|
252
|
+
* the visibility toggle matches keys like "index.html#hero" — and callers must
|
|
253
|
+
* hide all keys in ONE call so the file is patched in a single atomic write
|
|
254
|
+
* (per-element calls would clobber each other's reads).
|
|
255
|
+
*/
|
|
256
|
+
export function timelineKeysForSelections(
|
|
257
|
+
selections: readonly DomEditSelection[],
|
|
258
|
+
elements: TimelineElement[],
|
|
259
|
+
activeCompPath: string | null,
|
|
260
|
+
): string[] {
|
|
261
|
+
return selections
|
|
262
|
+
.map((selection) => resolveTimelineIdForSelection(selection, elements, activeCompPath))
|
|
263
|
+
.filter((key): key is string => key !== null);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export type ToggleHiddenHandler = (
|
|
267
|
+
elementKey: string | readonly string[],
|
|
268
|
+
hidden: boolean,
|
|
269
|
+
) => Promise<void> | void;
|
|
270
|
+
|
|
240
271
|
export function resolveTimelineSelectionSeekTime(
|
|
241
272
|
currentTime: number,
|
|
242
273
|
element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
StudioFileConflictError,
|
|
3
4
|
StudioSaveHttpError,
|
|
4
5
|
StudioSaveNetworkError,
|
|
5
6
|
buildStudioSaveFailureProperties,
|
|
@@ -8,6 +9,23 @@ import {
|
|
|
8
9
|
} from "./studioSaveDiagnostics";
|
|
9
10
|
|
|
10
11
|
describe("studio save diagnostics", () => {
|
|
12
|
+
it("preserves conflict versions and both sides for explicit recovery UI", () => {
|
|
13
|
+
const error = new StudioFileConflictError({
|
|
14
|
+
filePath: "index.html",
|
|
15
|
+
currentVersion: '"sha256:new"',
|
|
16
|
+
currentContent: "external",
|
|
17
|
+
attemptedContent: "local",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(getStudioSaveStatusCode(error)).toBe(409);
|
|
21
|
+
expect(error).toMatchObject({
|
|
22
|
+
filePath: "index.html",
|
|
23
|
+
currentVersion: '"sha256:new"',
|
|
24
|
+
currentContent: "external",
|
|
25
|
+
attemptedContent: "local",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
11
29
|
it("builds save_failure properties with stable diagnostics", () => {
|
|
12
30
|
const error = new StudioSaveHttpError("Failed to save index.html (503)", 503);
|
|
13
31
|
|
|
@@ -35,6 +35,27 @@ export class StudioSaveNetworkError extends Error {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export class StudioFileConflictError extends StudioSaveHttpError {
|
|
39
|
+
readonly filePath: string;
|
|
40
|
+
readonly currentVersion: string | null;
|
|
41
|
+
readonly currentContent: string | null;
|
|
42
|
+
readonly attemptedContent: string;
|
|
43
|
+
|
|
44
|
+
constructor(input: {
|
|
45
|
+
filePath: string;
|
|
46
|
+
currentVersion: string | null;
|
|
47
|
+
currentContent: string | null;
|
|
48
|
+
attemptedContent: string;
|
|
49
|
+
}) {
|
|
50
|
+
super(`Save conflict: ${input.filePath} changed outside this Studio session`, 409);
|
|
51
|
+
this.name = "StudioFileConflictError";
|
|
52
|
+
this.filePath = input.filePath;
|
|
53
|
+
this.currentVersion = input.currentVersion;
|
|
54
|
+
this.currentContent = input.currentContent;
|
|
55
|
+
this.attemptedContent = input.attemptedContent;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
function readNumericProperty(value: object, key: string): number | undefined {
|
|
39
60
|
const record = value as Record<string, unknown>;
|
|
40
61
|
const property = record[key];
|
|
@@ -71,6 +92,23 @@ export function getStudioSaveErrorMessage(error: unknown): string {
|
|
|
71
92
|
return "Unknown save failure";
|
|
72
93
|
}
|
|
73
94
|
|
|
95
|
+
export function markStudioSaveErrorAlreadyToasted<T>(error: T): T {
|
|
96
|
+
if (!error || typeof error !== "object") return error;
|
|
97
|
+
try {
|
|
98
|
+
Object.defineProperty(error, "alreadyToasted", { value: true, configurable: true });
|
|
99
|
+
} catch {
|
|
100
|
+
// Best effort: failure reporting must not replace the original save error.
|
|
101
|
+
}
|
|
102
|
+
return error;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isStudioSaveErrorAlreadyToasted(error: unknown): boolean {
|
|
106
|
+
if (!error || typeof error !== "object") return false;
|
|
107
|
+
if ((error as { alreadyToasted?: unknown }).alreadyToasted === true) return true;
|
|
108
|
+
const cause = (error as { cause?: unknown }).cause;
|
|
109
|
+
return cause !== error && isStudioSaveErrorAlreadyToasted(cause);
|
|
110
|
+
}
|
|
111
|
+
|
|
74
112
|
export function getStudioSaveStatusCode(error: unknown): number | undefined {
|
|
75
113
|
if (!error || typeof error !== "object") return undefined;
|
|
76
114
|
const direct =
|