@hyperframes/studio 0.8.49 → 0.8.51
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-CqON2Rzo.js → hyperframes-player-Cn_PpHqP.js} +3 -3
- package/dist/assets/index-C05SNrvY.js +360 -0
- package/dist/assets/{index-mIgSsKqs.js → index-CPS3Cvj8.js} +1 -1
- package/dist/assets/{index-BGovDksE.js → index-Clrb11lj.js} +1 -1
- package/dist/assets/index-DUR9qzQM.css +1 -0
- package/dist/index.d.ts +29 -16
- package/dist/index.html +2 -2
- package/dist/index.js +7283 -8606
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +11 -12
- package/src/App.tsx +132 -143
- package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
- package/src/captions/components/CaptionOverlayUtils.ts +2 -3
- package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
- package/src/captions/components/CaptionTimeline.tsx +1 -1
- package/src/captions/components/shared.tsx +2 -2
- package/src/components/AskAgentModal.tsx +5 -5
- package/src/components/EditorShell.selectionSync.test.tsx +4 -0
- package/src/components/EditorShell.tsx +28 -7
- package/src/components/ExternalFileConflictBanner.tsx +16 -16
- package/src/components/LintModal.tsx +6 -16
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/SaveQueuePausedBanner.tsx +2 -2
- package/src/components/StudioHeader.tsx +2 -62
- package/src/components/StudioLeftSidebar.tsx +7 -4
- package/src/components/StudioOverlays.tsx +2 -6
- package/src/components/StudioRightPanel.tsx +4 -4
- package/src/components/StudioToast.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +7 -7
- package/src/components/dock/Dock.test.tsx +192 -0
- package/src/components/dock/Dock.tsx +264 -0
- package/src/components/dock/dock.css +23 -0
- package/src/components/dock/dockLayout.ts +58 -0
- package/src/components/dock/dockLayoutSchema.ts +66 -0
- package/src/components/dock/dockLayoutStore.ts +91 -0
- package/src/components/dock/panelRegistry.ts +67 -0
- package/src/components/editor/AnimationCard.tsx +4 -4
- package/src/components/editor/AnimationCardParts.tsx +6 -6
- package/src/components/editor/BlockParamsPanel.tsx +4 -4
- package/src/components/editor/BorderRadiusEditor.tsx +1 -1
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +1 -1
- package/src/components/editor/EaseParamFields.tsx +5 -5
- package/src/components/editor/FileTree.tsx +4 -4
- package/src/components/editor/FileTreeIcons.tsx +1 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -13
- package/src/components/editor/InlineTextToolbar.tsx +1 -1
- package/src/components/editor/InspectorHeaderActions.tsx +3 -3
- package/src/components/editor/KeyframeDiamond.tsx +1 -1
- package/src/components/editor/KeyframeEaseList.tsx +1 -1
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/MarqueeOverlay.tsx +1 -1
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PreviewGuides.test.tsx +88 -0
- package/src/components/editor/PreviewGuides.tsx +125 -0
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.test.tsx +21 -0
- package/src/components/editor/SnapToolbar.tsx +27 -3
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/TopologyLens.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +2 -2
- package/src/components/editor/audioFxRevealTarget.ts +1 -0
- package/src/components/editor/domEditing.test.ts +1 -1
- package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
- package/src/components/editor/domEditingAgentPrompt.ts +9 -3
- package/src/components/editor/previewGuidesStore.ts +26 -0
- package/src/components/editor/propertyPanelColor.tsx +11 -11
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
- package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
- package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +1 -1
- package/src/components/editor/propertyPanelFill.tsx +4 -4
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
- package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
- package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +5 -5
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +6 -6
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
- package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
- package/src/components/editor/propertyPanelSections.tsx +8 -8
- package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
- package/src/components/feedback/feedbackTrigger.ts +0 -1
- package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
- package/src/components/nle/NLEPreview.test.ts +13 -0
- package/src/components/nle/NLEPreview.tsx +12 -7
- package/src/components/nle/PreviewOverlays.tsx +6 -6
- package/src/components/nle/PreviewPane.test.tsx +52 -0
- package/src/components/nle/PreviewPane.tsx +4 -2
- package/src/components/nle/TimelinePane.tsx +17 -3
- package/src/components/nle/TimelineResizeDivider.tsx +2 -2
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -3
- package/src/components/panels/SlideshowPanel.tsx +2 -2
- package/src/components/panels/SlideshowSubPanels.tsx +20 -17
- package/src/components/panels/VariablesBindElement.tsx +2 -2
- package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
- package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +3 -3
- package/src/components/panels/VariablesValueControls.tsx +2 -2
- package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
- package/src/components/renders/RenderQueue.test.tsx +7 -0
- package/src/components/renders/RenderQueue.tsx +6 -6
- package/src/components/renders/RenderQueueItem.tsx +6 -6
- package/src/components/sidebar/AssetCard.tsx +8 -8
- package/src/components/sidebar/AssetContextMenu.tsx +8 -8
- package/src/components/sidebar/AssetsTab.tsx +2 -2
- package/src/components/sidebar/AudioRow.tsx +6 -6
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.tsx +5 -5
- package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
- package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
- package/src/components/sidebar/LeftSidebar.tsx +19 -5
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/ui/Button.test.tsx +60 -0
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/SearchInput.tsx +2 -2
- package/src/components/ui/Tooltip.tsx +1 -1
- package/src/hooks/appHotkeysDispatch.ts +11 -0
- package/src/hooks/timelineAudioGroupCreate.ts +2 -2
- package/src/hooks/timelineTrackVisibility.ts +8 -11
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
- package/src/hooks/useAppHotkeys.ts +4 -1
- package/src/hooks/useAskAgentModal.ts +2 -0
- package/src/hooks/useClipboard.test.ts +234 -0
- package/src/hooks/useClipboard.ts +264 -70
- package/src/hooks/useGroupCommits.test.ts +0 -1
- package/src/hooks/useLintModal.test.tsx +65 -0
- package/src/hooks/useLintModal.ts +3 -0
- package/src/hooks/useRazorSplit.history.test.tsx +0 -1
- package/src/hooks/useRazorSplit.test.ts +0 -2
- package/src/hooks/useRazorSplit.test.tsx +0 -1
- package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
- package/src/hooks/useSdkSession.ts +43 -10
- package/src/hooks/useStudioContextValue.ts +5 -19
- package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
- package/src/hooks/useTimelineAssetDropOps.ts +52 -11
- package/src/hooks/useTimelineEditing.test.tsx +0 -1
- package/src/main.tsx +9 -3
- package/src/player/components/AudioWaveform.tsx +1 -1
- package/src/player/components/AutomationSelectionMenu.tsx +2 -2
- package/src/player/components/ClipContextMenu.tsx +90 -10
- package/src/player/components/CompositionThumbnail.test.ts +0 -3
- package/src/player/components/CompositionThumbnail.tsx +3 -4
- package/src/player/components/EditModal.tsx +1 -1
- package/src/player/components/ImageThumbnail.tsx +1 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
- package/src/player/components/LayerDisclosureRow.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +1 -1
- package/src/player/components/ShortcutsPanel.tsx +9 -9
- package/src/player/components/SpeedMenu.tsx +3 -5
- package/src/player/components/Timeline.tsx +16 -15
- package/src/player/components/TimelineCanvas.tsx +45 -8
- package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
- package/src/player/components/TimelineEmptyState.tsx +5 -5
- package/src/player/components/TimelineFxButton.tsx +6 -6
- package/src/player/components/TimelineGestureOverlay.test.tsx +35 -1
- package/src/player/components/TimelineGestureOverlay.tsx +1 -1
- package/src/player/components/TimelineGroupHeader.tsx +5 -5
- package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
- package/src/player/components/TimelineGroupRow.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +54 -1
- package/src/player/components/TimelineLanes.tsx +2 -5
- package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
- package/src/player/components/TimelineOverlays.tsx +12 -0
- package/src/player/components/TimelineRuler.tsx +1 -1
- package/src/player/components/TimelineShortcutHint.tsx +1 -1
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
- package/src/player/components/TimelineTypes.ts +6 -1
- package/src/player/components/TrackGapContextMenu.tsx +1 -1
- package/src/player/components/VideoThumbnail.tsx +4 -2
- package/src/player/components/VolumeControl.tsx +2 -2
- package/src/player/components/timelineAuthoredMoveTarget.ts +1 -2
- package/src/player/components/timelineCallbacks.ts +8 -8
- package/src/player/components/timelineClipChildren.test.ts +2 -2
- package/src/player/components/timelineClipChildren.tsx +2 -1
- package/src/player/components/timelineClipDragCommit.ts +2 -2
- package/src/player/components/timelineClipDragPreview.ts +51 -27
- package/src/player/components/timelineClipDragTypes.ts +3 -0
- package/src/player/components/timelineDragDrop.test.tsx +92 -1
- package/src/player/components/timelineDragDrop.ts +82 -33
- package/src/player/components/timelineEditCapabilities.ts +1 -2
- package/src/player/components/timelineEditing.test.ts +0 -16
- package/src/player/components/timelineGroupEditing.test.ts +1 -3
- package/src/player/components/timelineGroupEditing.ts +0 -1
- package/src/player/components/timelineLaneProps.ts +3 -0
- package/src/player/components/timelineSnapping.test.ts +27 -0
- package/src/player/components/timelineSnapping.ts +14 -1
- package/src/player/components/timelineTrackDisplay.ts +1 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +0 -53
- package/src/player/components/trackHeaderLabelRows.tsx +4 -4
- package/src/player/components/useTimelineClipContextMenu.ts +32 -0
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
- package/src/player/components/useTimelineClipDrag.ts +30 -3
- package/src/player/components/useTimelineTrackDerivations.ts +1 -1
- package/src/player/hooks/timelineDomBinding.test.ts +0 -3
- package/src/player/hooks/timelineSyncHydration.test.ts +18 -0
- package/src/player/hooks/timelineSyncHydration.ts +37 -18
- package/src/player/hooks/useTimelinePlayer.test.ts +3 -20
- package/src/player/hooks/useTimelineRowElements.test.ts +28 -0
- package/src/player/hooks/useTimelineRowElements.ts +17 -0
- package/src/player/hooks/useTimelineSyncCallbacks.emptyManifest.test.tsx +41 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -10
- package/src/player/lib/describeClips.test.ts +95 -0
- package/src/player/lib/describeClips.ts +52 -0
- package/src/player/lib/timelineDOM.test.ts +0 -80
- package/src/player/lib/timelineDOM.ts +1 -61
- package/src/player/lib/timelineElementHelpers.ts +2 -53
- package/src/player/store/playerStore.ts +5 -0
- package/src/player/store/timelineElement.ts +1 -3
- package/src/shims/canvasBrowserStub.js +50 -0
- package/src/styles/iconTokens.ts +20 -0
- package/src/styles/palette-reset.css +6 -0
- package/src/styles/studio.css +24 -21
- package/src/styles/tailwind-preset.shared.js +3 -0
- package/src/styles/theme.css +213 -0
- package/src/styles/theme.test.ts +196 -0
- package/src/utils/clipboardPayload.test.ts +42 -2
- package/src/utils/clipboardPayload.ts +57 -32
- package/src/utils/previewSafeMargins.test.ts +34 -0
- package/src/utils/previewSafeMargins.ts +10 -0
- package/src/utils/razorSplitTransaction.test.ts +0 -1
- package/src/utils/sdkCanvasComposition.test.ts +77 -0
- package/src/utils/studioUiPreferences.test.ts +43 -0
- package/src/utils/studioUiPreferences.ts +34 -6
- package/src/utils/timelineDropTrackInsert.test.ts +134 -0
- package/src/utils/timelineDropTrackInsert.ts +83 -0
- package/src/utils/timelineElementSplit.test.ts +1 -2
- package/src/utils/timelineElementSplit.ts +0 -1
- package/src/webmcp/tools/lookTools.test.ts +40 -0
- package/src/webmcp/tools/lookTools.ts +32 -1
- package/dist/assets/index-B9LfWNyM.css +0 -1
- package/dist/assets/index-Bgmx2h7W.js +0 -441
- package/src/components/StudioGlobalDragOverlay.tsx +0 -24
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
- package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
- package/src/components/storyboard/FramePoster.test.tsx +0 -61
- package/src/components/storyboard/FramePoster.tsx +0 -86
- package/src/components/storyboard/StoryboardDirection.tsx +0 -45
- package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
- package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
- package/src/components/storyboard/StoryboardGrid.tsx +0 -55
- package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
- package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
- package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
- package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
- package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
- package/src/components/storyboard/StoryboardView.tsx +0 -169
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
- package/src/components/storyboard/frameComments.test.ts +0 -95
- package/src/components/storyboard/frameComments.ts +0 -115
- package/src/components/storyboard/frameStatus.ts +0 -36
- package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
- package/src/components/storyboard/storyboardReviewStage.ts +0 -45
- package/src/components/storyboard/useFrameComments.ts +0 -98
- package/src/contexts/ViewModeContext.tsx +0 -142
- package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
- package/src/hooks/useProjectSignaturePoll.ts +0 -68
- package/src/hooks/useStoryboard.ts +0 -99
- package/src/player/hooks/useExpandedTimelineElements.test.ts +0 -774
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -460
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { PreviewGuides } from "./PreviewGuides";
|
|
7
|
+
import { usePreviewGuidesStore } from "./previewGuidesStore";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
const compRect = vi.hoisted(() => ({
|
|
12
|
+
current: { left: 20, top: 20, width: 400, height: 225, scaleX: 1, scaleY: 1 },
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("./useDomEditCompositionRect", () => ({
|
|
15
|
+
useDomEditCompositionRect: () => compRect.current,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
compRect.current = { left: 20, top: 20, width: 400, height: 225, scaleX: 1, scaleY: 1 };
|
|
20
|
+
document.body.innerHTML = "";
|
|
21
|
+
window.localStorage.clear();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function render(rulerVisible: boolean, safeMarginsVisible: boolean) {
|
|
25
|
+
usePreviewGuidesStore.setState({ rulerVisible, safeMarginsVisible });
|
|
26
|
+
const host = document.createElement("div");
|
|
27
|
+
document.body.append(host);
|
|
28
|
+
act(() => {
|
|
29
|
+
createRoot(host).render(<PreviewGuides iframeRef={{ current: null }} />);
|
|
30
|
+
});
|
|
31
|
+
return host;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("PreviewGuides", () => {
|
|
35
|
+
it("draws nothing when both toggles are off", () => {
|
|
36
|
+
const host = render(false, false);
|
|
37
|
+
expect(host.querySelector("[data-testid]")).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("draws the ruler with ticks 0 to 100 every 10 on both edges", () => {
|
|
41
|
+
const host = render(true, false);
|
|
42
|
+
const top = host.querySelector('[data-testid="preview-ruler-top"]');
|
|
43
|
+
expect(Array.from(top?.children ?? []).map((t) => t.textContent)).toEqual(
|
|
44
|
+
Array.from({ length: 11 }, (_, i) => String(i * 10)),
|
|
45
|
+
);
|
|
46
|
+
expect(host.querySelector('[data-testid="preview-ruler-left"]')).not.toBeNull();
|
|
47
|
+
expect(host.querySelector('[data-testid="preview-safe-90"]')).toBeNull();
|
|
48
|
+
expect(host.querySelector('[data-testid="preview-safe-80"]')).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("draws the 90% and 80% boxes with edge-midpoint ticks and no labels or caption band", () => {
|
|
52
|
+
const host = render(false, true);
|
|
53
|
+
const box = (pct: number) =>
|
|
54
|
+
host.querySelector<HTMLElement>(`[data-testid="preview-safe-${pct}"]`);
|
|
55
|
+
expect([box(90)?.style.left, box(90)?.style.bottom]).toEqual(["5%", "5%"]);
|
|
56
|
+
expect([box(80)?.style.top, box(80)?.style.right]).toEqual(["10%", "10%"]);
|
|
57
|
+
expect(box(90)?.children.length).toBe(4);
|
|
58
|
+
expect(box(80)?.children.length).toBe(4);
|
|
59
|
+
expect(host.textContent).toBe("");
|
|
60
|
+
expect(host.querySelector('[data-testid="preview-ruler-top"]')).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("places the ruler in the gutter outside the frame", () => {
|
|
64
|
+
const host = render(true, false);
|
|
65
|
+
const top = host.querySelector<HTMLElement>('[data-testid="preview-ruler-top"]');
|
|
66
|
+
const left = host.querySelector<HTMLElement>('[data-testid="preview-ruler-left"]');
|
|
67
|
+
expect([top?.style.left, top?.style.top, top?.style.width]).toEqual(["20px", "4px", "400px"]);
|
|
68
|
+
expect([left?.style.left, left?.style.top, left?.style.height]).toEqual([
|
|
69
|
+
"4px",
|
|
70
|
+
"20px",
|
|
71
|
+
"225px",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("draws nothing until the composition has a size", () => {
|
|
76
|
+
compRect.current = { left: 0, top: 0, width: 0, height: 0, scaleX: 1, scaleY: 1 };
|
|
77
|
+
const host = render(true, true);
|
|
78
|
+
expect(host.querySelector("[data-testid]")).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("draws the same boxes on a portrait composition", () => {
|
|
82
|
+
compRect.current = { left: 20, top: 20, width: 225, height: 400, scaleX: 1, scaleY: 1 };
|
|
83
|
+
const host = render(false, true);
|
|
84
|
+
const box = host.querySelector<HTMLElement>('[data-testid="preview-safe-90"]');
|
|
85
|
+
expect([box?.style.left, box?.style.top]).toEqual(["5%", "5%"]);
|
|
86
|
+
expect(host.querySelector('[data-testid="preview-safe-80"]')).not.toBeNull();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { memo, useRef, type RefObject } from "react";
|
|
2
|
+
import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
|
|
3
|
+
import { RULER_GUTTER_PX, usePreviewGuidesStore } from "./previewGuidesStore";
|
|
4
|
+
import { SAFE_BOX_PERCENTS, safeBoxInsetPercent } from "../../utils/previewSafeMargins";
|
|
5
|
+
|
|
6
|
+
interface PreviewGuidesProps {
|
|
7
|
+
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TICKS = Array.from({ length: 11 }, (_, i) => i * 10);
|
|
11
|
+
const INK = "rgba(255,255,255,0.7)";
|
|
12
|
+
|
|
13
|
+
/** Ruler and safe-margin boxes drawn over the preview pane, never inside the composition. */
|
|
14
|
+
export const PreviewGuides = memo(function PreviewGuides({ iframeRef }: PreviewGuidesProps) {
|
|
15
|
+
const rulerVisible = usePreviewGuidesStore((s) => s.rulerVisible);
|
|
16
|
+
const safeMarginsVisible = usePreviewGuidesStore((s) => s.safeMarginsVisible);
|
|
17
|
+
if (!rulerVisible && !safeMarginsVisible) return null;
|
|
18
|
+
return (
|
|
19
|
+
<ActiveGuides
|
|
20
|
+
iframeRef={iframeRef}
|
|
21
|
+
rulerVisible={rulerVisible}
|
|
22
|
+
safeMarginsVisible={safeMarginsVisible}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
interface ActiveGuidesProps extends PreviewGuidesProps {
|
|
28
|
+
rulerVisible: boolean;
|
|
29
|
+
safeMarginsVisible: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Owns the composition-rect subscription, so it only runs while a guide is on. */
|
|
33
|
+
function ActiveGuides({ iframeRef, rulerVisible, safeMarginsVisible }: ActiveGuidesProps) {
|
|
34
|
+
const paneRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const rect = useDomEditCompositionRect({ iframeRef, overlayRef: paneRef });
|
|
36
|
+
const ready = rect.width > 0 && rect.height > 0;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div ref={paneRef} aria-hidden="true" className="pointer-events-none absolute inset-0 z-20">
|
|
40
|
+
{ready && rulerVisible && (
|
|
41
|
+
<>
|
|
42
|
+
<div
|
|
43
|
+
data-testid="preview-ruler-top"
|
|
44
|
+
className="absolute bg-black/70"
|
|
45
|
+
style={{
|
|
46
|
+
left: rect.left,
|
|
47
|
+
top: rect.top - RULER_GUTTER_PX,
|
|
48
|
+
width: rect.width,
|
|
49
|
+
height: RULER_GUTTER_PX,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{TICKS.map((pct) => (
|
|
53
|
+
<span
|
|
54
|
+
key={pct}
|
|
55
|
+
className="absolute bottom-0 border-l text-[9px] leading-none tabular-nums pl-0.5 h-2.5"
|
|
56
|
+
style={{ left: `${pct}%`, borderColor: INK, color: INK }}
|
|
57
|
+
>
|
|
58
|
+
{pct}
|
|
59
|
+
</span>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
<div
|
|
63
|
+
data-testid="preview-ruler-left"
|
|
64
|
+
className="absolute bg-black/70"
|
|
65
|
+
style={{
|
|
66
|
+
left: rect.left - RULER_GUTTER_PX,
|
|
67
|
+
top: rect.top,
|
|
68
|
+
width: RULER_GUTTER_PX,
|
|
69
|
+
height: rect.height,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{TICKS.map((pct) => (
|
|
73
|
+
<span
|
|
74
|
+
key={pct}
|
|
75
|
+
className="absolute right-0 border-t text-[9px] leading-none tabular-nums pt-0.5 w-2.5"
|
|
76
|
+
style={{ top: `${pct}%`, borderColor: INK, color: INK }}
|
|
77
|
+
>
|
|
78
|
+
{pct}
|
|
79
|
+
</span>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
{ready && safeMarginsVisible && (
|
|
85
|
+
<div
|
|
86
|
+
className="absolute"
|
|
87
|
+
style={{ left: rect.left, top: rect.top, width: rect.width, height: rect.height }}
|
|
88
|
+
>
|
|
89
|
+
{SAFE_BOX_PERCENTS.map((boxPercent) => (
|
|
90
|
+
<SafeBox key={boxPercent} boxPercent={boxPercent} />
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const TICK_PX = 7;
|
|
99
|
+
const EDGE_MIDPOINTS = [
|
|
100
|
+
{ edge: "top", style: { left: "50%", top: -(TICK_PX + 1) / 2, width: 1, height: TICK_PX } },
|
|
101
|
+
{ edge: "bottom", style: { left: "50%", bottom: -(TICK_PX + 1) / 2, width: 1, height: TICK_PX } },
|
|
102
|
+
{ edge: "left", style: { top: "50%", left: -(TICK_PX + 1) / 2, width: TICK_PX, height: 1 } },
|
|
103
|
+
{ edge: "right", style: { top: "50%", right: -(TICK_PX + 1) / 2, width: TICK_PX, height: 1 } },
|
|
104
|
+
] as const;
|
|
105
|
+
|
|
106
|
+
/** One thin white box inset from every edge, with a tick at the midpoint of each edge. */
|
|
107
|
+
function SafeBox({ boxPercent }: { boxPercent: number }) {
|
|
108
|
+
const inset = `${safeBoxInsetPercent(boxPercent)}%`;
|
|
109
|
+
return (
|
|
110
|
+
<div
|
|
111
|
+
data-testid={`preview-safe-${boxPercent}`}
|
|
112
|
+
className="absolute border border-white/90"
|
|
113
|
+
style={{
|
|
114
|
+
left: inset,
|
|
115
|
+
top: inset,
|
|
116
|
+
right: inset,
|
|
117
|
+
bottom: inset,
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{EDGE_MIDPOINTS.map(({ edge, style }) => (
|
|
121
|
+
<span key={edge} className="absolute bg-white/90" style={style} />
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -83,7 +83,7 @@ export function PromotableControl({
|
|
|
83
83
|
<span
|
|
84
84
|
// Sits above the row (not on top of top-0) so it clears a value that
|
|
85
85
|
// renders flush to the row's right edge, e.g. flat Font/Color rows.
|
|
86
|
-
className="pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
|
|
86
|
+
className="pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded-sm bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
|
|
87
87
|
title={`Bound to variable "${promote.boundId}"`}
|
|
88
88
|
>
|
|
89
89
|
◆ {promote.boundId}
|
|
@@ -97,7 +97,7 @@ export function PromotableControl({
|
|
|
97
97
|
e.stopPropagation();
|
|
98
98
|
promote.promote();
|
|
99
99
|
}}
|
|
100
|
-
className="absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
|
|
100
|
+
className="absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded-sm bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
|
|
101
101
|
>
|
|
102
102
|
◇ var
|
|
103
103
|
</button>
|
|
@@ -18,7 +18,7 @@ function FlatEmptyState() {
|
|
|
18
18
|
</svg>
|
|
19
19
|
</span>
|
|
20
20
|
<div className="text-[13px] font-semibold text-panel-text-0">Nothing selected</div>
|
|
21
|
-
<div className="max-w-[250px] text-[11px] leading-
|
|
21
|
+
<div className="max-w-[250px] text-[11px] leading-normal text-panel-text-3">
|
|
22
22
|
Click any element on the canvas to edit it, or drag to select several.
|
|
23
23
|
</div>
|
|
24
24
|
<div className="mt-2 flex w-full flex-col gap-1.5">
|
|
@@ -27,7 +27,7 @@ function FlatEmptyState() {
|
|
|
27
27
|
<span className="text-panel-danger">●</span>
|
|
28
28
|
Record a gesture
|
|
29
29
|
</span>
|
|
30
|
-
<span className="rounded border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
|
|
30
|
+
<span className="rounded-sm border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
|
|
31
31
|
R
|
|
32
32
|
</span>
|
|
33
33
|
</span>
|
|
@@ -36,7 +36,7 @@ function FlatEmptyState() {
|
|
|
36
36
|
<span className="text-panel-accent">✦</span>
|
|
37
37
|
Describe a change to the agent
|
|
38
38
|
</span>
|
|
39
|
-
<span className="rounded border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
|
|
39
|
+
<span className="rounded-sm border border-panel-border-input px-[5px] py-px font-mono text-[9px] text-panel-text-5">
|
|
40
40
|
⌘K
|
|
41
41
|
</span>
|
|
42
42
|
</span>
|
|
@@ -74,7 +74,7 @@ function FlatMultiSelectState({
|
|
|
74
74
|
return (
|
|
75
75
|
<div className="flex flex-col gap-3 px-4 py-3">
|
|
76
76
|
<div className="flex items-center gap-3 rounded-xl border border-panel-border bg-panel-surface p-3">
|
|
77
|
-
<span className="flex h-8 w-8
|
|
77
|
+
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-panel-accent/10 text-panel-accent">
|
|
78
78
|
<Layers size={16} />
|
|
79
79
|
</span>
|
|
80
80
|
<div className="min-w-0 flex-1">
|
|
@@ -90,7 +90,7 @@ function FlatMultiSelectState({
|
|
|
90
90
|
data-flat-multiselect-clear="true"
|
|
91
91
|
aria-label="Clear selection"
|
|
92
92
|
onClick={onClearSelection}
|
|
93
|
-
className="flex h-[26px] w-[26px]
|
|
93
|
+
className="flex h-[26px] w-[26px] shrink-0 items-center justify-center text-panel-text-3"
|
|
94
94
|
>
|
|
95
95
|
<svg
|
|
96
96
|
width="13"
|
|
@@ -113,14 +113,14 @@ function FlatMultiSelectState({
|
|
|
113
113
|
className="flex items-center gap-2 rounded-lg border border-panel-border bg-panel-bg px-2.5 py-[7px]"
|
|
114
114
|
>
|
|
115
115
|
<span
|
|
116
|
-
className={`flex h-[18px] w-[18px]
|
|
116
|
+
className={`flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-sm text-[9px] font-bold ${className}`}
|
|
117
117
|
>
|
|
118
118
|
{glyph}
|
|
119
119
|
</span>
|
|
120
120
|
<span className="min-w-0 flex-1 truncate text-[11px] text-panel-text-1">
|
|
121
121
|
{element.label}
|
|
122
122
|
</span>
|
|
123
|
-
<span className="
|
|
123
|
+
<span className="shrink-0 font-mono text-[9px] text-panel-text-4">
|
|
124
124
|
{element.id ? `#${element.id}` : element.selector}
|
|
125
125
|
</span>
|
|
126
126
|
</span>
|
|
@@ -41,13 +41,13 @@ export function PropertyPanelFlatHeader({
|
|
|
41
41
|
<Icon
|
|
42
42
|
size={15}
|
|
43
43
|
data-flat-header-icon="true"
|
|
44
|
-
className={`
|
|
44
|
+
className={`shrink-0 ${ICON_COLOR_BY_KIND[elementKind]}`}
|
|
45
45
|
/>
|
|
46
46
|
<div className="flex min-w-0 flex-1 items-baseline gap-2">
|
|
47
47
|
<span className="truncate text-[13px] font-semibold text-panel-text-0">{name}</span>
|
|
48
48
|
<span className="truncate font-mono text-[10px] text-panel-text-4">{meta}</span>
|
|
49
49
|
</div>
|
|
50
|
-
<div className="flex
|
|
50
|
+
<div className="flex shrink-0 items-center gap-2.5 text-panel-text-3">
|
|
51
51
|
{showUngroup && (
|
|
52
52
|
<button
|
|
53
53
|
type="button"
|
|
@@ -8,6 +8,7 @@ import { usePlayerStore } from "../../player/store/playerStore";
|
|
|
8
8
|
import type { LeftSidebarHandle } from "../sidebar/LeftSidebar";
|
|
9
9
|
import type { DomEditSelection } from "./domEditing";
|
|
10
10
|
import { SnapToolbar } from "./SnapToolbar";
|
|
11
|
+
import { usePreviewGuidesStore } from "./previewGuidesStore";
|
|
11
12
|
|
|
12
13
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
14
|
|
|
@@ -53,6 +54,7 @@ function AppHotkeyHarness() {
|
|
|
53
54
|
handleCopy: vi.fn(() => false),
|
|
54
55
|
handlePaste: vi.fn(async () => undefined),
|
|
55
56
|
handleCut: vi.fn(async () => false),
|
|
57
|
+
handleDuplicate: vi.fn(async () => false),
|
|
56
58
|
onResetKeyframes: vi.fn(() => false),
|
|
57
59
|
onDeleteSelectedKeyframes: vi.fn(),
|
|
58
60
|
});
|
|
@@ -119,3 +121,22 @@ describe("SnapToolbar keyboard shortcuts", () => {
|
|
|
119
121
|
act(() => root.unmount());
|
|
120
122
|
});
|
|
121
123
|
});
|
|
124
|
+
|
|
125
|
+
describe("SnapToolbar ruler and safe-margin toggles", () => {
|
|
126
|
+
it.each([
|
|
127
|
+
["Toggle ruler", "rulerVisible"],
|
|
128
|
+
["Toggle safe margins", "safeMarginsVisible"],
|
|
129
|
+
])("%s flips %s and remembers it", (label, key) => {
|
|
130
|
+
usePreviewGuidesStore.setState({ rulerVisible: false, safeMarginsVisible: false });
|
|
131
|
+
const { root } = renderToolbar();
|
|
132
|
+
const button = () => document.querySelector<HTMLButtonElement>(`[aria-label="${label}"]`);
|
|
133
|
+
expect(button()?.getAttribute("aria-pressed")).toBe("false");
|
|
134
|
+
|
|
135
|
+
act(() => button()?.click());
|
|
136
|
+
|
|
137
|
+
expect(button()?.getAttribute("aria-pressed")).toBe("true");
|
|
138
|
+
const stored = window.localStorage.getItem("hf-studio-ui-preferences") ?? "{}";
|
|
139
|
+
expect(JSON.parse(stored)[key]).toBe(true);
|
|
140
|
+
act(() => root.unmount());
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { MagnetStraight, GridFour, Path } from "@phosphor-icons/react";
|
|
2
|
+
import { MagnetStraight, GridFour, Path, Ruler, FrameCorners } from "@phosphor-icons/react";
|
|
3
3
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
4
4
|
import { usePlayerStore } from "../../player/store/playerStore";
|
|
5
|
+
import { usePreviewGuidesStore } from "./previewGuidesStore";
|
|
5
6
|
|
|
6
7
|
const SNAP_DEFAULTS = {
|
|
7
8
|
snapEnabled: true,
|
|
@@ -39,6 +40,7 @@ export const SnapToolbar = memo(function SnapToolbar({ onSnapChange }: SnapToolb
|
|
|
39
40
|
const motionPathCreateAvailable = usePlayerStore((s) => s.motionPathCreateAvailable);
|
|
40
41
|
const motionPathArmed = usePlayerStore((s) => s.motionPathArmed);
|
|
41
42
|
const setMotionPathArmed = usePlayerStore((s) => s.setMotionPathArmed);
|
|
43
|
+
const guides = usePreviewGuidesStore();
|
|
42
44
|
const popoverRef = useRef<HTMLDivElement>(null);
|
|
43
45
|
const gridButtonRef = useRef<HTMLButtonElement>(null);
|
|
44
46
|
|
|
@@ -116,6 +118,28 @@ export const SnapToolbar = memo(function SnapToolbar({ onSnapChange }: SnapToolb
|
|
|
116
118
|
<Path size={16} weight={motionPathArmed ? "fill" : "regular"} />
|
|
117
119
|
</button>
|
|
118
120
|
)}
|
|
121
|
+
{(
|
|
122
|
+
[
|
|
123
|
+
["rulerVisible", "Ruler", Ruler],
|
|
124
|
+
["safeMarginsVisible", "Safe margins", FrameCorners],
|
|
125
|
+
] as const
|
|
126
|
+
).map(([key, label, Icon]) => (
|
|
127
|
+
<button
|
|
128
|
+
key={key}
|
|
129
|
+
type="button"
|
|
130
|
+
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
|
|
131
|
+
guides[key]
|
|
132
|
+
? "bg-studio-accent/20 text-studio-accent"
|
|
133
|
+
: "bg-black/40 text-white/60 hover:bg-black/60 hover:text-white/80"
|
|
134
|
+
}`}
|
|
135
|
+
onClick={() => guides.toggle(key)}
|
|
136
|
+
title={`${label} ${guides[key] ? "on" : "off"}`}
|
|
137
|
+
aria-label={`Toggle ${label.toLowerCase()}`}
|
|
138
|
+
aria-pressed={guides[key]}
|
|
139
|
+
>
|
|
140
|
+
<Icon size={16} weight={guides[key] ? "fill" : "regular"} />
|
|
141
|
+
</button>
|
|
142
|
+
))}
|
|
119
143
|
<button
|
|
120
144
|
type="button"
|
|
121
145
|
className={`rounded-md p-1.5 transition-colors active:scale-[0.95] ${
|
|
@@ -155,7 +179,7 @@ export const SnapToolbar = memo(function SnapToolbar({ onSnapChange }: SnapToolb
|
|
|
155
179
|
</button>
|
|
156
180
|
<button
|
|
157
181
|
type="button"
|
|
158
|
-
className="absolute -right-0.5 -bottom-0.5 rounded p-0.5 text-white/50 hover:text-white/90 bg-black/50"
|
|
182
|
+
className="absolute -right-0.5 -bottom-0.5 rounded-sm p-0.5 text-white/50 hover:text-white/90 bg-black/50"
|
|
159
183
|
onClick={() => setGridPopoverOpen((v) => !v)}
|
|
160
184
|
title="Grid options"
|
|
161
185
|
aria-label="Grid options"
|
|
@@ -185,7 +209,7 @@ export const SnapToolbar = memo(function SnapToolbar({ onSnapChange }: SnapToolb
|
|
|
185
209
|
updatePrefs({ gridSpacing: val });
|
|
186
210
|
}
|
|
187
211
|
}}
|
|
188
|
-
className="w-16 rounded bg-neutral-900 border border-neutral-600 px-1.5 py-0.5 text-xs text-white text-right tabular-nums outline-
|
|
212
|
+
className="w-16 rounded-sm bg-neutral-900 border border-neutral-600 px-1.5 py-0.5 text-xs text-white text-right tabular-nums outline-hidden focus:border-studio-accent"
|
|
189
213
|
/>
|
|
190
214
|
</label>
|
|
191
215
|
<label className="flex items-center gap-2 text-xs text-white/80 cursor-pointer">
|
|
@@ -139,7 +139,7 @@ export function TimelineFxPopover({
|
|
|
139
139
|
ref={rootRef}
|
|
140
140
|
role="dialog"
|
|
141
141
|
aria-label="Effects"
|
|
142
|
-
className="z-
|
|
142
|
+
className="z-200 flex flex-col overflow-hidden rounded-md border border-white/10 bg-[#1b1b1f] p-2 shadow-xl"
|
|
143
143
|
style={clampedStyle(anchorRect)}
|
|
144
144
|
onKeyDown={onKeyDown}
|
|
145
145
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -143,7 +143,7 @@ export function TopologyLens({ iframeRef, activeCompositionPath }: TopologyLensP
|
|
|
143
143
|
ref={overlayRef}
|
|
144
144
|
aria-hidden="true"
|
|
145
145
|
data-topology-lens={visible ? state.phase : "hidden"}
|
|
146
|
-
className="pointer-events-none absolute inset-0 z-
|
|
146
|
+
className="pointer-events-none absolute inset-0 z-58 overflow-hidden"
|
|
147
147
|
>
|
|
148
148
|
{visible && (
|
|
149
149
|
<>
|
|
@@ -171,7 +171,7 @@ export function TopologyLens({ iframeRef, activeCompositionPath }: TopologyLensP
|
|
|
171
171
|
key={index}
|
|
172
172
|
data-topology-contour="true"
|
|
173
173
|
data-topology-node={label}
|
|
174
|
-
className="hf-topology-contour pointer-events-none absolute z-
|
|
174
|
+
className="hf-topology-contour pointer-events-none absolute z-1 rounded-sm"
|
|
175
175
|
style={{
|
|
176
176
|
...rectStyle(rect),
|
|
177
177
|
animationDelay: `${Math.min(index, 5) * 28}ms`,
|
|
@@ -181,7 +181,7 @@ export function TopologyLens({ iframeRef, activeCompositionPath }: TopologyLensP
|
|
|
181
181
|
{!reducedMotion && (
|
|
182
182
|
<div
|
|
183
183
|
data-topology-scan="true"
|
|
184
|
-
className="pointer-events-none absolute z-
|
|
184
|
+
className="pointer-events-none absolute z-2 overflow-hidden rounded-sm"
|
|
185
185
|
style={rectStyle(geometry.field.rect)}
|
|
186
186
|
>
|
|
187
187
|
<div className="hf-topology-scan absolute inset-y-0 left-0 w-1/4" />
|
|
@@ -196,7 +196,7 @@ export function TopologyLens({ iframeRef, activeCompositionPath }: TopologyLensP
|
|
|
196
196
|
data-topology-terminal={
|
|
197
197
|
state.phase === "localizing" ? (state.terminal ?? undefined) : undefined
|
|
198
198
|
}
|
|
199
|
-
className="hf-topology-target pointer-events-none absolute z-
|
|
199
|
+
className="hf-topology-target pointer-events-none absolute z-3 rounded-md"
|
|
200
200
|
style={rectStyle(geometry.target.rect)}
|
|
201
201
|
/>
|
|
202
202
|
</>
|
|
@@ -183,7 +183,7 @@ export function Transform3DCube({
|
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
return (
|
|
186
|
-
<div className="relative overflow-hidden rounded-lg border border-neutral-800 bg-
|
|
186
|
+
<div className="relative overflow-hidden rounded-lg border border-neutral-800 bg-linear-to-b from-neutral-900 to-neutral-950">
|
|
187
187
|
<svg
|
|
188
188
|
ref={svgRef}
|
|
189
189
|
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
|
@@ -294,7 +294,7 @@ export function Transform3DCube({
|
|
|
294
294
|
onClick={onRecenter}
|
|
295
295
|
title="Reset 3D orientation"
|
|
296
296
|
aria-label="Reset 3D orientation"
|
|
297
|
-
className="absolute right-1.5 top-1.5 rounded p-0.5 text-neutral-500 hover:bg-neutral-800 hover:text-neutral-200"
|
|
297
|
+
className="absolute right-1.5 top-1.5 rounded-sm p-0.5 text-neutral-500 hover:bg-neutral-800 hover:text-neutral-200"
|
|
298
298
|
>
|
|
299
299
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
300
300
|
<circle cx="12" cy="12" r="9" strokeWidth="2" />
|
|
@@ -37,6 +37,7 @@ export function audioFxRevealTarget(
|
|
|
37
37
|
const parsed = parseAutomationTarget(target);
|
|
38
38
|
if (!parsed) return null;
|
|
39
39
|
if (parsed.kind === "volume") return { kind: "volume" };
|
|
40
|
+
if (parsed.kind === "rate") return null;
|
|
40
41
|
if (parsed.kind === "preset") {
|
|
41
42
|
// A preset-level lane names the preset, not a node inside it: find its run
|
|
42
43
|
// by the first node that belongs to it, which is how `runKey` is built.
|
|
@@ -1103,7 +1103,7 @@ describe("patch builders and prompt builder", () => {
|
|
|
1103
1103
|
expect(prompt).toContain("Computed styles (browser-resolved):");
|
|
1104
1104
|
expect(prompt).toContain("Target HTML:");
|
|
1105
1105
|
expect(prompt).toContain("Guardrails:");
|
|
1106
|
-
expect(prompt).toContain("Do not modify other elements' data-* attributes or positioning
|
|
1106
|
+
expect(prompt).toContain("Do not modify other elements' data-* attributes or positioning");
|
|
1107
1107
|
});
|
|
1108
1108
|
|
|
1109
1109
|
it("uses an absolute source path in copied agent prompts when provided", () => {
|
|
@@ -78,9 +78,10 @@ describe("buildElementAgentPrompt", () => {
|
|
|
78
78
|
"Text: Hello world",
|
|
79
79
|
"",
|
|
80
80
|
"Guardrails:",
|
|
81
|
-
"- Make a targeted change to this element only.",
|
|
82
|
-
"- Preserve the rest of the composition and its timing.",
|
|
83
|
-
"- Do not modify other elements' data-* attributes or positioning.",
|
|
81
|
+
"- Make a targeted change to this element only, unless the request is a timeline edit.",
|
|
82
|
+
"- Preserve the rest of the composition and its timing, except what a timeline edit changes.",
|
|
83
|
+
"- Do not modify other elements' data-* attributes or positioning, except where the requested timeline edit requires it (split, retime, reorder, copy a group, swap media).",
|
|
84
|
+
"- For timeline edits (trim, split, speed, volume, copy, swap), follow the creator-editing-recipes reference of the hyperframes-core skill and use its exact attribute forms.",
|
|
84
85
|
"- Prefer existing inline styles or existing CSS rules for this element over adding unrelated selectors.",
|
|
85
86
|
].join("\n"),
|
|
86
87
|
);
|
|
@@ -160,9 +161,10 @@ describe("buildPickerAgentPrompt", () => {
|
|
|
160
161
|
// Both end on the identical guardrails block, byte for byte.
|
|
161
162
|
const guardrails = [
|
|
162
163
|
"Guardrails:",
|
|
163
|
-
"- Make a targeted change to this element only.",
|
|
164
|
-
"- Preserve the rest of the composition and its timing.",
|
|
165
|
-
"- Do not modify other elements' data-* attributes or positioning.",
|
|
164
|
+
"- Make a targeted change to this element only, unless the request is a timeline edit.",
|
|
165
|
+
"- Preserve the rest of the composition and its timing, except what a timeline edit changes.",
|
|
166
|
+
"- Do not modify other elements' data-* attributes or positioning, except where the requested timeline edit requires it (split, retime, reorder, copy a group, swap media).",
|
|
167
|
+
"- For timeline edits (trim, split, speed, volume, copy, swap), follow the creator-editing-recipes reference of the hyperframes-core skill and use its exact attribute forms.",
|
|
166
168
|
"- Prefer existing inline styles or existing CSS rules for this element over adding unrelated selectors.",
|
|
167
169
|
].join("\n");
|
|
168
170
|
expect(domPrompt.endsWith(guardrails)).toBe(true);
|
|
@@ -28,9 +28,10 @@ export interface AgentPromptElementInfo {
|
|
|
28
28
|
|
|
29
29
|
const GUARDRAIL_LINES = [
|
|
30
30
|
"Guardrails:",
|
|
31
|
-
"- Make a targeted change to this element only.",
|
|
32
|
-
"- Preserve the rest of the composition and its timing.",
|
|
33
|
-
"- Do not modify other elements' data-* attributes or positioning.",
|
|
31
|
+
"- Make a targeted change to this element only, unless the request is a timeline edit.",
|
|
32
|
+
"- Preserve the rest of the composition and its timing, except what a timeline edit changes.",
|
|
33
|
+
"- Do not modify other elements' data-* attributes or positioning, except where the requested timeline edit requires it (split, retime, reorder, copy a group, swap media).",
|
|
34
|
+
"- For timeline edits (trim, split, speed, volume, copy, swap), follow the creator-editing-recipes reference of the hyperframes-core skill and use its exact attribute forms.",
|
|
34
35
|
"- Prefer existing inline styles or existing CSS rules for this element over adding unrelated selectors.",
|
|
35
36
|
];
|
|
36
37
|
|
|
@@ -96,6 +97,7 @@ export function buildElementAgentPrompt({
|
|
|
96
97
|
selectionContext,
|
|
97
98
|
userInstruction,
|
|
98
99
|
sourceFilePath,
|
|
100
|
+
timeline,
|
|
99
101
|
}: {
|
|
100
102
|
selection: DomEditSelection;
|
|
101
103
|
currentTime: number;
|
|
@@ -103,6 +105,8 @@ export function buildElementAgentPrompt({
|
|
|
103
105
|
selectionContext?: string;
|
|
104
106
|
userInstruction?: string;
|
|
105
107
|
sourceFilePath?: string;
|
|
108
|
+
/** The `formatTimelineBlock` text; omitted or empty when the timeline has no clips. */
|
|
109
|
+
timeline?: string;
|
|
106
110
|
}): string {
|
|
107
111
|
const displayedSourceFile = sourceFilePath?.trim() || selection.sourceFile;
|
|
108
112
|
const info: AgentPromptElementInfo = {
|
|
@@ -136,6 +140,8 @@ export function buildElementAgentPrompt({
|
|
|
136
140
|
|
|
137
141
|
lines.push(...buildElementDetailLines(info));
|
|
138
142
|
|
|
143
|
+
if (timeline) lines.push("", timeline);
|
|
144
|
+
|
|
139
145
|
if (tagSnippet) {
|
|
140
146
|
lines.push("", "Target HTML:", tagSnippet);
|
|
141
147
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
3
|
+
|
|
4
|
+
/** Space the preview fit reserves above and left of the frame while the ruler is on. */
|
|
5
|
+
export const RULER_GUTTER_PX = 16;
|
|
6
|
+
|
|
7
|
+
type GuideKey = "rulerVisible" | "safeMarginsVisible";
|
|
8
|
+
|
|
9
|
+
interface PreviewGuidesState {
|
|
10
|
+
rulerVisible: boolean;
|
|
11
|
+
safeMarginsVisible: boolean;
|
|
12
|
+
toggle: (key: GuideKey) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const usePreviewGuidesStore = create<PreviewGuidesState>((set, get) => {
|
|
16
|
+
const stored = readStudioUiPreferences();
|
|
17
|
+
return {
|
|
18
|
+
rulerVisible: stored.rulerVisible ?? false,
|
|
19
|
+
safeMarginsVisible: stored.safeMarginsVisible ?? false,
|
|
20
|
+
toggle: (key) => {
|
|
21
|
+
const next = !get()[key];
|
|
22
|
+
writeStudioUiPreferences({ [key]: next });
|
|
23
|
+
set({ [key]: next });
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|