@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.
Files changed (109) hide show
  1. package/dist/assets/hyperframes-player-BBrKzTGd.js +459 -0
  2. package/dist/assets/index-0P10SwC_.css +1 -0
  3. package/dist/assets/{index-BXYJbIax.js → index-312a3Ceu.js} +1 -1
  4. package/dist/assets/index-B9YvRJz1.js +396 -0
  5. package/dist/assets/{index-Ct-X2wlf.js → index-CQHiecE7.js} +1 -1
  6. package/dist/{chunk-JND3XUJL.js → chunk-RCLGSZ7C.js} +1 -1
  7. package/dist/chunk-RCLGSZ7C.js.map +1 -0
  8. package/dist/{domEditingLayers-UIQZJCOA.js → domEditingLayers-S6YOLVRG.js} +2 -2
  9. package/dist/index.d.ts +48 -10
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +6045 -3507
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +26 -24
  15. package/src/components/AskAgentModal.tsx +23 -6
  16. package/src/components/LintModal.tsx +40 -10
  17. package/src/components/MediaPreview.tsx +36 -1
  18. package/src/components/SaveQueuePausedBanner.tsx +7 -6
  19. package/src/components/StudioErrorBoundary.tsx +19 -31
  20. package/src/components/StudioFeedbackBar.tsx +11 -2
  21. package/src/components/StudioGlobalDragOverlay.tsx +1 -1
  22. package/src/components/StudioHeader.tsx +184 -106
  23. package/src/components/StudioLeftSidebar.tsx +24 -3
  24. package/src/components/StudioOverlays.tsx +34 -8
  25. package/src/components/StudioPreviewArea.tsx +8 -0
  26. package/src/components/StudioRightPanel.tsx +160 -9
  27. package/src/components/StudioSplash.tsx +6 -3
  28. package/src/components/StudioToast.tsx +12 -11
  29. package/src/components/TimelineToolbar.test.tsx +9 -3
  30. package/src/components/TimelineToolbar.tsx +63 -29
  31. package/src/components/editor/DomEditCropHandles.tsx +238 -0
  32. package/src/components/editor/DomEditOverlay.tsx +111 -108
  33. package/src/components/editor/DomEditRotateHandle.tsx +41 -0
  34. package/src/components/editor/PropertyPanel.test.ts +48 -0
  35. package/src/components/editor/PropertyPanel.tsx +27 -47
  36. package/src/components/editor/PropertyPanelEmptyState.tsx +33 -0
  37. package/src/components/editor/SnapToolbar.tsx +20 -1
  38. package/src/components/editor/clipPathHelpers.ts +113 -0
  39. package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
  40. package/src/components/editor/colorGradingScopePatch.ts +57 -0
  41. package/src/components/editor/domEditOverlayCrop.test.ts +106 -0
  42. package/src/components/editor/domEditOverlayCrop.ts +115 -0
  43. package/src/components/editor/domEditOverlayGeometry.ts +14 -0
  44. package/src/components/editor/domEditOverlayShape.ts +39 -0
  45. package/src/components/editor/domEditing.test.ts +2 -0
  46. package/src/components/editor/domEditingTypes.ts +3 -0
  47. package/src/components/editor/manualEditingAvailability.test.ts +0 -12
  48. package/src/components/editor/manualEditingAvailability.ts +0 -6
  49. package/src/components/editor/marqueeCommit.ts +3 -2
  50. package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
  51. package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
  52. package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
  53. package/src/components/editor/propertyPanelHelpers.ts +32 -125
  54. package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
  55. package/src/components/editor/propertyPanelStyleSections.tsx +75 -1
  56. package/src/components/editor/propertyPanelTypes.ts +113 -0
  57. package/src/components/editor/snapTargetCollection.ts +2 -3
  58. package/src/components/editor/useDomEditCompositionRect.ts +68 -0
  59. package/src/components/editor/useDomEditOverlayGestures.ts +13 -6
  60. package/src/components/editor/useDomEditOverlayRects.ts +5 -3
  61. package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
  62. package/src/components/nle/NLELayout.tsx +54 -51
  63. package/src/components/nle/NLEPreview.tsx +41 -5
  64. package/src/components/nle/TimelineResizeDivider.tsx +97 -0
  65. package/src/components/nle/usePreviewBlockDrop.ts +31 -7
  66. package/src/components/renders/RenderQueue.tsx +125 -24
  67. package/src/components/renders/RenderQueueItem.tsx +132 -64
  68. package/src/components/renders/useRenderQueue.ts +136 -24
  69. package/src/components/sidebar/AssetsTab.test.ts +87 -0
  70. package/src/components/sidebar/AssetsTab.tsx +113 -15
  71. package/src/components/sidebar/BlocksTab.tsx +2 -1
  72. package/src/components/sidebar/GlobalAssetsView.tsx +91 -0
  73. package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
  74. package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
  75. package/src/components/storyboard/StoryboardView.tsx +6 -0
  76. package/src/components/studioColorGradingScope.ts +124 -0
  77. package/src/components/studioMediaJobs.ts +123 -0
  78. package/src/components/ui/Button.tsx +20 -11
  79. package/src/components/ui/HyperframesLoader.tsx +9 -2
  80. package/src/components/ui/SearchInput.tsx +53 -0
  81. package/src/components/ui/Tooltip.tsx +52 -6
  82. package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
  83. package/src/components/ui/useDialogBehavior.ts +83 -0
  84. package/src/contexts/PanelLayoutContext.tsx +3 -0
  85. package/src/contexts/StudioContext.tsx +7 -0
  86. package/src/hooks/domSelectionTestHarness.ts +1 -0
  87. package/src/hooks/useCompositionContentLoader.ts +40 -0
  88. package/src/hooks/useCropMode.ts +91 -0
  89. package/src/hooks/useDomEditCommits.test.tsx +2 -0
  90. package/src/hooks/useEditorSave.ts +14 -0
  91. package/src/hooks/useFileManager.ts +39 -25
  92. package/src/hooks/useFrameCapture.ts +12 -1
  93. package/src/hooks/usePanelLayout.ts +1 -0
  94. package/src/hooks/usePreviewPersistence.ts +24 -15
  95. package/src/hooks/useStudioContextValue.ts +5 -0
  96. package/src/hooks/useToast.ts +66 -13
  97. package/src/player/components/ShortcutsPanel.tsx +9 -0
  98. package/src/player/store/playerStore.ts +14 -0
  99. package/src/styles/studio.css +61 -0
  100. package/src/utils/sdkResolverShadow.test.ts +53 -3
  101. package/src/utils/sdkResolverShadow.ts +41 -1
  102. package/src/utils/studioPendingEdits.test.ts +43 -0
  103. package/src/utils/studioPendingEdits.ts +45 -0
  104. package/src/utils/studioUiPreferences.ts +4 -0
  105. package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
  106. package/dist/assets/index-DmkOvZns.css +0 -1
  107. package/dist/assets/index-SYSbQyK4.js +0 -396
  108. package/dist/chunk-JND3XUJL.js.map +0 -1
  109. /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 { toOverlayRect } from "./domEditOverlayGeometry";
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 = toOverlayRect(overlayEl, iframe, el);
61
+ const overlayRect = toVisibleOverlayRect(overlayEl, iframe, el);
61
62
  if (!overlayRect) continue;
62
63
  const r: Rect = {
63
64
  left: overlayRect.left,