@hyperframes/studio 0.7.61 → 0.7.63
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-Xvx2hkrc.js → hyperframes-player-BO2UXx-E.js} +1 -1
- package/dist/assets/{index-D-uFclFj.js → index-C0A0DlLk.js} +1 -1
- package/dist/assets/index-C_7k3kwr.js +426 -0
- package/dist/assets/index-Ceyz8Qt2.css +1 -0
- package/dist/assets/{index-BCpoiv9S.js → index-_72wCKSP.js} +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.html +2 -2
- package/dist/index.js +3504 -2955
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +15 -10
- package/src/components/EditorShell.tsx +12 -0
- package/src/components/StudioLeftSidebar.tsx +3 -0
- package/src/components/editor/DomEditSelectionChrome.test.tsx +51 -0
- package/src/components/editor/PropertyPanelFlat.tsx +6 -0
- package/src/components/editor/compositionReliabilityFixture.integration.test.ts +105 -0
- package/src/components/editor/domEditing.test.ts +34 -0
- package/src/components/editor/propertyPanelColor.test.tsx +31 -0
- package/src/components/editor/propertyPanelColor.tsx +150 -28
- package/src/components/editor/propertyPanelCommitField.tsx +178 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +34 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +3 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +5 -1
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +6 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +47 -4
- package/src/components/editor/propertyPanelFlatTextSection.tsx +8 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +4 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +3 -100
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +37 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +6 -0
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +87 -0
- package/src/components/sidebar/CompositionsTab.tsx +45 -2
- package/src/components/sidebar/LeftSidebar.tsx +3 -0
- package/src/contexts/StudioContext.tsx +4 -0
- package/src/hooks/timelineEditingHelpers.ts +10 -4
- package/src/hooks/usePreviewPersistence.ts +7 -1
- package/src/hooks/useRazorSplit.history.test.tsx +21 -28
- package/src/hooks/useRazorSplit.test.tsx +20 -9
- package/src/hooks/useRazorSplit.testHelpers.ts +34 -22
- package/src/hooks/useRazorSplit.ts +78 -350
- package/src/hooks/useTimelineAddAtPlayhead.ts +15 -0
- package/src/hooks/useTimelineAssetDropOps.ts +52 -2
- package/src/hooks/useTimelineEditing.ts +19 -17
- package/src/hooks/useTimelineGroupEditing.ts +6 -7
- package/src/player/components/Timeline.tsx +12 -1
- package/src/player/components/timelineAuthoredMoveTarget.ts +77 -0
- package/src/player/components/timelineAuthoredTrack.ts +29 -0
- package/src/player/components/timelineCallbacks.ts +4 -0
- package/src/player/components/timelineClipDragCommit.test.ts +176 -0
- package/src/player/components/timelineClipDragCommit.ts +77 -105
- package/src/player/components/timelineClipDragPreview.test.ts +33 -1
- package/src/player/components/timelineClipDragPreview.ts +2 -1
- package/src/player/components/timelineCollision.test.ts +18 -0
- package/src/player/components/timelineCollision.ts +12 -9
- package/src/player/components/timelineDragDrop.ts +28 -7
- package/src/player/components/timelineEditCapabilities.ts +4 -1
- package/src/player/components/timelineGroupEditing.test.ts +10 -0
- package/src/player/components/timelineGroupEditing.ts +4 -3
- package/src/player/components/timelineLaneMoveRefresh.ts +14 -0
- package/src/player/components/timelineLayout.ts +3 -1
- package/src/player/components/timelineZMirror.ts +1 -1
- package/src/player/components/useTimelineClipDrag.ts +5 -0
- package/src/player/components/useTimelineEditPinning.ts +12 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +42 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/lib/playbackTypes.ts +2 -0
- package/src/player/lib/timelineDOM.test.ts +58 -0
- package/src/player/lib/timelineDOM.ts +18 -1
- package/src/player/lib/timelineElementHelpers.ts +21 -11
- package/src/player/store/playerStore.ts +2 -1
- package/src/utils/razorSplitTransaction.test.ts +175 -0
- package/src/utils/razorSplitTransaction.ts +200 -0
- package/src/utils/studioPreviewHelpers.test.ts +27 -0
- package/src/utils/timelineCompositionDrop.test.ts +12 -0
- package/src/utils/timelineCompositionDrop.ts +16 -0
- package/src/utils/timelineCompositionInsert.test.ts +142 -0
- package/src/utils/timelineCompositionInsert.ts +90 -0
- package/src/utils/timelineElementSplit.test.ts +14 -2
- package/src/utils/timelineElementSplit.ts +5 -1
- package/dist/assets/index-2mxh_HSy.js +0 -426
- package/dist/assets/index-BhWig0mx.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.63",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"gsap": "^3.13.0",
|
|
47
47
|
"marked": "^14.1.4",
|
|
48
48
|
"mediabunny": "^1.45.3",
|
|
49
|
-
"@hyperframes/core": "0.7.
|
|
50
|
-
"@hyperframes/parsers": "0.7.
|
|
51
|
-
"@hyperframes/player": "0.7.
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/
|
|
49
|
+
"@hyperframes/core": "0.7.63",
|
|
50
|
+
"@hyperframes/parsers": "0.7.63",
|
|
51
|
+
"@hyperframes/player": "0.7.63",
|
|
52
|
+
"@hyperframes/studio-server": "0.7.63",
|
|
53
|
+
"@hyperframes/sdk": "0.7.63"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/react": "19",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^6.4.2",
|
|
66
66
|
"vitest": "^3.2.4",
|
|
67
67
|
"zustand": "^5.0.0",
|
|
68
|
-
"@hyperframes/producer": "0.7.
|
|
68
|
+
"@hyperframes/producer": "0.7.63"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -23,7 +23,6 @@ import { useDomEditSession } from "./hooks/useDomEditSession";
|
|
|
23
23
|
import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync";
|
|
24
24
|
import { useStudioSdkSessions } from "./hooks/useStudioSdkSessions";
|
|
25
25
|
import { useBlockHandlers } from "./hooks/useBlockHandlers";
|
|
26
|
-
import { useAddAssetAtPlayhead } from "./hooks/useAddAssetAtPlayhead";
|
|
27
26
|
import { useAppHotkeys } from "./hooks/useAppHotkeys";
|
|
28
27
|
import { useClipboard } from "./hooks/useClipboard";
|
|
29
28
|
import { deleteSelectedKeyframes } from "./hooks/timelineEditingHelpers";
|
|
@@ -58,6 +57,7 @@ import { FileManagerProvider } from "./contexts/FileManagerContext";
|
|
|
58
57
|
import { DomEditProvider } from "./contexts/DomEditContext";
|
|
59
58
|
import { StudioSplash } from "./components/StudioSplash";
|
|
60
59
|
import { useServerConnection } from "./hooks/useServerConnection";
|
|
60
|
+
import { useTimelineAddAtPlayhead } from "./hooks/useTimelineAddAtPlayhead";
|
|
61
61
|
import {
|
|
62
62
|
normalizeStudioCompositionPath,
|
|
63
63
|
readStudioUrlStateFromWindow,
|
|
@@ -65,7 +65,6 @@ import {
|
|
|
65
65
|
} from "./utils/studioUrlState";
|
|
66
66
|
import { trackStudioSessionStart } from "./telemetry/events";
|
|
67
67
|
import { hasFiredSessionStart, markSessionStartFired } from "./telemetry/config";
|
|
68
|
-
type CanvasRect = { left: number; top: number; width: number; height: number };
|
|
69
68
|
// fallow-ignore-next-line complexity
|
|
70
69
|
export function StudioApp() {
|
|
71
70
|
const { projectId, resolving, waitingForServer } = useServerConnection();
|
|
@@ -100,9 +99,6 @@ export function StudioApp() {
|
|
|
100
99
|
const timelineDuration = usePlayerStore((s) => s.duration);
|
|
101
100
|
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
102
101
|
const isMasterView = !activeCompPath || activeCompPath === "index.html";
|
|
103
|
-
const activePreviewUrl = activeCompPath
|
|
104
|
-
? `/api/projects/${projectId}/preview/comp/${activeCompPath}`
|
|
105
|
-
: null;
|
|
106
102
|
const effectiveTimelineDuration = useMemo(() => {
|
|
107
103
|
const maxEnd =
|
|
108
104
|
timelineElements.length > 0
|
|
@@ -196,7 +192,13 @@ export function StudioApp() {
|
|
|
196
192
|
},
|
|
197
193
|
[timelineEditing.handleTimelineGroupMove],
|
|
198
194
|
);
|
|
199
|
-
const
|
|
195
|
+
const {
|
|
196
|
+
addAssetAtPlayhead: handleAddAssetAtPlayhead,
|
|
197
|
+
addCompositionAtPlayhead: handleAddCompositionAtPlayhead,
|
|
198
|
+
} = useTimelineAddAtPlayhead(
|
|
199
|
+
timelineEditing.handleTimelineAssetDrop,
|
|
200
|
+
timelineEditing.handleTimelineCompositionDrop,
|
|
201
|
+
);
|
|
200
202
|
const {
|
|
201
203
|
activeBlockParams,
|
|
202
204
|
setActiveBlockParams,
|
|
@@ -338,7 +340,9 @@ export function StudioApp() {
|
|
|
338
340
|
const renderClipContent = useRenderClipContent({
|
|
339
341
|
projectIdRef: fileManager.projectIdRef,
|
|
340
342
|
compIdToSrc,
|
|
341
|
-
activePreviewUrl
|
|
343
|
+
activePreviewUrl: activeCompPath
|
|
344
|
+
? `/api/projects/${projectId}/preview/comp/${activeCompPath}`
|
|
345
|
+
: null,
|
|
342
346
|
effectiveTimelineDuration,
|
|
343
347
|
});
|
|
344
348
|
const compositionDimensions = useCompositionDimensions();
|
|
@@ -369,14 +373,13 @@ export function StudioApp() {
|
|
|
369
373
|
});
|
|
370
374
|
handleToggleRecordingRef.current = handleToggleRecording;
|
|
371
375
|
const recordingToggle = STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : undefined;
|
|
372
|
-
const canvasRectRef = useRef<
|
|
376
|
+
const canvasRectRef = useRef<DOMRect | null>(null);
|
|
373
377
|
useLayoutEffect(() => {
|
|
374
378
|
if (gestureState !== "recording" || !previewIframe) {
|
|
375
379
|
canvasRectRef.current = null;
|
|
376
380
|
return;
|
|
377
381
|
}
|
|
378
|
-
|
|
379
|
-
canvasRectRef.current = { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
382
|
+
canvasRectRef.current = previewIframe.getBoundingClientRect();
|
|
380
383
|
}, [gestureState, previewIframe]);
|
|
381
384
|
const handlePreviewIframeRef = useCallback(
|
|
382
385
|
(iframe: HTMLIFrameElement | null) => {
|
|
@@ -511,6 +514,7 @@ export function StudioApp() {
|
|
|
511
514
|
lintFindingCount={lintModal?.length ?? findingsByFile.size}
|
|
512
515
|
lintFindingsByFile={findingsByFile}
|
|
513
516
|
onAddAssetToTimeline={handleAddAssetAtPlayhead}
|
|
517
|
+
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
|
|
514
518
|
/>
|
|
515
519
|
}
|
|
516
520
|
right={
|
|
@@ -540,6 +544,7 @@ export function StudioApp() {
|
|
|
540
544
|
handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
|
|
541
545
|
handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
|
|
542
546
|
handleTimelineBlockDrop={handleTimelineBlockDrop}
|
|
547
|
+
handleTimelineCompositionDrop={timelineEditing.handleTimelineCompositionDrop}
|
|
543
548
|
handlePreviewBlockDrop={handlePreviewBlockDrop}
|
|
544
549
|
handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
|
|
545
550
|
handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
|
|
@@ -42,6 +42,10 @@ export interface EditorShellProps extends TimelineEditCallbackDeps {
|
|
|
42
42
|
blockName: string,
|
|
43
43
|
placement: TimelineDropPlacement,
|
|
44
44
|
) => Promise<void> | void;
|
|
45
|
+
handleTimelineCompositionDrop?: (
|
|
46
|
+
sourcePath: string,
|
|
47
|
+
placement: TimelineDropPlacement,
|
|
48
|
+
) => Promise<void> | void;
|
|
45
49
|
handlePreviewBlockDrop?: (
|
|
46
50
|
blockName: string,
|
|
47
51
|
position: { left: number; top: number },
|
|
@@ -72,6 +76,7 @@ export function EditorShell({
|
|
|
72
76
|
handleTimelineElementDelete,
|
|
73
77
|
handleTimelineAssetDrop,
|
|
74
78
|
handleTimelineBlockDrop,
|
|
79
|
+
handleTimelineCompositionDrop,
|
|
75
80
|
handlePreviewBlockDrop,
|
|
76
81
|
handleTimelineFileDrop,
|
|
77
82
|
handleTimelineElementMove,
|
|
@@ -140,6 +145,7 @@ export function EditorShell({
|
|
|
140
145
|
onFileDrop={handleTimelineFileDrop}
|
|
141
146
|
onAssetDrop={handleTimelineAssetDrop}
|
|
142
147
|
onBlockDrop={handleTimelineBlockDrop}
|
|
148
|
+
onCompositionDrop={handleTimelineCompositionDrop}
|
|
143
149
|
onDeleteElement={handleTimelineElementDelete}
|
|
144
150
|
previewOverlay={
|
|
145
151
|
<PreviewOverlays
|
|
@@ -174,6 +180,10 @@ interface EditorShellBodyProps {
|
|
|
174
180
|
onFileDrop: (files: File[], placement?: TimelineDropPlacement) => Promise<void> | void;
|
|
175
181
|
onAssetDrop: (assetPath: string, placement: TimelineDropPlacement) => Promise<void> | void;
|
|
176
182
|
onBlockDrop?: (blockName: string, placement: TimelineDropPlacement) => Promise<void> | void;
|
|
183
|
+
onCompositionDrop?: (
|
|
184
|
+
sourcePath: string,
|
|
185
|
+
placement: TimelineDropPlacement,
|
|
186
|
+
) => Promise<void> | void;
|
|
177
187
|
onDeleteElement: (element: TimelineElement) => Promise<void> | void;
|
|
178
188
|
}
|
|
179
189
|
|
|
@@ -189,6 +199,7 @@ function EditorShellBody({
|
|
|
189
199
|
onFileDrop,
|
|
190
200
|
onAssetDrop,
|
|
191
201
|
onBlockDrop,
|
|
202
|
+
onCompositionDrop,
|
|
192
203
|
onDeleteElement,
|
|
193
204
|
}: EditorShellBodyProps) {
|
|
194
205
|
const { compositionStack, updateCompositionStack, containerRef } = useNLEContext();
|
|
@@ -233,6 +244,7 @@ function EditorShellBody({
|
|
|
233
244
|
onFileDrop={onFileDrop}
|
|
234
245
|
onAssetDrop={onAssetDrop}
|
|
235
246
|
onBlockDrop={onBlockDrop}
|
|
247
|
+
onCompositionDrop={onCompositionDrop}
|
|
236
248
|
onDeleteElement={onDeleteElement}
|
|
237
249
|
onSelectTimelineElement={onSelectTimelineElement}
|
|
238
250
|
timelineFooter={
|
|
@@ -19,6 +19,7 @@ export interface StudioLeftSidebarProps {
|
|
|
19
19
|
lintFindingCount?: number;
|
|
20
20
|
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
21
21
|
onAddAssetToTimeline?: (path: string) => void;
|
|
22
|
+
onAddCompositionToTimeline?: (path: string) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
// fallow-ignore-next-line complexity
|
|
@@ -32,6 +33,7 @@ export function StudioLeftSidebar({
|
|
|
32
33
|
lintFindingCount,
|
|
33
34
|
lintFindingsByFile,
|
|
34
35
|
onAddAssetToTimeline,
|
|
36
|
+
onAddCompositionToTimeline,
|
|
35
37
|
}: StudioLeftSidebarProps) {
|
|
36
38
|
const {
|
|
37
39
|
leftCollapsed,
|
|
@@ -150,6 +152,7 @@ export function StudioLeftSidebar({
|
|
|
150
152
|
onAddBlock={onAddBlock}
|
|
151
153
|
onPreviewBlock={onPreviewBlock}
|
|
152
154
|
onAddAssetToTimeline={onAddAssetToTimeline}
|
|
155
|
+
onAddCompositionToTimeline={onAddCompositionToTimeline}
|
|
153
156
|
/>
|
|
154
157
|
{/* Vertical resize divider: 3px visible seam, 8px pointer-capture zone via
|
|
155
158
|
the absolutely-positioned inner hit area. The outer element is w-[3px] so
|
|
@@ -9,6 +9,57 @@ import { DomEditSelectionChrome } from "./DomEditSelectionChrome";
|
|
|
9
9
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
10
|
|
|
11
11
|
describe("DomEditSelectionChrome crop composition", () => {
|
|
12
|
+
it("renders overlay-only transparent chrome at headline geometry without changing composition bytes", () => {
|
|
13
|
+
const composition = document.implementation.createHTMLDocument();
|
|
14
|
+
composition.body.innerHTML = `
|
|
15
|
+
<section class="hl-block"><div class="hl-mask" style="overflow:hidden;background:transparent">
|
|
16
|
+
<h1 class="hl-text">Launch title</h1>
|
|
17
|
+
</div></section>
|
|
18
|
+
`;
|
|
19
|
+
const headline = composition.querySelector<HTMLElement>(".hl-text")!;
|
|
20
|
+
const before = composition.documentElement.outerHTML;
|
|
21
|
+
const selection = {
|
|
22
|
+
element: headline,
|
|
23
|
+
selector: ".hl-text",
|
|
24
|
+
capabilities: {
|
|
25
|
+
canCrop: false,
|
|
26
|
+
canApplyManualOffset: false,
|
|
27
|
+
canApplyManualSize: false,
|
|
28
|
+
canApplyManualRotation: false,
|
|
29
|
+
},
|
|
30
|
+
} as unknown as DomEditSelection;
|
|
31
|
+
const host = document.createElement("div");
|
|
32
|
+
document.body.append(host);
|
|
33
|
+
const root = createRoot(host);
|
|
34
|
+
act(() => {
|
|
35
|
+
root.render(
|
|
36
|
+
<DomEditSelectionChrome
|
|
37
|
+
selection={selection}
|
|
38
|
+
overlayRect={{ left: 44, top: 52, width: 220, height: 48, editScaleX: 1, editScaleY: 1 }}
|
|
39
|
+
allowCanvasMovement={false}
|
|
40
|
+
boxRef={createRef()}
|
|
41
|
+
boxChromeClass="border border-studio-accent/80"
|
|
42
|
+
boxClipPath={undefined}
|
|
43
|
+
selectionKey="headline"
|
|
44
|
+
groupSelectionCount={0}
|
|
45
|
+
blockedMoveRef={createRef()}
|
|
46
|
+
gestures={{ startGesture: vi.fn() } as never}
|
|
47
|
+
onStyleCommit={vi.fn()}
|
|
48
|
+
onBoxMouseDown={vi.fn()}
|
|
49
|
+
onBoxClick={vi.fn()}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
const chrome = host.querySelector<HTMLElement>('[data-dom-edit-selection-box="true"]')!;
|
|
54
|
+
expect(chrome.style.cssText).toContain("left: 44px");
|
|
55
|
+
expect(chrome.style.cssText).toContain("width: 220px");
|
|
56
|
+
expect(chrome.style.background).toBe("");
|
|
57
|
+
expect(chrome.className).not.toMatch(/bg-/);
|
|
58
|
+
expect(composition.documentElement.outerHTML).toBe(before);
|
|
59
|
+
act(() => root.unmount());
|
|
60
|
+
host.remove();
|
|
61
|
+
});
|
|
62
|
+
|
|
12
63
|
it("places rotated crop UI in exactly one oriented coordinate plane", () => {
|
|
13
64
|
const element = document.createElement("div");
|
|
14
65
|
element.id = "clip";
|
|
@@ -78,6 +78,7 @@ export function PropertyPanelFlat({
|
|
|
78
78
|
onClearSelection,
|
|
79
79
|
onUngroup,
|
|
80
80
|
onSetStyle,
|
|
81
|
+
onPreviewStyle,
|
|
81
82
|
onSetAttribute,
|
|
82
83
|
onSetAttributes,
|
|
83
84
|
onSetAttributeLive,
|
|
@@ -86,6 +87,7 @@ export function PropertyPanelFlat({
|
|
|
86
87
|
onRemoveBackground,
|
|
87
88
|
onSetText,
|
|
88
89
|
onSetTextFieldStyle,
|
|
90
|
+
onPreviewTextFieldStyle,
|
|
89
91
|
onAddTextField,
|
|
90
92
|
onRemoveTextField,
|
|
91
93
|
onAskAgent,
|
|
@@ -147,6 +149,7 @@ export function PropertyPanelFlat({
|
|
|
147
149
|
| "onClearSelection"
|
|
148
150
|
| "onUngroup"
|
|
149
151
|
| "onSetStyle"
|
|
152
|
+
| "onPreviewStyle"
|
|
150
153
|
| "onSetAttribute"
|
|
151
154
|
| "onSetAttributes"
|
|
152
155
|
| "onSetAttributeLive"
|
|
@@ -155,6 +158,7 @@ export function PropertyPanelFlat({
|
|
|
155
158
|
| "onRemoveBackground"
|
|
156
159
|
| "onSetText"
|
|
157
160
|
| "onSetTextFieldStyle"
|
|
161
|
+
| "onPreviewTextFieldStyle"
|
|
158
162
|
| "onAddTextField"
|
|
159
163
|
| "onRemoveTextField"
|
|
160
164
|
| "onAskAgent"
|
|
@@ -360,6 +364,7 @@ export function PropertyPanelFlat({
|
|
|
360
364
|
onImportFonts={onImportFonts}
|
|
361
365
|
onSetText={onSetText}
|
|
362
366
|
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
367
|
+
onPreviewTextFieldStyle={onPreviewTextFieldStyle}
|
|
363
368
|
onAddTextField={onAddTextField}
|
|
364
369
|
onRemoveTextField={onRemoveTextField}
|
|
365
370
|
/>
|
|
@@ -382,6 +387,7 @@ export function PropertyPanelFlat({
|
|
|
382
387
|
styles={styles}
|
|
383
388
|
assets={assets}
|
|
384
389
|
onSetStyle={onSetStyle}
|
|
390
|
+
onPreviewStyle={onPreviewStyle}
|
|
385
391
|
onImportAssets={onImportAssets}
|
|
386
392
|
gsapBorderRadius={gsapBorderRadius}
|
|
387
393
|
/>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { patchElementInHtml } from "@hyperframes/studio-server/source-mutation";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { buildDomEditStylePatchOperation } from "./domEditing";
|
|
8
|
+
|
|
9
|
+
const fixtureDir = join(
|
|
10
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
11
|
+
"../../../tests/e2e/fixtures/composition-reliability",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
function fixture(path: string): string {
|
|
15
|
+
return readFileSync(join(fixtureDir, path), "utf8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parse(html: string): Document {
|
|
19
|
+
return new DOMParser().parseFromString(html, "text/html");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inTemplate(document: Document, selector: string): Element | null {
|
|
23
|
+
for (const template of Array.from(document.querySelectorAll("template"))) {
|
|
24
|
+
const match = template.content.querySelector(selector);
|
|
25
|
+
if (match) return match;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("composition reliability acceptance fixture", () => {
|
|
31
|
+
const indexSource = fixture("index.html");
|
|
32
|
+
const titleSource = fixture("compositions/title-card.html");
|
|
33
|
+
const nestedSource = fixture("compositions/nested-shell.html");
|
|
34
|
+
|
|
35
|
+
it("owns repeated root hosts, a nested host, transparent headline topology, and collisions", () => {
|
|
36
|
+
const index = parse(indexSource);
|
|
37
|
+
const repeated = Array.from(
|
|
38
|
+
index.querySelectorAll('[data-composition-src="compositions/title-card.html"]'),
|
|
39
|
+
);
|
|
40
|
+
expect(repeated).toHaveLength(2);
|
|
41
|
+
expect(repeated.map((host) => host.getAttribute("data-start"))).toEqual(["0", "4"]);
|
|
42
|
+
expect(
|
|
43
|
+
index.querySelector('[data-composition-src="compositions/nested-shell.html"]'),
|
|
44
|
+
).toBeTruthy();
|
|
45
|
+
|
|
46
|
+
const nested = parse(nestedSource);
|
|
47
|
+
const nestedHost = inTemplate(nested, '[data-composition-src="title-card.html"]');
|
|
48
|
+
expect(nestedHost).toBeTruthy();
|
|
49
|
+
const nestedDependency = nestedHost?.getAttribute("data-composition-src");
|
|
50
|
+
expect(
|
|
51
|
+
nestedDependency ? existsSync(resolve(fixtureDir, "compositions", nestedDependency)) : false,
|
|
52
|
+
).toBe(true);
|
|
53
|
+
|
|
54
|
+
const title = parse(titleSource);
|
|
55
|
+
const mask = inTemplate(title, ".hl-mask");
|
|
56
|
+
const headline = inTemplate(title, ".hl-mask > .hl-text");
|
|
57
|
+
expect(mask?.textContent).toContain("Reliable compositions");
|
|
58
|
+
expect(inTemplate(title, "style")?.textContent).toMatch(
|
|
59
|
+
/\.hl-mask\s*\{[^}]*overflow:\s*hidden;[^}]*background:\s*transparent;/,
|
|
60
|
+
);
|
|
61
|
+
expect(headline?.tagName).toBe("H1");
|
|
62
|
+
|
|
63
|
+
const collisionA = index.querySelector('[data-hf-id="collision-a"]')!;
|
|
64
|
+
const collisionB = index.querySelector('[data-hf-id="collision-b"]')!;
|
|
65
|
+
const layered = index.querySelector('[data-hf-id="layer-overlap"]')!;
|
|
66
|
+
expect(collisionA.getAttribute("data-track-index")).toBe(
|
|
67
|
+
collisionB.getAttribute("data-track-index"),
|
|
68
|
+
);
|
|
69
|
+
expect(
|
|
70
|
+
Number(collisionA.getAttribute("data-start")) +
|
|
71
|
+
Number(collisionA.getAttribute("data-duration")),
|
|
72
|
+
).toBe(Number(collisionB.getAttribute("data-start")));
|
|
73
|
+
expect(layered.getAttribute("data-start")).toBe(collisionB.getAttribute("data-start"));
|
|
74
|
+
expect(layered.getAttribute("data-track-index")).not.toBe(
|
|
75
|
+
collisionB.getAttribute("data-track-index"),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("keeps timeline host edits in the root source and headline color in the template source", () => {
|
|
80
|
+
const moved = patchElementInHtml(indexSource, { hfId: "title-host-a" }, [
|
|
81
|
+
{ type: "attribute", property: "start", value: "5" },
|
|
82
|
+
{ type: "attribute", property: "duration", value: "2" },
|
|
83
|
+
]);
|
|
84
|
+
expect(moved.matched).toBe(true);
|
|
85
|
+
expect(moved.html).toContain('data-hf-id="title-host-a"');
|
|
86
|
+
expect(moved.html).toContain('data-start="5"');
|
|
87
|
+
expect(moved.html).toContain('data-duration="2"');
|
|
88
|
+
expect(moved.html).not.toContain('data-hf-id="title-text"');
|
|
89
|
+
expect(titleSource).not.toContain("#12b886");
|
|
90
|
+
|
|
91
|
+
const recolored = patchElementInHtml(titleSource, { hfId: "title-text" }, [
|
|
92
|
+
buildDomEditStylePatchOperation("color", "#12b886"),
|
|
93
|
+
]);
|
|
94
|
+
expect(recolored.matched).toBe(true);
|
|
95
|
+
const recoloredDocument = parse(recolored.html);
|
|
96
|
+
expect(
|
|
97
|
+
inTemplate(recoloredDocument, '[data-hf-id="title-text"]')?.getAttribute("style"),
|
|
98
|
+
).toContain("color: #12b886");
|
|
99
|
+
expect(
|
|
100
|
+
inTemplate(recoloredDocument, '[data-hf-id="title-mask"]')?.getAttribute("style"),
|
|
101
|
+
).toBeNull();
|
|
102
|
+
expect(recolored.html).not.toContain('data-hf-id="title-host-a"');
|
|
103
|
+
expect(indexSource).not.toContain("#12b886");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -739,6 +739,40 @@ describe("resolveDomEditSelection", () => {
|
|
|
739
739
|
expect(selection?.selector).toBe("#copy");
|
|
740
740
|
});
|
|
741
741
|
|
|
742
|
+
it("keeps a transparent overflow mask structural when directly selecting its headline", async () => {
|
|
743
|
+
const document = createDocument(`
|
|
744
|
+
<template id="source-template"></template>
|
|
745
|
+
<section class="hl-block">
|
|
746
|
+
<div class="hl-mask" style="overflow: hidden; background: transparent">
|
|
747
|
+
<h1 class="hl-text">Launch title</h1>
|
|
748
|
+
</div>
|
|
749
|
+
</section>
|
|
750
|
+
`);
|
|
751
|
+
const headline = document.querySelector<HTMLElement>(".hl-text")!;
|
|
752
|
+
setElementRect(headline, { left: 44, top: 52, width: 220, height: 48 });
|
|
753
|
+
const selection = await resolveDomEditSelection(headline, {
|
|
754
|
+
activeCompositionPath: "index.html",
|
|
755
|
+
isMasterView: false,
|
|
756
|
+
preferClipAncestor: false,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
expect(selection?.element).toBe(headline);
|
|
760
|
+
expect(selection?.selector).toBe(".hl-text");
|
|
761
|
+
expect(selection?.textFields).toMatchObject([{ source: "self", tagName: "h1" }]);
|
|
762
|
+
expect(selection?.boundingBox).toEqual({ x: 44, y: 52, width: 220, height: 48 });
|
|
763
|
+
// Explicit layer navigation remains free to resolve the structural mask.
|
|
764
|
+
const mask = document.querySelector<HTMLElement>(".hl-mask")!;
|
|
765
|
+
expect(
|
|
766
|
+
(
|
|
767
|
+
await resolveDomEditSelection(mask, {
|
|
768
|
+
activeCompositionPath: "index.html",
|
|
769
|
+
isMasterView: false,
|
|
770
|
+
preferClipAncestor: false,
|
|
771
|
+
})
|
|
772
|
+
)?.element,
|
|
773
|
+
).toBe(mask);
|
|
774
|
+
});
|
|
775
|
+
|
|
742
776
|
// fallow-ignore-next-line code-duplication
|
|
743
777
|
it("collects simple child text blocks as separate editable fields", async () => {
|
|
744
778
|
const document = createDocument(`
|
|
@@ -11,6 +11,15 @@ afterEach(() => {
|
|
|
11
11
|
document.body.innerHTML = "";
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
function renderColorField(onCommit: (value: string) => void): void {
|
|
15
|
+
const host = document.createElement("div");
|
|
16
|
+
document.body.append(host);
|
|
17
|
+
const root = createRoot(host);
|
|
18
|
+
act(() => {
|
|
19
|
+
root.render(<ColorField flat label="Color" value="rgb(255, 176, 32)" onCommit={onCommit} />);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
describe("ColorField flat trigger", () => {
|
|
15
24
|
it("renders label and value inline with a small swatch, no boxed border", () => {
|
|
16
25
|
const host = document.createElement("div");
|
|
@@ -25,4 +34,26 @@ describe("ColorField flat trigger", () => {
|
|
|
25
34
|
expect(host.textContent).toContain("Color");
|
|
26
35
|
act(() => root.unmount());
|
|
27
36
|
});
|
|
37
|
+
|
|
38
|
+
it("persists one keyboard slider gesture on keyup", () => {
|
|
39
|
+
const onCommit = vi.fn();
|
|
40
|
+
renderColorField(onCommit);
|
|
41
|
+
const trigger = document.querySelector<HTMLButtonElement>('[data-flat-color-trigger="true"]');
|
|
42
|
+
if (!trigger) throw new Error("Color trigger was not rendered");
|
|
43
|
+
act(() => {
|
|
44
|
+
trigger.click();
|
|
45
|
+
});
|
|
46
|
+
const hue = document.querySelector<HTMLElement>('[role="slider"][aria-label="Hue"]');
|
|
47
|
+
if (!hue) throw new Error("Hue slider was not rendered");
|
|
48
|
+
|
|
49
|
+
act(() => {
|
|
50
|
+
hue.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "ArrowRight" }));
|
|
51
|
+
});
|
|
52
|
+
expect(onCommit).not.toHaveBeenCalled();
|
|
53
|
+
act(() => {
|
|
54
|
+
hue.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "ArrowRight" }));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(onCommit).toHaveBeenCalledOnce();
|
|
58
|
+
});
|
|
28
59
|
});
|