@hyperframes/studio 0.7.106 → 0.7.107
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-BLrfwq5o.js} +1 -1
- package/dist/assets/{index-CRzCCuPH.js → index-BA9yhzfR.js} +1 -1
- package/dist/assets/{index-BbYg_isc.js → index-BSBk5srs.js} +1 -1
- package/dist/assets/{index-DerI0ikN.js → index-dF-CmLZu.js} +218 -218
- package/dist/assets/index-zQ4JFwwB.css +1 -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 +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +5243 -3933
- 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/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/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/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +119 -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 +4 -0
- package/src/hooks/domEditPersistFailure.ts +0 -7
- package/src/hooks/useAppHotkeys.ts +4 -4
- package/src/hooks/useDomEditCommits.test.tsx +103 -21
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -0
- package/src/hooks/useDomEditTextCommits.ts +110 -11
- package/src/hooks/useFileTree.ts +8 -3
- package/src/hooks/useInlineTextEdit.test.tsx +555 -0
- package/src/hooks/useInlineTextEdit.ts +320 -0
- package/src/player/lib/playbackShortcuts.ts +5 -4
- 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/utils/sourcePatcher.ts +5 -1
- package/src/utils/studioHelpers.ts +2 -5
- 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-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
|
@@ -24,6 +24,7 @@ export interface DomEditActionsValue extends Pick<
|
|
|
24
24
|
| "handleDomRotationCommit"
|
|
25
25
|
| "handleDomManualEditsReset"
|
|
26
26
|
| "handleDomTextCommit"
|
|
27
|
+
| "handleDomRichTextCommit"
|
|
27
28
|
| "handleDomTextFieldStyleCommit"
|
|
28
29
|
| "handleDomAddTextField"
|
|
29
30
|
| "handleDomRemoveTextField"
|
|
@@ -149,6 +150,7 @@ export function DomEditProvider({
|
|
|
149
150
|
handleDomManualEditsReset,
|
|
150
151
|
|
|
151
152
|
handleDomTextCommit,
|
|
153
|
+
handleDomRichTextCommit,
|
|
152
154
|
handleDomTextFieldStyleCommit,
|
|
153
155
|
handleDomAddTextField,
|
|
154
156
|
handleDomRemoveTextField,
|
|
@@ -236,6 +238,7 @@ export function DomEditProvider({
|
|
|
236
238
|
handleDomRotationCommit,
|
|
237
239
|
handleDomManualEditsReset,
|
|
238
240
|
handleDomTextCommit,
|
|
241
|
+
handleDomRichTextCommit,
|
|
239
242
|
handleDomTextFieldStyleCommit,
|
|
240
243
|
handleDomAddTextField,
|
|
241
244
|
handleDomRemoveTextField,
|
|
@@ -305,6 +308,7 @@ export function DomEditProvider({
|
|
|
305
308
|
handleDomRotationCommit,
|
|
306
309
|
handleDomManualEditsReset,
|
|
307
310
|
handleDomTextCommit,
|
|
311
|
+
handleDomRichTextCommit,
|
|
308
312
|
handleDomTextFieldStyleCommit,
|
|
309
313
|
handleDomAddTextField,
|
|
310
314
|
handleDomRemoveTextField,
|
|
@@ -19,13 +19,6 @@ export class DomEditPersistUnsafeValueError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export class DomEditPersistUnsupportedTextStructureError extends Error {
|
|
23
|
-
constructor() {
|
|
24
|
-
super("Couldn't save this text structure change");
|
|
25
|
-
this.name = "DomEditPersistUnsupportedTextStructureError";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
export type DomEditPersistFailureSelection = Pick<
|
|
30
23
|
DomEditSelection,
|
|
31
24
|
"label" | "hfId" | "id" | "selector" | "selectorIndex" | "sourceFile"
|
|
@@ -4,7 +4,7 @@ import type { TimelineElement } from "../player";
|
|
|
4
4
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
5
5
|
import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
|
|
6
6
|
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
7
|
-
import {
|
|
7
|
+
import { isTypingTarget } from "../utils/typingTarget";
|
|
8
8
|
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
|
|
9
9
|
import { canSplitElement } from "../utils/timelineElementSplit";
|
|
10
10
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
@@ -188,14 +188,14 @@ function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallba
|
|
|
188
188
|
return true;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
if (key === "g" && !event.altKey && !
|
|
191
|
+
if (key === "g" && !event.altKey && !isTypingTarget(event.target)) {
|
|
192
192
|
event.preventDefault();
|
|
193
193
|
if (event.shiftKey) cb.onUngroupSelection?.();
|
|
194
194
|
else cb.onGroupSelection?.();
|
|
195
195
|
return true;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
if (!event.shiftKey && !event.altKey && !
|
|
198
|
+
if (!event.shiftKey && !event.altKey && !isTypingTarget(event.target)) {
|
|
199
199
|
if (key === "c") {
|
|
200
200
|
if (cb.handleCopy()) {
|
|
201
201
|
event.preventDefault();
|
|
@@ -454,7 +454,7 @@ export function useAppHotkeys({
|
|
|
454
454
|
dispatchModifierKey(event, key, cb);
|
|
455
455
|
return;
|
|
456
456
|
}
|
|
457
|
-
if (!
|
|
457
|
+
if (!isTypingTarget(event.target)) dispatchPlainKey(event, key, cb);
|
|
458
458
|
}, []);
|
|
459
459
|
|
|
460
460
|
// eslint-disable-next-line no-restricted-syntax
|
|
@@ -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 () => {
|
|
@@ -378,6 +378,7 @@ export function useDomEditCommits({
|
|
|
378
378
|
handleDomHtmlAttributeCommit,
|
|
379
379
|
handleDomAttributesCommit,
|
|
380
380
|
handleDomTextCommit,
|
|
381
|
+
handleDomRichTextCommit,
|
|
381
382
|
commitDomTextFields,
|
|
382
383
|
handleDomTextFieldStyleCommit,
|
|
383
384
|
handleDomAddTextField,
|
|
@@ -441,6 +442,7 @@ export function useDomEditCommits({
|
|
|
441
442
|
handleDomHtmlAttributeCommit,
|
|
442
443
|
handleDomAttributesCommit,
|
|
443
444
|
handleDomTextCommit,
|
|
445
|
+
handleDomRichTextCommit,
|
|
444
446
|
commitDomTextFields,
|
|
445
447
|
handleDomTextFieldStyleCommit,
|
|
446
448
|
handleDomAddTextField,
|
|
@@ -231,6 +231,7 @@ export function useDomEditSession({
|
|
|
231
231
|
handleDomHtmlAttributeCommit,
|
|
232
232
|
handleDomAttributesCommit,
|
|
233
233
|
handleDomTextCommit,
|
|
234
|
+
handleDomRichTextCommit,
|
|
234
235
|
handleDomTextFieldStyleCommit,
|
|
235
236
|
handleDomAddTextField,
|
|
236
237
|
handleDomRemoveTextField,
|
|
@@ -501,6 +502,7 @@ export function useDomEditSession({
|
|
|
501
502
|
handleDomRotationCommit: handleGsapAwareRotationCommit,
|
|
502
503
|
handleDomManualEditsReset,
|
|
503
504
|
handleDomTextCommit,
|
|
505
|
+
handleDomRichTextCommit,
|
|
504
506
|
handleDomTextFieldStyleCommit,
|
|
505
507
|
handleDomAddTextField,
|
|
506
508
|
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,
|
|
@@ -269,9 +294,6 @@ export function useDomEditTextCommits({
|
|
|
269
294
|
}
|
|
270
295
|
},
|
|
271
296
|
persist: async () => {
|
|
272
|
-
if (textCommit.usesSerializedTextFields && textCommit.childOperations === null) {
|
|
273
|
-
throw new DomEditPersistUnsupportedTextStructureError();
|
|
274
|
-
}
|
|
275
297
|
await persistDomEditOperations(domEditSelection, textCommit.operations, {
|
|
276
298
|
label: "Edit text",
|
|
277
299
|
skipRefresh: true,
|
|
@@ -307,6 +329,85 @@ export function useDomEditTextCommits({
|
|
|
307
329
|
],
|
|
308
330
|
);
|
|
309
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Persist an element's own markup, for a text edit that styled part of it.
|
|
334
|
+
*
|
|
335
|
+
* Its own commit rather than a mode of the one above: that one plans a change
|
|
336
|
+
* to the text-field model, which escapes markup on the way out and refuses a
|
|
337
|
+
* change in child structure, and both of those are correct for the design
|
|
338
|
+
* panel. Styling a run of characters is neither of those things. The element
|
|
339
|
+
* already holds what the user typed, so there is nothing to apply, only
|
|
340
|
+
* something to save and something to put back if saving fails.
|
|
341
|
+
*/
|
|
342
|
+
const handleDomRichTextCommit = useCallback(
|
|
343
|
+
async ({ element, html, previousHtml }: InlineTextEditCommit) => {
|
|
344
|
+
if (!domEditSelection) return;
|
|
345
|
+
// The same gate that let the edit open, not the design panel's.
|
|
346
|
+
//
|
|
347
|
+
// The panel's rule is about its text fields, and it has none for an
|
|
348
|
+
// element whose text contains a line break: a `<span>` holding `<br>`s
|
|
349
|
+
// is not a leaf, so nothing inside is a field and the element reports no
|
|
350
|
+
// editable text at all. Editing in place does not use fields — it
|
|
351
|
+
// rewrites the element's own markup — so refusing on that rule refused
|
|
352
|
+
// elements the caret had just been opened in, and every colour the user
|
|
353
|
+
// chose was dropped on the way out with nothing said about it.
|
|
354
|
+
if (!canCommitInlineTextSelection(domEditSelection, element)) return;
|
|
355
|
+
const iframe = previewIframeRef.current;
|
|
356
|
+
const doc = iframe?.contentDocument;
|
|
357
|
+
// A preview reload replaces the document. Never resolve this commit onto
|
|
358
|
+
// the replacement node: it did not own the edit or its rollback snapshot.
|
|
359
|
+
if (!ownsCurrentPreviewElement(domEditSelection, element, doc)) return;
|
|
360
|
+
const isLatestTextCommit = bumpDomEditCommitVersion(domTextCommitVersionRef);
|
|
361
|
+
const operations = [buildDomEditRichTextPatchOperation(html)];
|
|
362
|
+
let appliedHtml = "";
|
|
363
|
+
|
|
364
|
+
await runDomEditCommit({
|
|
365
|
+
capture: () => {},
|
|
366
|
+
apply: () => {
|
|
367
|
+
// Idempotent: the caret put this there. Assigned anyway so a commit
|
|
368
|
+
// raised from anywhere but the element itself still lands.
|
|
369
|
+
element.innerHTML = html;
|
|
370
|
+
appliedHtml = element.innerHTML;
|
|
371
|
+
},
|
|
372
|
+
persist: async () => {
|
|
373
|
+
await persistDomEditOperations(domEditSelection, operations, {
|
|
374
|
+
label: "Edit text",
|
|
375
|
+
skipRefresh: true,
|
|
376
|
+
shouldSave: isLatestTextCommit,
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
shouldRevert: () => isLatestTextCommit(),
|
|
380
|
+
revert: () => {
|
|
381
|
+
// An external actor that changed the live node while the request was
|
|
382
|
+
// in flight owns its new value; only roll back the value we submitted.
|
|
383
|
+
if (element.isConnected && element.innerHTML === appliedHtml) {
|
|
384
|
+
element.innerHTML = previousHtml;
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
onError: (error) =>
|
|
388
|
+
reportDomEditPersistFailure(domEditSelection, operations, error, showToast),
|
|
389
|
+
shouldResync: isLatestTextCommit,
|
|
390
|
+
resync: () =>
|
|
391
|
+
resyncDomTextSelectionFromPreview(
|
|
392
|
+
doc,
|
|
393
|
+
domEditSelection,
|
|
394
|
+
activeCompPath,
|
|
395
|
+
buildDomSelectionFromTarget,
|
|
396
|
+
applyDomSelection,
|
|
397
|
+
),
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
[
|
|
401
|
+
activeCompPath,
|
|
402
|
+
applyDomSelection,
|
|
403
|
+
buildDomSelectionFromTarget,
|
|
404
|
+
domEditSelection,
|
|
405
|
+
persistDomEditOperations,
|
|
406
|
+
previewIframeRef,
|
|
407
|
+
showToast,
|
|
408
|
+
],
|
|
409
|
+
);
|
|
410
|
+
|
|
310
411
|
const commitDomTextFields = useCallback(
|
|
311
412
|
async (
|
|
312
413
|
selection: DomEditSelection,
|
|
@@ -342,9 +443,6 @@ export function useDomEditTextCommits({
|
|
|
342
443
|
}
|
|
343
444
|
},
|
|
344
445
|
persist: async () => {
|
|
345
|
-
if (textCommit.usesSerializedTextFields && textCommit.childOperations === null) {
|
|
346
|
-
throw new DomEditPersistUnsupportedTextStructureError();
|
|
347
|
-
}
|
|
348
446
|
await persistDomEditOperations(selection, textCommit.operations, {
|
|
349
447
|
label: "Edit text",
|
|
350
448
|
skipRefresh: true,
|
|
@@ -477,6 +575,7 @@ export function useDomEditTextCommits({
|
|
|
477
575
|
handleDomHtmlAttributeCommit,
|
|
478
576
|
handleDomAttributesCommit,
|
|
479
577
|
handleDomTextCommit,
|
|
578
|
+
handleDomRichTextCommit,
|
|
480
579
|
commitDomTextFields,
|
|
481
580
|
handleDomTextFieldStyleCommit,
|
|
482
581
|
handleDomAddTextField,
|
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);
|