@hyperframes/studio 0.7.37 → 0.7.39
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-BBrKzTGd.js +459 -0
- package/dist/assets/index-0P10SwC_.css +1 -0
- package/dist/assets/{index-BXYJbIax.js → index-312a3Ceu.js} +1 -1
- package/dist/assets/index-B9YvRJz1.js +396 -0
- package/dist/assets/{index-Ct-X2wlf.js → index-CQHiecE7.js} +1 -1
- package/dist/{chunk-JND3XUJL.js → chunk-RCLGSZ7C.js} +1 -1
- package/dist/chunk-RCLGSZ7C.js.map +1 -0
- package/dist/{domEditingLayers-UIQZJCOA.js → domEditingLayers-S6YOLVRG.js} +2 -2
- package/dist/index.d.ts +48 -10
- package/dist/index.html +2 -2
- package/dist/index.js +6045 -3507
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -24
- package/src/components/AskAgentModal.tsx +23 -6
- package/src/components/LintModal.tsx +40 -10
- package/src/components/MediaPreview.tsx +36 -1
- package/src/components/SaveQueuePausedBanner.tsx +7 -6
- package/src/components/StudioErrorBoundary.tsx +19 -31
- package/src/components/StudioFeedbackBar.tsx +11 -2
- package/src/components/StudioGlobalDragOverlay.tsx +1 -1
- package/src/components/StudioHeader.tsx +184 -106
- package/src/components/StudioLeftSidebar.tsx +24 -3
- package/src/components/StudioOverlays.tsx +34 -8
- package/src/components/StudioPreviewArea.tsx +8 -0
- package/src/components/StudioRightPanel.tsx +160 -9
- package/src/components/StudioSplash.tsx +6 -3
- package/src/components/StudioToast.tsx +12 -11
- package/src/components/TimelineToolbar.test.tsx +9 -3
- package/src/components/TimelineToolbar.tsx +63 -29
- package/src/components/editor/DomEditCropHandles.tsx +238 -0
- package/src/components/editor/DomEditOverlay.tsx +111 -108
- package/src/components/editor/DomEditRotateHandle.tsx +41 -0
- package/src/components/editor/PropertyPanel.test.ts +48 -0
- package/src/components/editor/PropertyPanel.tsx +27 -47
- package/src/components/editor/PropertyPanelEmptyState.tsx +33 -0
- package/src/components/editor/SnapToolbar.tsx +20 -1
- package/src/components/editor/clipPathHelpers.ts +113 -0
- package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
- package/src/components/editor/colorGradingScopePatch.ts +57 -0
- package/src/components/editor/domEditOverlayCrop.test.ts +106 -0
- package/src/components/editor/domEditOverlayCrop.ts +115 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayShape.ts +39 -0
- package/src/components/editor/domEditing.test.ts +2 -0
- package/src/components/editor/domEditingTypes.ts +3 -0
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +0 -6
- package/src/components/editor/marqueeCommit.ts +3 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
- package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
- package/src/components/editor/propertyPanelHelpers.ts +32 -125
- package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
- package/src/components/editor/propertyPanelStyleSections.tsx +75 -1
- package/src/components/editor/propertyPanelTypes.ts +113 -0
- package/src/components/editor/snapTargetCollection.ts +2 -3
- package/src/components/editor/useDomEditCompositionRect.ts +68 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +13 -6
- package/src/components/editor/useDomEditOverlayRects.ts +5 -3
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
- package/src/components/nle/NLELayout.tsx +54 -51
- package/src/components/nle/NLEPreview.tsx +41 -5
- package/src/components/nle/TimelineResizeDivider.tsx +97 -0
- package/src/components/nle/usePreviewBlockDrop.ts +31 -7
- package/src/components/renders/RenderQueue.tsx +125 -24
- package/src/components/renders/RenderQueueItem.tsx +132 -64
- package/src/components/renders/useRenderQueue.ts +136 -24
- package/src/components/sidebar/AssetsTab.test.ts +87 -0
- package/src/components/sidebar/AssetsTab.tsx +113 -15
- package/src/components/sidebar/BlocksTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +91 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
- package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
- package/src/components/storyboard/StoryboardView.tsx +6 -0
- package/src/components/studioColorGradingScope.ts +124 -0
- package/src/components/studioMediaJobs.ts +123 -0
- package/src/components/ui/Button.tsx +20 -11
- package/src/components/ui/HyperframesLoader.tsx +9 -2
- package/src/components/ui/SearchInput.tsx +53 -0
- package/src/components/ui/Tooltip.tsx +52 -6
- package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
- package/src/components/ui/useDialogBehavior.ts +83 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +7 -0
- package/src/hooks/domSelectionTestHarness.ts +1 -0
- package/src/hooks/useCompositionContentLoader.ts +40 -0
- package/src/hooks/useCropMode.ts +91 -0
- package/src/hooks/useDomEditCommits.test.tsx +2 -0
- package/src/hooks/useEditorSave.ts +14 -0
- package/src/hooks/useFileManager.ts +39 -25
- package/src/hooks/useFrameCapture.ts +12 -1
- package/src/hooks/usePanelLayout.ts +1 -0
- package/src/hooks/usePreviewPersistence.ts +24 -15
- package/src/hooks/useStudioContextValue.ts +5 -0
- package/src/hooks/useToast.ts +66 -13
- package/src/player/components/ShortcutsPanel.tsx +9 -0
- package/src/player/store/playerStore.ts +14 -0
- package/src/styles/studio.css +61 -0
- package/src/utils/sdkResolverShadow.test.ts +53 -3
- package/src/utils/sdkResolverShadow.ts +41 -1
- package/src/utils/studioPendingEdits.test.ts +43 -0
- package/src/utils/studioPendingEdits.ts +45 -0
- package/src/utils/studioUiPreferences.ts +4 -0
- package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
- package/dist/assets/index-DmkOvZns.css +0 -1
- package/dist/assets/index-SYSbQyK4.js +0 -396
- package/dist/chunk-JND3XUJL.js.map +0 -1
- /package/dist/{domEditingLayers-UIQZJCOA.js.map → domEditingLayers-S6YOLVRG.js.map} +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { parseInsetClipPathSides, type ClipPathInsetSides } from "./clipPathHelpers";
|
|
2
|
+
|
|
3
|
+
export type CropEdge = "top" | "right" | "bottom" | "left";
|
|
4
|
+
|
|
5
|
+
export interface CropScreenRect {
|
|
6
|
+
left: number;
|
|
7
|
+
top: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Element-space insets → the cropped region in overlay (screen) space. */
|
|
13
|
+
export function cropRectFromInsets(
|
|
14
|
+
rect: CropScreenRect,
|
|
15
|
+
insets: ClipPathInsetSides,
|
|
16
|
+
scaleX: number,
|
|
17
|
+
scaleY: number,
|
|
18
|
+
): CropScreenRect {
|
|
19
|
+
const sx = scaleX > 0 ? scaleX : 1;
|
|
20
|
+
const sy = scaleY > 0 ? scaleY : 1;
|
|
21
|
+
const left = rect.left + insets.left * sx;
|
|
22
|
+
const top = rect.top + insets.top * sy;
|
|
23
|
+
return {
|
|
24
|
+
left,
|
|
25
|
+
top,
|
|
26
|
+
width: Math.max(0, rect.width - (insets.left + insets.right) * sx),
|
|
27
|
+
height: Math.max(0, rect.height - (insets.top + insets.bottom) * sy),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Current inset crop of an element (inline first, computed fallback), or zeros. */
|
|
32
|
+
export function readElementCropInsets(element: HTMLElement): ClipPathInsetSides & {
|
|
33
|
+
radius: number;
|
|
34
|
+
} {
|
|
35
|
+
const inline = element.style.getPropertyValue("clip-path").trim();
|
|
36
|
+
const value =
|
|
37
|
+
inline || element.ownerDocument.defaultView?.getComputedStyle(element).clipPath.trim() || "";
|
|
38
|
+
const parsed = parseInsetClipPathSides(value === "none" ? "" : value);
|
|
39
|
+
return parsed ?? { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CropInsetDragInput {
|
|
43
|
+
edge: CropEdge;
|
|
44
|
+
startInsets: ClipPathInsetSides;
|
|
45
|
+
deltaX: number;
|
|
46
|
+
deltaY: number;
|
|
47
|
+
scaleX: number;
|
|
48
|
+
scaleY: number;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clampInset(value: number, max: number): number {
|
|
54
|
+
if (!Number.isFinite(value)) return 0;
|
|
55
|
+
return Math.min(Math.max(0, value), Math.max(0, max));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveCropInsetFromEdgeDrag(input: CropInsetDragInput): ClipPathInsetSides {
|
|
59
|
+
const scaleX = input.scaleX > 0 ? input.scaleX : 1;
|
|
60
|
+
const scaleY = input.scaleY > 0 ? input.scaleY : 1;
|
|
61
|
+
const next = { ...input.startInsets };
|
|
62
|
+
|
|
63
|
+
if (input.edge === "left") {
|
|
64
|
+
next.left = clampInset(
|
|
65
|
+
input.startInsets.left + input.deltaX / scaleX,
|
|
66
|
+
input.width - next.right,
|
|
67
|
+
);
|
|
68
|
+
} else if (input.edge === "right") {
|
|
69
|
+
next.right = clampInset(
|
|
70
|
+
input.startInsets.right - input.deltaX / scaleX,
|
|
71
|
+
input.width - next.left,
|
|
72
|
+
);
|
|
73
|
+
} else if (input.edge === "top") {
|
|
74
|
+
next.top = clampInset(
|
|
75
|
+
input.startInsets.top + input.deltaY / scaleY,
|
|
76
|
+
input.height - next.bottom,
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
next.bottom = clampInset(
|
|
80
|
+
input.startInsets.bottom - input.deltaY / scaleY,
|
|
81
|
+
input.height - next.top,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Drag the whole crop window: both opposing insets shift together, the crop
|
|
89
|
+
* size stays constant, clamped inside the element bounds. */
|
|
90
|
+
export function resolveCropInsetFromMoveDrag(input: {
|
|
91
|
+
startInsets: ClipPathInsetSides;
|
|
92
|
+
deltaX: number;
|
|
93
|
+
deltaY: number;
|
|
94
|
+
scaleX: number;
|
|
95
|
+
scaleY: number;
|
|
96
|
+
}): ClipPathInsetSides {
|
|
97
|
+
const sx = input.scaleX > 0 ? input.scaleX : 1;
|
|
98
|
+
const sy = input.scaleY > 0 ? input.scaleY : 1;
|
|
99
|
+
const totalX = input.startInsets.left + input.startInsets.right;
|
|
100
|
+
const totalY = input.startInsets.top + input.startInsets.bottom;
|
|
101
|
+
const left = Math.min(Math.max(0, input.startInsets.left + input.deltaX / sx), totalX);
|
|
102
|
+
const top = Math.min(Math.max(0, input.startInsets.top + input.deltaY / sy), totalY);
|
|
103
|
+
return { left, right: totalX - left, top, bottom: totalY - top };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Display-only hug: shrink a projected rect by the element's inset crop.
|
|
107
|
+
* For rects nothing writes back to (e.g. the hover ring). */
|
|
108
|
+
export function hugRectForElement(
|
|
109
|
+
rect: CropScreenRect & { editScaleX: number; editScaleY: number },
|
|
110
|
+
element: HTMLElement,
|
|
111
|
+
): CropScreenRect {
|
|
112
|
+
const insets = readElementCropInsets(element);
|
|
113
|
+
if (insets.top <= 0 && insets.right <= 0 && insets.bottom <= 0 && insets.left <= 0) return rect;
|
|
114
|
+
return cropRectFromInsets(rect, insets, rect.editScaleX, rect.editScaleY);
|
|
115
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DomEditSelection, findElementForSelection } from "./domEditing";
|
|
2
2
|
import { isElementVisibleThroughAncestors } from "./domEditingDom";
|
|
3
|
+
import { hugRectForElement } from "./domEditOverlayCrop";
|
|
3
4
|
|
|
4
5
|
export interface OverlayRect {
|
|
5
6
|
left: number;
|
|
@@ -84,6 +85,19 @@ export function resolveDomEditCoordinateScale(input: {
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/** toOverlayRect, then shrunk to the element's visible (inset-cropped) region.
|
|
89
|
+
* For consumers that reason about what's ON SCREEN — snap targets, marquee
|
|
90
|
+
* hit-tests, display outlines. The selection box must keep the full rect
|
|
91
|
+
* (it is the gesture coordinate basis). */
|
|
92
|
+
export function toVisibleOverlayRect(
|
|
93
|
+
overlayEl: HTMLDivElement,
|
|
94
|
+
iframe: HTMLIFrameElement,
|
|
95
|
+
element: HTMLElement,
|
|
96
|
+
): OverlayRect | null {
|
|
97
|
+
const rect = toOverlayRect(overlayEl, iframe, element);
|
|
98
|
+
return rect ? { ...rect, ...hugRectForElement(rect, element) } : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
export function toOverlayRect(
|
|
88
102
|
overlayEl: HTMLDivElement,
|
|
89
103
|
iframe: HTMLIFrameElement,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DomEditSelection } from "./domEditing";
|
|
2
|
+
|
|
3
|
+
export interface DomEditSelectionShapeStyles {
|
|
4
|
+
borderRadius: string | number;
|
|
5
|
+
clipPath?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function readDomEditSelectionShapeStyles(
|
|
9
|
+
selection: DomEditSelection | null,
|
|
10
|
+
): DomEditSelectionShapeStyles {
|
|
11
|
+
const fallback = {
|
|
12
|
+
borderRadius: 8,
|
|
13
|
+
clipPath: undefined,
|
|
14
|
+
};
|
|
15
|
+
if (!selection?.element) return fallback;
|
|
16
|
+
try {
|
|
17
|
+
const tag = selection.element.tagName.toLowerCase();
|
|
18
|
+
if (tag === "svg" || tag === "img" || tag === "video" || tag === "canvas") return fallback;
|
|
19
|
+
const win = selection.element.ownerDocument.defaultView;
|
|
20
|
+
if (!win) return fallback;
|
|
21
|
+
const cs = win.getComputedStyle(selection.element);
|
|
22
|
+
const borderRadius = cs.borderRadius;
|
|
23
|
+
const clipPath = cs.clipPath;
|
|
24
|
+
return {
|
|
25
|
+
borderRadius: borderRadius && borderRadius !== "0px" ? borderRadius : 4,
|
|
26
|
+
clipPath: clipPath && clipPath !== "none" ? clipPath : undefined,
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Selection-box chrome: none when the crop-outline child draws it, inset
|
|
34
|
+
* shadow for clip-mirrored shapes, plain border otherwise. */
|
|
35
|
+
export function resolveBoxChromeClass(hasCropOutline: boolean, boxClipPath?: string): string {
|
|
36
|
+
if (hasCropOutline) return "";
|
|
37
|
+
if (boxClipPath) return "shadow-[inset_0_0_0_2px_rgba(60,230,172,0.6)]";
|
|
38
|
+
return "border border-studio-accent/80 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]";
|
|
39
|
+
}
|
|
@@ -76,6 +76,7 @@ describe("resolveDomEditCapabilities", () => {
|
|
|
76
76
|
canEditStyles: true,
|
|
77
77
|
canMove: true,
|
|
78
78
|
canResize: true,
|
|
79
|
+
canCrop: true,
|
|
79
80
|
canApplyManualOffset: true,
|
|
80
81
|
canApplyManualSize: true,
|
|
81
82
|
canApplyManualRotation: true,
|
|
@@ -424,6 +425,7 @@ describe("resolveDomEditSelection", () => {
|
|
|
424
425
|
canEditStyles: false,
|
|
425
426
|
canMove: true,
|
|
426
427
|
canResize: true,
|
|
428
|
+
canCrop: true,
|
|
427
429
|
canApplyManualOffset: false,
|
|
428
430
|
canApplyManualSize: false,
|
|
429
431
|
canApplyManualRotation: false,
|
|
@@ -50,6 +50,9 @@ export const CURATED_STYLE_PROPERTIES = [
|
|
|
50
50
|
export interface DomEditCapabilities {
|
|
51
51
|
canSelect: boolean;
|
|
52
52
|
canEditStyles: boolean;
|
|
53
|
+
/** Can take a non-destructive `clip-path: inset()` crop — broader than
|
|
54
|
+
* canEditStyles (a sub-composition host is croppable from the parent view). */
|
|
55
|
+
canCrop: boolean;
|
|
53
56
|
/** Directly editable authored left/top style fields. Canvas drag uses manual edits instead. */
|
|
54
57
|
canMove: boolean;
|
|
55
58
|
/** Directly editable authored width/height style fields. Canvas resize uses manual edits instead. */
|
|
@@ -24,18 +24,6 @@ describe("manual editing availability", () => {
|
|
|
24
24
|
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it("keeps color grading off by default", async () => {
|
|
28
|
-
const availability = await loadAvailabilityWithEnv({});
|
|
29
|
-
expect(availability.STUDIO_COLOR_GRADING_ENABLED).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("enables color grading with an explicit env flag", async () => {
|
|
33
|
-
const availability = await loadAvailabilityWithEnv({
|
|
34
|
-
VITE_STUDIO_ENABLE_COLOR_GRADING: "1",
|
|
35
|
-
});
|
|
36
|
-
expect(availability.STUDIO_COLOR_GRADING_ENABLED).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
27
|
it("disables preview selection when the inspector panel flag is explicitly off", async () => {
|
|
40
28
|
const availability = await loadAvailabilityWithEnv({
|
|
41
29
|
VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "0",
|
|
@@ -64,12 +64,6 @@ export const STUDIO_GSAP_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
64
64
|
true,
|
|
65
65
|
);
|
|
66
66
|
|
|
67
|
-
export const STUDIO_COLOR_GRADING_ENABLED = resolveStudioBooleanEnvFlag(
|
|
68
|
-
env,
|
|
69
|
-
["VITE_STUDIO_ENABLE_COLOR_GRADING", "VITE_STUDIO_COLOR_GRADING_ENABLED"],
|
|
70
|
-
false,
|
|
71
|
-
);
|
|
72
|
-
|
|
73
67
|
export const STUDIO_KEYFRAMES_ENABLED = resolveStudioBooleanEnvFlag(
|
|
74
68
|
env,
|
|
75
69
|
["VITE_STUDIO_ENABLE_KEYFRAMES", "VITE_STUDIO_KEYFRAMES_ENABLED"],
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
// fallow-ignore-file code-duplication
|
|
1
2
|
import { useCallback, useRef, useState } from "react";
|
|
2
3
|
import type { DomEditSelection } from "./domEditing";
|
|
3
4
|
import { collectDomEditLayerItems, resolveDomEditSelection } from "./domEditingLayers";
|
|
4
5
|
import { isElementComputedVisible } from "./domEditingElement";
|
|
5
6
|
import { coversComposition } from "../../utils/studioPreviewHelpers";
|
|
6
7
|
import { rectsOverlap, type Rect } from "../../utils/marqueeGeometry";
|
|
7
|
-
import {
|
|
8
|
+
import { toVisibleOverlayRect } from "./domEditOverlayGeometry";
|
|
8
9
|
|
|
9
10
|
interface MarqueeState {
|
|
10
11
|
startX: number;
|
|
@@ -57,7 +58,7 @@ function collectMarqueeHits(
|
|
|
57
58
|
const el = item.element;
|
|
58
59
|
if (!isElementComputedVisible(el)) continue;
|
|
59
60
|
if (coversComposition(el.getBoundingClientRect(), viewport)) continue;
|
|
60
|
-
const overlayRect =
|
|
61
|
+
const overlayRect = toVisibleOverlayRect(overlayEl, iframe, el);
|
|
61
62
|
if (!overlayRect) continue;
|
|
62
63
|
const r: Rect = {
|
|
63
64
|
left: overlayRect.left,
|