@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,465 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act, useState } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { FlatTextLayerList, FlatTextSection } from "./propertyPanelFlatTextSection";
|
|
7
|
+
import type { DomEditSelection, DomEditTextField } from "./domEditingTypes";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
document.body.innerHTML = "";
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function renderInto(node: React.ReactElement) {
|
|
16
|
+
const host = document.createElement("div");
|
|
17
|
+
document.body.append(host);
|
|
18
|
+
const root = createRoot(host);
|
|
19
|
+
act(() => {
|
|
20
|
+
root.render(node);
|
|
21
|
+
});
|
|
22
|
+
return { host, root };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const FIELDS = [
|
|
26
|
+
{
|
|
27
|
+
key: "a",
|
|
28
|
+
label: "Text",
|
|
29
|
+
value: "Headline",
|
|
30
|
+
tagName: "div",
|
|
31
|
+
attributes: [],
|
|
32
|
+
inlineStyles: {},
|
|
33
|
+
computedStyles: {},
|
|
34
|
+
source: "self" as const,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "b",
|
|
38
|
+
label: "Text",
|
|
39
|
+
value: "Subhead",
|
|
40
|
+
tagName: "span",
|
|
41
|
+
attributes: [],
|
|
42
|
+
inlineStyles: {},
|
|
43
|
+
computedStyles: {},
|
|
44
|
+
source: "self" as const,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
describe("FlatTextLayerList", () => {
|
|
49
|
+
it("falls back to a numbered label per index for empty fields, not a bare 'Text'", () => {
|
|
50
|
+
const emptyFields = [
|
|
51
|
+
{ ...FIELDS[0], value: "" },
|
|
52
|
+
{ ...FIELDS[1], value: "" },
|
|
53
|
+
];
|
|
54
|
+
const { host, root } = renderInto(
|
|
55
|
+
<FlatTextLayerList
|
|
56
|
+
fields={emptyFields as never}
|
|
57
|
+
activeFieldKey="a"
|
|
58
|
+
styles={{}}
|
|
59
|
+
onSelect={vi.fn()}
|
|
60
|
+
onAdd={vi.fn()}
|
|
61
|
+
onRemove={vi.fn()}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
expect(host.textContent).toContain("Text 1");
|
|
65
|
+
expect(host.textContent).toContain("Text 2");
|
|
66
|
+
act(() => root.unmount());
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("lists every field, highlights the active one, and fires onSelect/onAdd/onRemove", () => {
|
|
70
|
+
const onSelect = vi.fn();
|
|
71
|
+
const onAdd = vi.fn();
|
|
72
|
+
const onRemove = vi.fn();
|
|
73
|
+
const { host, root } = renderInto(
|
|
74
|
+
<FlatTextLayerList
|
|
75
|
+
fields={FIELDS as never}
|
|
76
|
+
activeFieldKey="a"
|
|
77
|
+
styles={{}}
|
|
78
|
+
onSelect={onSelect}
|
|
79
|
+
onAdd={onAdd}
|
|
80
|
+
onRemove={onRemove}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
expect(host.textContent).toContain("Headline");
|
|
84
|
+
expect(host.textContent).toContain("Subhead");
|
|
85
|
+
|
|
86
|
+
const rows = host.querySelectorAll('[data-flat-text-layer-row="true"]');
|
|
87
|
+
expect(rows).toHaveLength(2);
|
|
88
|
+
expect((rows[0] as HTMLElement).getAttribute("data-active")).toBe("true");
|
|
89
|
+
expect((rows[1] as HTMLElement).getAttribute("data-active")).toBe("false");
|
|
90
|
+
|
|
91
|
+
act(() => rows[1].dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
92
|
+
expect(onSelect).toHaveBeenCalledWith("b");
|
|
93
|
+
|
|
94
|
+
const addButton = host.querySelector<HTMLButtonElement>('[data-flat-text-layer-add="true"]');
|
|
95
|
+
act(() => addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
96
|
+
expect(onAdd).toHaveBeenCalledTimes(1);
|
|
97
|
+
|
|
98
|
+
const removeButton = host.querySelector<HTMLButtonElement>(
|
|
99
|
+
'[data-flat-text-layer-remove="true"]',
|
|
100
|
+
);
|
|
101
|
+
act(() => removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
102
|
+
expect(onRemove).toHaveBeenCalledWith("a");
|
|
103
|
+
// stopPropagation on the remove button must prevent the row's own onClick
|
|
104
|
+
// from also firing onSelect for the removed field's key.
|
|
105
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(onSelect).not.toHaveBeenCalledWith("a");
|
|
107
|
+
act(() => root.unmount());
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function makeMultiFieldElement(): DomEditSelection {
|
|
112
|
+
return {
|
|
113
|
+
element: document.createElement("div"),
|
|
114
|
+
id: "multi",
|
|
115
|
+
selector: ".multi",
|
|
116
|
+
label: "Multi",
|
|
117
|
+
tagName: "div",
|
|
118
|
+
sourceFile: "index.html",
|
|
119
|
+
compositionPath: "index.html",
|
|
120
|
+
isCompositionHost: false,
|
|
121
|
+
isInsideLockedComposition: false,
|
|
122
|
+
boundingBox: { x: 0, y: 0, width: 100, height: 100 },
|
|
123
|
+
textContent: "Headline Subhead",
|
|
124
|
+
dataAttributes: {},
|
|
125
|
+
inlineStyles: {},
|
|
126
|
+
computedStyles: {},
|
|
127
|
+
textFields: [
|
|
128
|
+
{
|
|
129
|
+
key: "a",
|
|
130
|
+
label: "Text",
|
|
131
|
+
value: "Headline",
|
|
132
|
+
tagName: "div",
|
|
133
|
+
attributes: [],
|
|
134
|
+
inlineStyles: {},
|
|
135
|
+
computedStyles: {},
|
|
136
|
+
source: "self",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: "b",
|
|
140
|
+
label: "Text",
|
|
141
|
+
value: "Subhead",
|
|
142
|
+
tagName: "span",
|
|
143
|
+
attributes: [],
|
|
144
|
+
inlineStyles: {},
|
|
145
|
+
computedStyles: {},
|
|
146
|
+
source: "self",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
capabilities: {
|
|
150
|
+
canSelect: true,
|
|
151
|
+
canEditStyles: true,
|
|
152
|
+
canCrop: true,
|
|
153
|
+
canMove: true,
|
|
154
|
+
canResize: true,
|
|
155
|
+
canApplyManualOffset: true,
|
|
156
|
+
canApplyManualSize: true,
|
|
157
|
+
canApplyManualRotation: true,
|
|
158
|
+
},
|
|
159
|
+
} as DomEditSelection;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function makeSingleFieldElement(overrides: Partial<DomEditTextField> = {}): DomEditSelection {
|
|
163
|
+
const base = makeMultiFieldElement();
|
|
164
|
+
return {
|
|
165
|
+
...base,
|
|
166
|
+
textFields: [
|
|
167
|
+
{
|
|
168
|
+
key: "a",
|
|
169
|
+
label: "Text",
|
|
170
|
+
value: "Headline",
|
|
171
|
+
tagName: "div",
|
|
172
|
+
attributes: [],
|
|
173
|
+
inlineStyles: {},
|
|
174
|
+
computedStyles: {},
|
|
175
|
+
source: "self",
|
|
176
|
+
...overrides,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
} as DomEditSelection;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function segmentedRowButtons(host: HTMLElement, label: string): HTMLButtonElement[] {
|
|
183
|
+
const labelSpan = Array.from(host.querySelectorAll("span")).find(
|
|
184
|
+
(el) => el.textContent === label,
|
|
185
|
+
);
|
|
186
|
+
const row = labelSpan?.parentElement;
|
|
187
|
+
return Array.from(row?.querySelectorAll<HTMLButtonElement>('[data-flat-segment="true"]') ?? []);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
describe("FlatTextFieldEditor controls", () => {
|
|
191
|
+
it("commits text-transform: capitalize when the new 'Ag' case button is clicked", () => {
|
|
192
|
+
const onSetTextFieldStyle = vi.fn();
|
|
193
|
+
const { host, root } = renderInto(
|
|
194
|
+
<FlatTextSection
|
|
195
|
+
element={makeSingleFieldElement()}
|
|
196
|
+
styles={{}}
|
|
197
|
+
fontAssets={[]}
|
|
198
|
+
onSetText={vi.fn()}
|
|
199
|
+
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
200
|
+
onAddTextField={vi.fn()}
|
|
201
|
+
onRemoveTextField={vi.fn()}
|
|
202
|
+
/>,
|
|
203
|
+
);
|
|
204
|
+
const capitalizeButton = segmentedRowButtons(host, "Case · Style").find(
|
|
205
|
+
(button) => button.textContent === "Ag",
|
|
206
|
+
);
|
|
207
|
+
expect(capitalizeButton).not.toBeUndefined();
|
|
208
|
+
act(() => capitalizeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
209
|
+
expect(onSetTextFieldStyle).toHaveBeenCalledWith("a", "text-transform", "capitalize");
|
|
210
|
+
act(() => root.unmount());
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("lights up 'right' for text-align: end but re-clicking it is a no-op — preserves the logical value", () => {
|
|
214
|
+
const onSetTextFieldStyle = vi.fn();
|
|
215
|
+
const { host, root } = renderInto(
|
|
216
|
+
<FlatTextSection
|
|
217
|
+
element={makeSingleFieldElement({ computedStyles: { "text-align": "end" } })}
|
|
218
|
+
styles={{}}
|
|
219
|
+
fontAssets={[]}
|
|
220
|
+
onSetText={vi.fn()}
|
|
221
|
+
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
222
|
+
onAddTextField={vi.fn()}
|
|
223
|
+
onRemoveTextField={vi.fn()}
|
|
224
|
+
/>,
|
|
225
|
+
);
|
|
226
|
+
const alignButtons = segmentedRowButtons(host, "Align");
|
|
227
|
+
const rightButton = alignButtons.find((button) => button.textContent === "R");
|
|
228
|
+
expect(rightButton).not.toBeUndefined();
|
|
229
|
+
expect(rightButton?.className).toContain("border-panel-accent");
|
|
230
|
+
// Clicking the option that's already visually active for "end" must NOT
|
|
231
|
+
// rewrite it to the physical "right" — that would destroy the logical
|
|
232
|
+
// semantics and break RTL content, where "end" and "right" differ.
|
|
233
|
+
act(() => rightButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
234
|
+
expect(onSetTextFieldStyle).not.toHaveBeenCalled();
|
|
235
|
+
act(() => root.unmount());
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("commits a genuine align change away from a logical value", () => {
|
|
239
|
+
const onSetTextFieldStyle = vi.fn();
|
|
240
|
+
const { host, root } = renderInto(
|
|
241
|
+
<FlatTextSection
|
|
242
|
+
element={makeSingleFieldElement({ computedStyles: { "text-align": "end" } })}
|
|
243
|
+
styles={{}}
|
|
244
|
+
fontAssets={[]}
|
|
245
|
+
onSetText={vi.fn()}
|
|
246
|
+
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
247
|
+
onAddTextField={vi.fn()}
|
|
248
|
+
onRemoveTextField={vi.fn()}
|
|
249
|
+
/>,
|
|
250
|
+
);
|
|
251
|
+
const alignButtons = segmentedRowButtons(host, "Align");
|
|
252
|
+
const centerButton = alignButtons.find((button) => button.textContent === "C");
|
|
253
|
+
expect(centerButton).not.toBeUndefined();
|
|
254
|
+
act(() => centerButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
255
|
+
expect(onSetTextFieldStyle).toHaveBeenCalledWith("a", "text-align", "center");
|
|
256
|
+
act(() => root.unmount());
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("live-commits the Size field on input, without requiring blur/Enter", async () => {
|
|
260
|
+
const onSetTextFieldStyle = vi.fn();
|
|
261
|
+
const { host, root } = renderInto(
|
|
262
|
+
<FlatTextSection
|
|
263
|
+
element={makeSingleFieldElement()}
|
|
264
|
+
styles={{}}
|
|
265
|
+
fontAssets={[]}
|
|
266
|
+
onSetText={vi.fn()}
|
|
267
|
+
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
268
|
+
onAddTextField={vi.fn()}
|
|
269
|
+
onRemoveTextField={vi.fn()}
|
|
270
|
+
/>,
|
|
271
|
+
);
|
|
272
|
+
const sizeLabel = Array.from(host.querySelectorAll("span")).find(
|
|
273
|
+
(el) => el.textContent === "Size",
|
|
274
|
+
);
|
|
275
|
+
const input = sizeLabel?.parentElement?.querySelector<HTMLInputElement>("input");
|
|
276
|
+
if (!input) throw new Error("expected the Size row's input");
|
|
277
|
+
act(() => {
|
|
278
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
279
|
+
window.HTMLInputElement.prototype,
|
|
280
|
+
"value",
|
|
281
|
+
)?.set;
|
|
282
|
+
nativeInputValueSetter?.call(input, "24px");
|
|
283
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
284
|
+
});
|
|
285
|
+
// liveCommit debounces on a 120ms timer — no blur/Enter dispatched here.
|
|
286
|
+
await act(async () => {
|
|
287
|
+
await new Promise((resolve) => setTimeout(resolve, 160));
|
|
288
|
+
});
|
|
289
|
+
expect(onSetTextFieldStyle).toHaveBeenCalledWith("a", "font-size", "24px");
|
|
290
|
+
act(() => root.unmount());
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("FlatTextSection — multi-field", () => {
|
|
295
|
+
it("shows the layer list, switches the active field's rows on selection, and has no doubled heading (this component never renders its own heading — the parent FlatGroup does)", () => {
|
|
296
|
+
const host = document.createElement("div");
|
|
297
|
+
document.body.append(host);
|
|
298
|
+
const root = createRoot(host);
|
|
299
|
+
act(() => {
|
|
300
|
+
root.render(
|
|
301
|
+
<FlatTextSection
|
|
302
|
+
element={makeMultiFieldElement()}
|
|
303
|
+
styles={{}}
|
|
304
|
+
fontAssets={[]}
|
|
305
|
+
onSetText={vi.fn()}
|
|
306
|
+
onSetTextFieldStyle={vi.fn()}
|
|
307
|
+
onAddTextField={vi.fn()}
|
|
308
|
+
onRemoveTextField={vi.fn()}
|
|
309
|
+
/>,
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
expect(host.textContent).toContain("Headline");
|
|
313
|
+
expect(host.textContent).toContain("Subhead");
|
|
314
|
+
// Active field's editor rows are visible (Font/Weight/etc. from FlatTextFieldEditor).
|
|
315
|
+
expect(host.textContent).toContain("Weight");
|
|
316
|
+
// Exactly one "Text layers" micro-label — this component doesn't duplicate its own list.
|
|
317
|
+
const layerLabels = Array.from(host.querySelectorAll("div")).filter(
|
|
318
|
+
(el) => el.textContent === "Text layers",
|
|
319
|
+
);
|
|
320
|
+
expect(layerLabels.length).toBeLessThanOrEqual(1);
|
|
321
|
+
|
|
322
|
+
const rows = host.querySelectorAll('[data-flat-text-layer-row="true"]');
|
|
323
|
+
act(() => rows[1].dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
324
|
+
expect(host.textContent).toContain("Subhead");
|
|
325
|
+
act(() => root.unmount());
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("wires onAdd/onRemove end-to-end: async onAddTextField switches the active field once it appears in props, and the resync effect falls back to the first field when the active one disappears", async () => {
|
|
329
|
+
let addResolved = false;
|
|
330
|
+
|
|
331
|
+
function Harness() {
|
|
332
|
+
const [fields, setFields] = useState<DomEditTextField[]>(makeMultiFieldElement().textFields);
|
|
333
|
+
const element: DomEditSelection = { ...makeMultiFieldElement(), textFields: fields };
|
|
334
|
+
return (
|
|
335
|
+
<FlatTextSection
|
|
336
|
+
element={element}
|
|
337
|
+
styles={{}}
|
|
338
|
+
fontAssets={[]}
|
|
339
|
+
onSetText={vi.fn()}
|
|
340
|
+
onSetTextFieldStyle={vi.fn()}
|
|
341
|
+
onAddTextField={() =>
|
|
342
|
+
Promise.resolve().then(() => {
|
|
343
|
+
addResolved = true;
|
|
344
|
+
setFields((prev) => [
|
|
345
|
+
...prev,
|
|
346
|
+
{
|
|
347
|
+
key: "c",
|
|
348
|
+
label: "Text",
|
|
349
|
+
value: "Third",
|
|
350
|
+
tagName: "div",
|
|
351
|
+
attributes: [],
|
|
352
|
+
inlineStyles: {},
|
|
353
|
+
computedStyles: {},
|
|
354
|
+
source: "self",
|
|
355
|
+
},
|
|
356
|
+
]);
|
|
357
|
+
return "c";
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
onRemoveTextField={(fieldKey: string) =>
|
|
361
|
+
setFields((prev) => prev.filter((field) => field.key !== fieldKey))
|
|
362
|
+
}
|
|
363
|
+
/>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const host = document.createElement("div");
|
|
368
|
+
document.body.append(host);
|
|
369
|
+
const root = createRoot(host);
|
|
370
|
+
act(() => {
|
|
371
|
+
root.render(<Harness />);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
let rows = host.querySelectorAll('[data-flat-text-layer-row="true"]');
|
|
375
|
+
expect(rows).toHaveLength(2);
|
|
376
|
+
expect((rows[0] as HTMLElement).getAttribute("data-active")).toBe("true");
|
|
377
|
+
|
|
378
|
+
const addButton = host.querySelector<HTMLButtonElement>('[data-flat-text-layer-add="true"]');
|
|
379
|
+
await act(async () => {
|
|
380
|
+
addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
381
|
+
await Promise.resolve();
|
|
382
|
+
await Promise.resolve();
|
|
383
|
+
});
|
|
384
|
+
expect(addResolved).toBe(true);
|
|
385
|
+
rows = host.querySelectorAll('[data-flat-text-layer-row="true"]');
|
|
386
|
+
expect(rows).toHaveLength(3);
|
|
387
|
+
expect((rows[2] as HTMLElement).getAttribute("data-active")).toBe("true");
|
|
388
|
+
|
|
389
|
+
// Remove the active field ("c") through the wired onRemoveTextField — the
|
|
390
|
+
// resync useEffect must fall back to the first remaining field ("a")
|
|
391
|
+
// since "c" no longer exists in element.textFields.
|
|
392
|
+
const removeButtons = host.querySelectorAll<HTMLButtonElement>(
|
|
393
|
+
'[data-flat-text-layer-remove="true"]',
|
|
394
|
+
);
|
|
395
|
+
act(() => {
|
|
396
|
+
removeButtons[2].dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
397
|
+
});
|
|
398
|
+
rows = host.querySelectorAll('[data-flat-text-layer-row="true"]');
|
|
399
|
+
expect(rows).toHaveLength(2);
|
|
400
|
+
expect((rows[0] as HTMLElement).getAttribute("data-active")).toBe("true");
|
|
401
|
+
|
|
402
|
+
act(() => root.unmount());
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("auto-focuses the Content textarea when a new text field is added", async () => {
|
|
406
|
+
let addResolved = false;
|
|
407
|
+
|
|
408
|
+
function Harness() {
|
|
409
|
+
const [fields, setFields] = useState<DomEditTextField[]>(makeMultiFieldElement().textFields);
|
|
410
|
+
const element: DomEditSelection = { ...makeMultiFieldElement(), textFields: fields };
|
|
411
|
+
return (
|
|
412
|
+
<FlatTextSection
|
|
413
|
+
element={element}
|
|
414
|
+
styles={{}}
|
|
415
|
+
fontAssets={[]}
|
|
416
|
+
onSetText={vi.fn()}
|
|
417
|
+
onSetTextFieldStyle={vi.fn()}
|
|
418
|
+
onAddTextField={() =>
|
|
419
|
+
Promise.resolve().then(() => {
|
|
420
|
+
addResolved = true;
|
|
421
|
+
setFields((prev) => [
|
|
422
|
+
...prev,
|
|
423
|
+
{
|
|
424
|
+
key: "c",
|
|
425
|
+
label: "Text",
|
|
426
|
+
value: "",
|
|
427
|
+
tagName: "div",
|
|
428
|
+
attributes: [],
|
|
429
|
+
inlineStyles: {},
|
|
430
|
+
computedStyles: {},
|
|
431
|
+
source: "self",
|
|
432
|
+
},
|
|
433
|
+
]);
|
|
434
|
+
return "c";
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
onRemoveTextField={vi.fn()}
|
|
438
|
+
/>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const host = document.createElement("div");
|
|
443
|
+
document.body.append(host);
|
|
444
|
+
const root = createRoot(host);
|
|
445
|
+
act(() => {
|
|
446
|
+
root.render(<Harness />);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const addButton = host.querySelector<HTMLButtonElement>('[data-flat-text-layer-add="true"]');
|
|
450
|
+
// Wait for onAddTextField's promise to resolve (adds field "c" and makes it
|
|
451
|
+
// active) before checking focus, mirroring the async add-field pattern above.
|
|
452
|
+
await act(async () => {
|
|
453
|
+
addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
454
|
+
await Promise.resolve();
|
|
455
|
+
await Promise.resolve();
|
|
456
|
+
});
|
|
457
|
+
expect(addResolved).toBe(true);
|
|
458
|
+
|
|
459
|
+
const contentTextarea = host.querySelector("textarea");
|
|
460
|
+
expect(contentTextarea).not.toBeNull();
|
|
461
|
+
expect(document.activeElement).toBe(contentTextarea);
|
|
462
|
+
|
|
463
|
+
act(() => root.unmount());
|
|
464
|
+
});
|
|
465
|
+
});
|