@hyperframes/studio 0.7.106 → 0.7.108
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-CvVcx_CV.js → hyperframes-player-QqavTWRK.js} +1 -1
- package/dist/assets/{index-CRzCCuPH.js → index-BQjXA1pz.js} +1 -1
- package/dist/assets/{index-BbYg_isc.js → index-BhHHMprP.js} +1 -1
- package/dist/assets/index-DNkh9mbV.css +1 -0
- package/dist/assets/index-GqONLcOl.js +428 -0
- package/dist/{chunk-OBAG3GWK.js → chunk-AZYHQC6V.js} +6 -7
- package/dist/chunk-AZYHQC6V.js.map +1 -0
- package/dist/{domEditingLayers-AT7G6F4L.js → domEditingLayers-7AMZ7GFI.js} +4 -2
- package/dist/index.d.ts +86 -3
- package/dist/index.html +2 -2
- package/dist/index.js +24325 -17831
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/EditorShell.tsx +0 -2
- package/src/components/StudioErrorBoundary.test.tsx +97 -0
- package/src/components/StudioErrorBoundary.tsx +7 -0
- package/src/components/StudioOverlays.tsx +15 -13
- package/src/components/StudioRightPanel.tsx +18 -1
- package/src/components/TimelineToolbar.test.tsx +46 -0
- package/src/components/TimelineToolbar.tsx +17 -0
- package/src/components/editor/DomEditOverlay.tsx +36 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +85 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +29 -3
- package/src/components/editor/InlineTextToolbar.test.tsx +296 -0
- package/src/components/editor/InlineTextToolbar.tsx +281 -0
- package/src/components/editor/OffCanvasIndicators.tsx +38 -0
- package/src/components/editor/PropertyPanel.test.tsx +5 -1
- package/src/components/editor/PropertyPanel.tsx +7 -23
- package/src/components/editor/PropertyPanelFlat.tsx +25 -0
- package/src/components/editor/audioFxSummary.test.ts +75 -0
- package/src/components/editor/audioFxSummary.ts +36 -0
- package/src/components/editor/audioFxTelemetry.test.ts +129 -0
- package/src/components/editor/audioFxTelemetry.ts +230 -0
- package/src/components/editor/domEditInlineText.test.ts +86 -0
- package/src/components/editor/domEditInlineText.ts +99 -0
- package/src/components/editor/domEditInlineTextElement.test.ts +59 -0
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingLayers.ts +7 -6
- package/src/components/editor/inlineTextStyleRange.test.ts +743 -0
- package/src/components/editor/inlineTextStyleRange.ts +593 -0
- package/src/components/editor/inlineTextStyleRead.ts +47 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +1830 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +299 -0
- package/src/components/editor/propertyPanelAudioFxGroupUtils.ts +21 -0
- package/src/components/editor/propertyPanelAutomation.test.ts +60 -0
- package/src/components/editor/propertyPanelAutomation.ts +97 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +37 -9
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFxAddMenu.tsx +209 -0
- package/src/components/editor/propertyPanelFxBandRuler.tsx +72 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +382 -0
- package/src/components/editor/propertyPanelFxControls.test.tsx +115 -0
- package/src/components/editor/propertyPanelFxControls.tsx +356 -0
- package/src/components/editor/propertyPanelFxEqModule.tsx +196 -0
- package/src/components/editor/propertyPanelFxFamily.ts +83 -0
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +269 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +112 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +339 -0
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +159 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +215 -0
- package/src/components/editor/propertyPanelFxPresetStyle.test.ts +142 -0
- package/src/components/editor/propertyPanelFxPresetStyle.ts +216 -0
- package/src/components/editor/propertyPanelFxRackChain.tsx +202 -0
- package/src/components/editor/propertyPanelFxSection.test.tsx +1747 -0
- package/src/components/editor/propertyPanelFxSection.tsx +550 -0
- package/src/components/editor/propertyPanelFxSectionTypes.ts +91 -0
- package/src/components/editor/propertyPanelTypes.ts +5 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useFxAudition.ts +95 -0
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxChainObserved.ts +83 -0
- package/src/components/editor/useFxLevelling.ts +235 -0
- package/src/components/editor/useInlineTextEditing.tsx +119 -0
- package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
- package/src/components/editor/useVolumeAutomation.ts +54 -0
- package/src/components/feedback/CrashFeedbackPrompt.tsx +27 -0
- package/src/components/feedback/StudioFeedbackCard.tsx +373 -0
- package/src/components/feedback/feedbackTrigger.test.ts +168 -0
- package/src/components/feedback/feedbackTrigger.ts +269 -0
- package/src/components/feedback/projectProvenance.test.ts +120 -0
- package/src/components/feedback/projectProvenance.ts +83 -0
- package/src/components/renders/useRenderQueue.ts +62 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +2 -2
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +19 -2
- package/src/contexts/DomEditContext.tsx +15 -0
- package/src/hooks/domEditPersistFailure.ts +0 -7
- package/src/hooks/useAppHotkeys.test.ts +198 -0
- package/src/hooks/useAppHotkeys.ts +31 -4
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +429 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +371 -0
- package/src/hooks/useDomEditAttributeCommits.ts +90 -10
- package/src/hooks/useDomEditCommits.test.tsx +141 -21
- package/src/hooks/useDomEditCommits.ts +4 -0
- package/src/hooks/useDomEditSession.ts +4 -0
- package/src/hooks/useDomEditTextCommits.ts +112 -11
- package/src/hooks/useElementLifecycleOps.ts +3 -0
- package/src/hooks/useFileTree.ts +8 -3
- package/src/hooks/useInlineTextEdit.test.tsx +555 -0
- package/src/hooks/useInlineTextEdit.ts +320 -0
- package/src/hooks/useLivePlayheadTime.ts +49 -0
- package/src/hooks/usePreviewPersistence.ts +7 -0
- package/src/hooks/useTimelineEditing.ts +3 -0
- package/src/player/components/AutomationSelectionMenu.tsx +76 -0
- package/src/player/components/AutomationValueInput.tsx +51 -0
- package/src/player/components/LayerDisclosureRow.tsx +5 -4
- package/src/player/components/Timeline.test.ts +84 -0
- package/src/player/components/TimelineAutomationLane.test.tsx +1720 -0
- package/src/player/components/TimelineAutomationLane.tsx +674 -0
- package/src/player/components/TimelineAutomationLaneSlot.test.tsx +193 -0
- package/src/player/components/TimelineLanes.test.tsx +24 -5
- package/src/player/components/TimelineLanes.tsx +142 -154
- package/src/player/components/TimelineTrackHeader.test.tsx +212 -1
- package/src/player/components/TimelineTrackHeader.tsx +166 -6
- package/src/player/components/TimelineTrackRow.tsx +8 -1
- package/src/player/components/automationClipboard.test.ts +94 -0
- package/src/player/components/automationClipboard.ts +136 -0
- package/src/player/components/automationGestureKeys.test.ts +49 -0
- package/src/player/components/automationGestureKeys.ts +49 -0
- package/src/player/components/automationLaneData.test.ts +270 -0
- package/src/player/components/automationLaneData.ts +258 -0
- package/src/player/components/automationLaneDragMath.ts +194 -0
- package/src/player/components/automationLaneGeometry.test.ts +239 -0
- package/src/player/components/automationLaneGeometry.ts +287 -0
- package/src/player/components/automationLaneHeight.ts +15 -0
- package/src/player/components/automationLanePointer.ts +20 -0
- package/src/player/components/automationLaneSelection.test.ts +183 -0
- package/src/player/components/automationLaneSelection.ts +135 -0
- package/src/player/components/automationShapes.test.ts +82 -0
- package/src/player/components/automationShapes.ts +70 -0
- package/src/player/components/automationSimplify.test.ts +48 -0
- package/src/player/components/automationSimplify.ts +51 -0
- package/src/player/components/timelineClipGestureHandlers.ts +223 -0
- package/src/player/components/timelineLaneProps.ts +19 -0
- package/src/player/components/timelineLayout.ts +11 -5
- package/src/player/components/useAutomationEdgeStretch.test.ts +27 -0
- package/src/player/components/useAutomationEdgeStretch.ts +269 -0
- package/src/player/components/useAutomationLaneGestures.ts +537 -0
- package/src/player/components/useAutomationLanes.test.tsx +112 -0
- package/src/player/components/useAutomationLanes.ts +170 -0
- package/src/player/components/useAutomationRangeDrag.ts +114 -0
- package/src/player/components/useTimelineTrackLayout.test.ts +107 -1
- package/src/player/components/useTimelineTrackLayout.ts +83 -7
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
- package/src/player/hooks/useTimelinePlayer.ts +13 -1
- package/src/player/lib/automationStoreSync.test.ts +83 -0
- package/src/player/lib/automationStoreSync.ts +56 -0
- package/src/player/lib/playbackShortcuts.ts +5 -4
- package/src/player/lib/timelineDOM.test.ts +33 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/store/automationSelectionSlice.test.ts +30 -0
- package/src/player/store/automationSelectionSlice.ts +54 -0
- package/src/player/store/playerStore.test.ts +15 -0
- package/src/player/store/playerStore.ts +54 -65
- package/src/player/store/timelineElement.ts +78 -0
- package/src/styles/studio.css +89 -0
- package/src/telemetry/agentRuntime.test.ts +45 -0
- package/src/telemetry/agentRuntime.ts +62 -0
- package/src/telemetry/breadcrumbs.test.ts +68 -0
- package/src/telemetry/breadcrumbs.ts +70 -0
- package/src/telemetry/client.ts +5 -0
- package/src/telemetry/events.ts +84 -3
- package/src/telemetry/system.ts +8 -0
- package/src/utils/sourcePatcher.ts +5 -1
- package/src/utils/studioFileHistory.test.ts +49 -0
- package/src/utils/studioFileHistory.ts +17 -1
- package/src/utils/studioHelpers.ts +2 -5
- package/src/utils/studioTelemetry.ts +16 -10
- package/src/utils/timelineDiscovery.ts +3 -24
- package/src/utils/typingTarget.test.ts +54 -0
- package/src/utils/typingTarget.ts +43 -0
- package/dist/assets/index-DerI0ikN.js +0 -428
- package/dist/assets/index-tBPidglp.css +0 -1
- package/dist/chunk-OBAG3GWK.js.map +0 -1
- package/src/components/StudioFeedbackBar.tsx +0 -217
- /package/dist/{domEditingLayers-AT7G6F4L.js.map → domEditingLayers-7AMZ7GFI.js.map} +0 -0
|
@@ -117,14 +117,6 @@ function stubPatchFetch(
|
|
|
117
117
|
return fetchMock;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function stubUnexpectedPersistFetch() {
|
|
121
|
-
const fetchMock = vi.fn(async (): Promise<Response> => {
|
|
122
|
-
throw new Error("persist should not run");
|
|
123
|
-
});
|
|
124
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
125
|
-
return fetchMock;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
120
|
async function flushAsyncWork(): Promise<void> {
|
|
129
121
|
for (let i = 0; i < 8; i += 1) {
|
|
130
122
|
await Promise.resolve();
|
|
@@ -812,14 +804,26 @@ function renderStyleCommitWithFetch(fetchHandler: FetchHandler) {
|
|
|
812
804
|
};
|
|
813
805
|
}
|
|
814
806
|
|
|
815
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Adding or removing a text layer, which no per-child operation can express.
|
|
809
|
+
*
|
|
810
|
+
* Both used to be refused outright — the panel offered the buttons and neither
|
|
811
|
+
* could ever save — so this asserts the opposite of what it used to: one
|
|
812
|
+
* `rich-text` operation carrying the element's new markup, and no complaint.
|
|
813
|
+
*/
|
|
814
|
+
async function expectPersistedTextStructureEdit(
|
|
816
815
|
commit: (hook: ReturnType<typeof useDomEditCommits>) => Promise<unknown>,
|
|
816
|
+
expectedMarkup: (markup: string) => void,
|
|
817
817
|
): Promise<void> {
|
|
818
|
-
const fetchMock =
|
|
818
|
+
const fetchMock = stubPatchFetch({
|
|
819
|
+
ok: true,
|
|
820
|
+
changed: true,
|
|
821
|
+
matched: true,
|
|
822
|
+
content: '<div data-hf-id="hf-card"><span>First</span></div>',
|
|
823
|
+
});
|
|
819
824
|
const { iframe, element } = createPreviewElement(
|
|
820
825
|
'<div data-hf-id="hf-card"><span>First</span><span>Second</span></div>',
|
|
821
826
|
);
|
|
822
|
-
const originalInnerHtml = element.innerHTML;
|
|
823
827
|
const selection = createSelection(element, {
|
|
824
828
|
textFields: [
|
|
825
829
|
textField({ key: "first", value: "First", source: "child" }),
|
|
@@ -833,18 +837,82 @@ async function expectRejectedTextStructureEdit(
|
|
|
833
837
|
await commit(rendered.hook);
|
|
834
838
|
});
|
|
835
839
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
expect.stringContaining("text structure change"),
|
|
839
|
-
"error",
|
|
840
|
+
const patchPost = fetchMock.mock.calls.find((call) =>
|
|
841
|
+
requestUrl(call[0]).includes("/file-mutations/patch-element/"),
|
|
840
842
|
);
|
|
841
|
-
expect(
|
|
842
|
-
|
|
843
|
+
expect(patchPost).toBeDefined();
|
|
844
|
+
const body = JSON.parse(String(patchPost?.[1]?.body)) as {
|
|
845
|
+
operations: Array<{ type: string; value?: string }>;
|
|
846
|
+
};
|
|
847
|
+
expect(body.operations).toHaveLength(1);
|
|
848
|
+
expect(body.operations[0]?.type).toBe("rich-text");
|
|
849
|
+
expectedMarkup(body.operations[0]?.value ?? "");
|
|
850
|
+
expect(rendered.showToast).not.toHaveBeenCalled();
|
|
843
851
|
} finally {
|
|
844
852
|
rendered.cleanup();
|
|
845
853
|
}
|
|
846
854
|
}
|
|
847
855
|
|
|
856
|
+
describe("useDomEditCommits rich-text persist handling", () => {
|
|
857
|
+
beforeEach(() => {
|
|
858
|
+
ensureCssEscape();
|
|
859
|
+
vi.clearAllMocks();
|
|
860
|
+
vi.unstubAllGlobals();
|
|
861
|
+
document.body.replaceChildren();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("restores the session snapshot when rich-text persistence fails", async () => {
|
|
865
|
+
stubPatchFetch({ ok: true, changed: false, matched: false });
|
|
866
|
+
const previousHtml = '<span style="color: red">Before</span>';
|
|
867
|
+
const html = '<span style="color: blue">After</span>';
|
|
868
|
+
const { iframe, element } = createPreviewElement(`<div data-hf-id="hf-card">${html}</div>`);
|
|
869
|
+
const rendered = renderDomEditCommits(createSelection(element), iframe);
|
|
870
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
871
|
+
|
|
872
|
+
try {
|
|
873
|
+
await act(async () => {
|
|
874
|
+
await rendered.hook.handleDomRichTextCommit({ element, html, previousHtml });
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
expect(element.innerHTML).toBe(previousHtml);
|
|
878
|
+
expect(rendered.showToast).toHaveBeenCalledWith(
|
|
879
|
+
expect.stringMatching(/Couldn't save "Hero title"/),
|
|
880
|
+
"error",
|
|
881
|
+
);
|
|
882
|
+
} finally {
|
|
883
|
+
warnSpy.mockRestore();
|
|
884
|
+
rendered.cleanup();
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it("does not retarget a commit onto a replacement preview node", async () => {
|
|
889
|
+
const fetchMock = stubPatchFetch({ ok: true, changed: true, matched: true });
|
|
890
|
+
const html = '<span style="color: blue">After</span>';
|
|
891
|
+
const { iframe, element } = createPreviewElement(`<div data-hf-id="hf-card">${html}</div>`);
|
|
892
|
+
const rendered = renderDomEditCommits(createSelection(element), iframe);
|
|
893
|
+
const replacement = document.createElement("div");
|
|
894
|
+
replacement.dataset.hfId = "hf-card";
|
|
895
|
+
replacement.innerHTML = "Reloaded elsewhere";
|
|
896
|
+
element.replaceWith(replacement);
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
await act(async () => {
|
|
900
|
+
await rendered.hook.handleDomRichTextCommit({
|
|
901
|
+
element,
|
|
902
|
+
html,
|
|
903
|
+
previousHtml: "Before",
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
expect(replacement.innerHTML).toBe("Reloaded elsewhere");
|
|
908
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
909
|
+
expect(rendered.showToast).not.toHaveBeenCalled();
|
|
910
|
+
} finally {
|
|
911
|
+
rendered.cleanup();
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
848
916
|
describe("useDomEditCommits style persist handling", () => {
|
|
849
917
|
beforeEach(() => {
|
|
850
918
|
ensureCssEscape();
|
|
@@ -1146,12 +1214,26 @@ describe("useDomEditCommits style persist handling", () => {
|
|
|
1146
1214
|
}
|
|
1147
1215
|
});
|
|
1148
1216
|
|
|
1149
|
-
it("
|
|
1150
|
-
await
|
|
1217
|
+
it("persists an added child text field as the element's new markup", async () => {
|
|
1218
|
+
await expectPersistedTextStructureEdit(
|
|
1219
|
+
(hook) => hook.handleDomAddTextField("first"),
|
|
1220
|
+
(markup) => {
|
|
1221
|
+
expect(markup).toContain("First");
|
|
1222
|
+
expect(markup).toContain("Second");
|
|
1223
|
+
// The layer that was added, between the two that were there.
|
|
1224
|
+
expect(markup.match(/<span/g) ?? []).toHaveLength(3);
|
|
1225
|
+
},
|
|
1226
|
+
);
|
|
1151
1227
|
});
|
|
1152
1228
|
|
|
1153
|
-
it("
|
|
1154
|
-
await
|
|
1229
|
+
it("persists a removed child text field as the element's new markup", async () => {
|
|
1230
|
+
await expectPersistedTextStructureEdit(
|
|
1231
|
+
(hook) => hook.handleDomRemoveTextField("first"),
|
|
1232
|
+
(markup) => {
|
|
1233
|
+
expect(markup).not.toContain("First");
|
|
1234
|
+
expect(markup).toContain("Second");
|
|
1235
|
+
},
|
|
1236
|
+
);
|
|
1155
1237
|
});
|
|
1156
1238
|
|
|
1157
1239
|
it("keeps single self text commits on the text-content path", async () => {
|
|
@@ -1256,6 +1338,44 @@ describe("useDomEditCommits attribute persist handling", () => {
|
|
|
1256
1338
|
}
|
|
1257
1339
|
});
|
|
1258
1340
|
|
|
1341
|
+
it("applies a preview-only write without persisting it", async () => {
|
|
1342
|
+
// What a drag needs from every pointermove: the preview and the audio graph
|
|
1343
|
+
// follow, the file does not. Persisting each move filled the undo stack with
|
|
1344
|
+
// fragments of one gesture — and because those writes race, a follow-up's
|
|
1345
|
+
// "before" often was not the previous entry's "after", so history refused to
|
|
1346
|
+
// coalesce them and undo took back a few milliseconds of the drag.
|
|
1347
|
+
const fetchSpy = stubPatchFetch({ ok: true, changed: true, matched: true });
|
|
1348
|
+
const { iframe, element } = createPreviewElement();
|
|
1349
|
+
const rendered = renderDomEditCommits(createSelection(element), iframe);
|
|
1350
|
+
|
|
1351
|
+
try {
|
|
1352
|
+
await act(async () => {
|
|
1353
|
+
await rendered.hook.handleDomAttributeLiveCommit("volume", "0.7", undefined, {
|
|
1354
|
+
previewOnly: true,
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
expect(element.getAttribute("data-volume")).toBe("0.7");
|
|
1358
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
1359
|
+
} finally {
|
|
1360
|
+
rendered.cleanup();
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
it("still persists a live write that does not ask to be preview-only", async () => {
|
|
1365
|
+
const fetchSpy = stubPatchFetch({ ok: true, changed: true, matched: true });
|
|
1366
|
+
const { iframe, element } = createPreviewElement();
|
|
1367
|
+
const rendered = renderDomEditCommits(createSelection(element), iframe);
|
|
1368
|
+
|
|
1369
|
+
try {
|
|
1370
|
+
await act(async () => {
|
|
1371
|
+
await rendered.hook.handleDomAttributeLiveCommit("volume", "0.7");
|
|
1372
|
+
});
|
|
1373
|
+
expect(fetchSpy).toHaveBeenCalled();
|
|
1374
|
+
} finally {
|
|
1375
|
+
rendered.cleanup();
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1259
1379
|
it("keeps a data-attribute commit on success", async () => {
|
|
1260
1380
|
stubPatchFetch({
|
|
1261
1381
|
ok: true,
|
|
@@ -375,9 +375,11 @@ export function useDomEditCommits({
|
|
|
375
375
|
handleDomStyleCommit,
|
|
376
376
|
handleDomAttributeCommit,
|
|
377
377
|
handleDomAttributeLiveCommit,
|
|
378
|
+
handleDomAttributeQuietCommit,
|
|
378
379
|
handleDomHtmlAttributeCommit,
|
|
379
380
|
handleDomAttributesCommit,
|
|
380
381
|
handleDomTextCommit,
|
|
382
|
+
handleDomRichTextCommit,
|
|
381
383
|
commitDomTextFields,
|
|
382
384
|
handleDomTextFieldStyleCommit,
|
|
383
385
|
handleDomAddTextField,
|
|
@@ -438,9 +440,11 @@ export function useDomEditCommits({
|
|
|
438
440
|
handleDomStyleCommit,
|
|
439
441
|
handleDomAttributeCommit,
|
|
440
442
|
handleDomAttributeLiveCommit,
|
|
443
|
+
handleDomAttributeQuietCommit,
|
|
441
444
|
handleDomHtmlAttributeCommit,
|
|
442
445
|
handleDomAttributesCommit,
|
|
443
446
|
handleDomTextCommit,
|
|
447
|
+
handleDomRichTextCommit,
|
|
444
448
|
commitDomTextFields,
|
|
445
449
|
handleDomTextFieldStyleCommit,
|
|
446
450
|
handleDomAddTextField,
|
|
@@ -228,9 +228,11 @@ export function useDomEditSession({
|
|
|
228
228
|
handleDomStyleCommit,
|
|
229
229
|
handleDomAttributeCommit,
|
|
230
230
|
handleDomAttributeLiveCommit,
|
|
231
|
+
handleDomAttributeQuietCommit,
|
|
231
232
|
handleDomHtmlAttributeCommit,
|
|
232
233
|
handleDomAttributesCommit,
|
|
233
234
|
handleDomTextCommit,
|
|
235
|
+
handleDomRichTextCommit,
|
|
234
236
|
handleDomTextFieldStyleCommit,
|
|
235
237
|
handleDomAddTextField,
|
|
236
238
|
handleDomRemoveTextField,
|
|
@@ -492,6 +494,7 @@ export function useDomEditSession({
|
|
|
492
494
|
handleDomStyleCommit,
|
|
493
495
|
handleDomAttributeCommit,
|
|
494
496
|
handleDomAttributeLiveCommit,
|
|
497
|
+
handleDomAttributeQuietCommit,
|
|
495
498
|
handleDomHtmlAttributeCommit,
|
|
496
499
|
handleDomAttributesCommit,
|
|
497
500
|
handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
|
|
@@ -501,6 +504,7 @@ export function useDomEditSession({
|
|
|
501
504
|
handleDomRotationCommit: handleGsapAwareRotationCommit,
|
|
502
505
|
handleDomManualEditsReset,
|
|
503
506
|
handleDomTextCommit,
|
|
507
|
+
handleDomRichTextCommit,
|
|
504
508
|
handleDomTextFieldStyleCommit,
|
|
505
509
|
handleDomAddTextField,
|
|
506
510
|
handleDomRemoveTextField,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ensureImportedFontFace,
|
|
12
12
|
} from "../utils/studioFontHelpers";
|
|
13
13
|
import {
|
|
14
|
+
buildDomEditRichTextPatchOperation,
|
|
14
15
|
buildDomEditStylePatchOperation,
|
|
15
16
|
buildDomEditTextPatchOperation,
|
|
16
17
|
findElementForSelection,
|
|
@@ -23,17 +24,16 @@ import {
|
|
|
23
24
|
} from "../components/editor/domEditing";
|
|
24
25
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
25
26
|
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
27
|
+
import { canEditElementTextInline } from "../components/editor/domEditInlineText";
|
|
26
28
|
import { buildTextFieldChildOperations } from "./domEditTextFieldCommitOps";
|
|
27
|
-
import {
|
|
28
|
-
DomEditPersistUnsupportedTextStructureError,
|
|
29
|
-
reportDomEditPersistFailure,
|
|
30
|
-
} from "./domEditPersistFailure";
|
|
29
|
+
import { reportDomEditPersistFailure } from "./domEditPersistFailure";
|
|
31
30
|
import {
|
|
32
31
|
bumpDomEditCommitMapVersion,
|
|
33
32
|
bumpDomEditCommitVersion,
|
|
34
33
|
runDomEditCommit,
|
|
35
34
|
} from "./domEditCommitRunner";
|
|
36
35
|
import { useDomEditAttributeCommits } from "./useDomEditAttributeCommits";
|
|
36
|
+
import type { InlineTextEditCommit } from "./useInlineTextEdit";
|
|
37
37
|
|
|
38
38
|
// ── Types ──
|
|
39
39
|
|
|
@@ -62,6 +62,20 @@ interface DomTextCommitPlan {
|
|
|
62
62
|
operations: PatchOperation[];
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function canCommitInlineTextSelection(selection: DomEditSelection, element: HTMLElement): boolean {
|
|
66
|
+
if (selection.isCompositionHost || selection.isInsideLockedComposition) return false;
|
|
67
|
+
return canEditElementTextInline(element);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ownsCurrentPreviewElement(
|
|
71
|
+
selection: DomEditSelection,
|
|
72
|
+
element: HTMLElement,
|
|
73
|
+
document: Document | null | undefined,
|
|
74
|
+
): document is Document {
|
|
75
|
+
if (!document || !element.isConnected) return false;
|
|
76
|
+
return element === selection.element && element.ownerDocument === document;
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
function buildDomStyleCommitOperations(
|
|
66
80
|
property: string,
|
|
67
81
|
value: string,
|
|
@@ -102,9 +116,20 @@ function planDomTextCommit(
|
|
|
102
116
|
const childOperations = usesSerializedTextFields
|
|
103
117
|
? buildTextFieldChildOperations(originalTextFields, nextTextFields)
|
|
104
118
|
: null;
|
|
119
|
+
// Per-child operations when the layers still line up one-for-one, and the
|
|
120
|
+
// element's whole markup when they do not.
|
|
121
|
+
//
|
|
122
|
+
// `buildTextFieldChildOperations` can only address children that already
|
|
123
|
+
// exist, so it returns null for any change in how many there are — which is
|
|
124
|
+
// every delete and every add. That used to end here with "Couldn't save this
|
|
125
|
+
// text structure change": the panel offered a remove button and an Add text
|
|
126
|
+
// field row, and neither could ever save. A structure change has one honest
|
|
127
|
+
// operation, which is to write the structure.
|
|
105
128
|
const operations =
|
|
106
129
|
childOperations ??
|
|
107
|
-
(usesSerializedTextFields
|
|
130
|
+
(usesSerializedTextFields
|
|
131
|
+
? [buildDomEditRichTextPatchOperation(nextContent)]
|
|
132
|
+
: [buildDomEditTextPatchOperation(nextContent)]);
|
|
108
133
|
|
|
109
134
|
return {
|
|
110
135
|
usesSerializedTextFields,
|
|
@@ -148,6 +173,7 @@ export function useDomEditTextCommits({
|
|
|
148
173
|
const {
|
|
149
174
|
handleDomAttributeCommit,
|
|
150
175
|
handleDomAttributeLiveCommit,
|
|
176
|
+
handleDomAttributeQuietCommit,
|
|
151
177
|
handleDomHtmlAttributeCommit,
|
|
152
178
|
handleDomAttributesCommit,
|
|
153
179
|
} = useDomEditAttributeCommits({
|
|
@@ -269,9 +295,6 @@ export function useDomEditTextCommits({
|
|
|
269
295
|
}
|
|
270
296
|
},
|
|
271
297
|
persist: async () => {
|
|
272
|
-
if (textCommit.usesSerializedTextFields && textCommit.childOperations === null) {
|
|
273
|
-
throw new DomEditPersistUnsupportedTextStructureError();
|
|
274
|
-
}
|
|
275
298
|
await persistDomEditOperations(domEditSelection, textCommit.operations, {
|
|
276
299
|
label: "Edit text",
|
|
277
300
|
skipRefresh: true,
|
|
@@ -307,6 +330,85 @@ export function useDomEditTextCommits({
|
|
|
307
330
|
],
|
|
308
331
|
);
|
|
309
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Persist an element's own markup, for a text edit that styled part of it.
|
|
335
|
+
*
|
|
336
|
+
* Its own commit rather than a mode of the one above: that one plans a change
|
|
337
|
+
* to the text-field model, which escapes markup on the way out and refuses a
|
|
338
|
+
* change in child structure, and both of those are correct for the design
|
|
339
|
+
* panel. Styling a run of characters is neither of those things. The element
|
|
340
|
+
* already holds what the user typed, so there is nothing to apply, only
|
|
341
|
+
* something to save and something to put back if saving fails.
|
|
342
|
+
*/
|
|
343
|
+
const handleDomRichTextCommit = useCallback(
|
|
344
|
+
async ({ element, html, previousHtml }: InlineTextEditCommit) => {
|
|
345
|
+
if (!domEditSelection) return;
|
|
346
|
+
// The same gate that let the edit open, not the design panel's.
|
|
347
|
+
//
|
|
348
|
+
// The panel's rule is about its text fields, and it has none for an
|
|
349
|
+
// element whose text contains a line break: a `<span>` holding `<br>`s
|
|
350
|
+
// is not a leaf, so nothing inside is a field and the element reports no
|
|
351
|
+
// editable text at all. Editing in place does not use fields — it
|
|
352
|
+
// rewrites the element's own markup — so refusing on that rule refused
|
|
353
|
+
// elements the caret had just been opened in, and every colour the user
|
|
354
|
+
// chose was dropped on the way out with nothing said about it.
|
|
355
|
+
if (!canCommitInlineTextSelection(domEditSelection, element)) return;
|
|
356
|
+
const iframe = previewIframeRef.current;
|
|
357
|
+
const doc = iframe?.contentDocument;
|
|
358
|
+
// A preview reload replaces the document. Never resolve this commit onto
|
|
359
|
+
// the replacement node: it did not own the edit or its rollback snapshot.
|
|
360
|
+
if (!ownsCurrentPreviewElement(domEditSelection, element, doc)) return;
|
|
361
|
+
const isLatestTextCommit = bumpDomEditCommitVersion(domTextCommitVersionRef);
|
|
362
|
+
const operations = [buildDomEditRichTextPatchOperation(html)];
|
|
363
|
+
let appliedHtml = "";
|
|
364
|
+
|
|
365
|
+
await runDomEditCommit({
|
|
366
|
+
capture: () => {},
|
|
367
|
+
apply: () => {
|
|
368
|
+
// Idempotent: the caret put this there. Assigned anyway so a commit
|
|
369
|
+
// raised from anywhere but the element itself still lands.
|
|
370
|
+
element.innerHTML = html;
|
|
371
|
+
appliedHtml = element.innerHTML;
|
|
372
|
+
},
|
|
373
|
+
persist: async () => {
|
|
374
|
+
await persistDomEditOperations(domEditSelection, operations, {
|
|
375
|
+
label: "Edit text",
|
|
376
|
+
skipRefresh: true,
|
|
377
|
+
shouldSave: isLatestTextCommit,
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
shouldRevert: () => isLatestTextCommit(),
|
|
381
|
+
revert: () => {
|
|
382
|
+
// An external actor that changed the live node while the request was
|
|
383
|
+
// in flight owns its new value; only roll back the value we submitted.
|
|
384
|
+
if (element.isConnected && element.innerHTML === appliedHtml) {
|
|
385
|
+
element.innerHTML = previousHtml;
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
onError: (error) =>
|
|
389
|
+
reportDomEditPersistFailure(domEditSelection, operations, error, showToast),
|
|
390
|
+
shouldResync: isLatestTextCommit,
|
|
391
|
+
resync: () =>
|
|
392
|
+
resyncDomTextSelectionFromPreview(
|
|
393
|
+
doc,
|
|
394
|
+
domEditSelection,
|
|
395
|
+
activeCompPath,
|
|
396
|
+
buildDomSelectionFromTarget,
|
|
397
|
+
applyDomSelection,
|
|
398
|
+
),
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
[
|
|
402
|
+
activeCompPath,
|
|
403
|
+
applyDomSelection,
|
|
404
|
+
buildDomSelectionFromTarget,
|
|
405
|
+
domEditSelection,
|
|
406
|
+
persistDomEditOperations,
|
|
407
|
+
previewIframeRef,
|
|
408
|
+
showToast,
|
|
409
|
+
],
|
|
410
|
+
);
|
|
411
|
+
|
|
310
412
|
const commitDomTextFields = useCallback(
|
|
311
413
|
async (
|
|
312
414
|
selection: DomEditSelection,
|
|
@@ -342,9 +444,6 @@ export function useDomEditTextCommits({
|
|
|
342
444
|
}
|
|
343
445
|
},
|
|
344
446
|
persist: async () => {
|
|
345
|
-
if (textCommit.usesSerializedTextFields && textCommit.childOperations === null) {
|
|
346
|
-
throw new DomEditPersistUnsupportedTextStructureError();
|
|
347
|
-
}
|
|
348
447
|
await persistDomEditOperations(selection, textCommit.operations, {
|
|
349
448
|
label: "Edit text",
|
|
350
449
|
skipRefresh: true,
|
|
@@ -474,9 +573,11 @@ export function useDomEditTextCommits({
|
|
|
474
573
|
handleDomStyleCommit,
|
|
475
574
|
handleDomAttributeCommit,
|
|
476
575
|
handleDomAttributeLiveCommit,
|
|
576
|
+
handleDomAttributeQuietCommit,
|
|
477
577
|
handleDomHtmlAttributeCommit,
|
|
478
578
|
handleDomAttributesCommit,
|
|
479
579
|
handleDomTextCommit,
|
|
580
|
+
handleDomRichTextCommit,
|
|
480
581
|
commitDomTextFields,
|
|
481
582
|
handleDomTextFieldStyleCommit,
|
|
482
583
|
handleDomAddTextField,
|
|
@@ -142,6 +142,9 @@ export function useElementLifecycleOps({
|
|
|
142
142
|
kind: "timeline",
|
|
143
143
|
files: { [targetPath]: patchedContent },
|
|
144
144
|
readFile: async () => originalContent,
|
|
145
|
+
// remove-element already wrote the removal, so disk holds THAT — not
|
|
146
|
+
// the content read at the top. Undo still goes back to the original.
|
|
147
|
+
diskContent: { [targetPath]: patchedContent },
|
|
145
148
|
writeFile: writeProjectFile,
|
|
146
149
|
recordEdit: editHistory.recordEdit,
|
|
147
150
|
});
|
package/src/hooks/useFileTree.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import { FONT_EXT } from "../utils/mediaTypes";
|
|
3
3
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
4
|
+
import { captureProjectProvenance } from "../components/feedback/projectProvenance";
|
|
4
5
|
|
|
5
6
|
interface UseFileTreeOptions {
|
|
6
7
|
projectId: string | null;
|
|
@@ -24,9 +25,13 @@ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
|
24
25
|
fetch(`/api/projects/${projectId}`)
|
|
25
26
|
.then((r) => r.json())
|
|
26
27
|
.then((data: { files?: string[]; dir?: string; compositions?: string[] }) => {
|
|
27
|
-
if (
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
28
|
+
if (cancelled) return;
|
|
29
|
+
if (data.files) setFileTree(data.files);
|
|
30
|
+
if (data.compositions) setCompositionPaths(data.compositions);
|
|
31
|
+
setProjectDir(typeof data.dir === "string" ? data.dir : null);
|
|
32
|
+
// Snapshot how this project was made, while the listing is in hand and
|
|
33
|
+
// the app is still alive. A crash later has no other way to learn it.
|
|
34
|
+
void captureProjectProvenance(projectId, data.files ?? [], data.compositions ?? []);
|
|
30
35
|
})
|
|
31
36
|
.catch(() => {
|
|
32
37
|
if (!cancelled) setProjectDir(null);
|