@hyperframes/studio 0.7.57 → 0.7.59
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--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +27 -7
- package/dist/index.html +2 -2
- package/dist/index.js +18029 -11563
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +6 -6
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +55 -1
- package/src/hooks/timelineTrackVisibility.ts +31 -13
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +81 -162
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +182 -84
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +306 -155
- package/src/hooks/useTimelineEditing.ts +111 -101
- package/src/hooks/useTimelineEditingTypes.ts +4 -1
- package/src/hooks/useTimelineGroupEditing.ts +51 -5
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-DfmYkU44.js +0 -423
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
2
|
+
|
|
3
|
+
export function PropertyPanelFlatFooter({
|
|
4
|
+
onAskAgent,
|
|
5
|
+
recordingState,
|
|
6
|
+
recordingDuration,
|
|
7
|
+
onToggleRecording,
|
|
8
|
+
}: {
|
|
9
|
+
onAskAgent?: () => void;
|
|
10
|
+
recordingState?: "idle" | "recording" | "preview";
|
|
11
|
+
recordingDuration?: number;
|
|
12
|
+
onToggleRecording?: () => void;
|
|
13
|
+
}) {
|
|
14
|
+
const track = useTrackDesignInput();
|
|
15
|
+
const recording = recordingState === "recording";
|
|
16
|
+
const recordTitle = recording
|
|
17
|
+
? `Stop recording ${(recordingDuration ?? 0).toFixed(1)}s`
|
|
18
|
+
: "Record gesture (R)";
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
// No border-t here: every possible element immediately above this footer
|
|
22
|
+
// in the new fixed-headers + scrollable-open-section layout (a collapsed
|
|
23
|
+
// FlatGroupHeader, or the open group's scrollable body wrapper) already
|
|
24
|
+
// draws its own border-b in normal document flow — nothing here is
|
|
25
|
+
// `position: sticky` anymore, so there's no rounding seam to seal (see
|
|
26
|
+
// p11-scrollable-open-section-report.md).
|
|
27
|
+
<div className="flex items-center justify-between bg-panel-bg px-4 py-[11px]">
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
data-flat-footer-ask="true"
|
|
31
|
+
onClick={() => {
|
|
32
|
+
track("button", "Ask agent");
|
|
33
|
+
onAskAgent?.();
|
|
34
|
+
}}
|
|
35
|
+
disabled={!onAskAgent}
|
|
36
|
+
className="flex items-center gap-[7px] text-[11px] font-medium text-panel-text-2 disabled:cursor-not-allowed"
|
|
37
|
+
>
|
|
38
|
+
<svg
|
|
39
|
+
width="13"
|
|
40
|
+
height="13"
|
|
41
|
+
viewBox="0 0 16 16"
|
|
42
|
+
fill="currentColor"
|
|
43
|
+
className="text-panel-accent"
|
|
44
|
+
>
|
|
45
|
+
<path d="M8 1l1.4 4.6L14 7l-4.6 1.4L8 13l-1.4-4.6L2 7l4.6-1.4z" />
|
|
46
|
+
</svg>
|
|
47
|
+
Ask agent about this element
|
|
48
|
+
</button>
|
|
49
|
+
{onToggleRecording && (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
data-flat-footer-record="true"
|
|
53
|
+
aria-label={recordTitle}
|
|
54
|
+
title={recordTitle}
|
|
55
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
56
|
+
onClick={() => {
|
|
57
|
+
track("button", "Gesture recording");
|
|
58
|
+
onToggleRecording();
|
|
59
|
+
}}
|
|
60
|
+
className={recording ? "text-panel-danger animate-pulse" : "text-panel-danger"}
|
|
61
|
+
>
|
|
62
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
|
63
|
+
{recording ? (
|
|
64
|
+
<rect x="2" y="2" width="10" height="10" rx="1.5" />
|
|
65
|
+
) : (
|
|
66
|
+
<circle cx="7" cy="7" r="6" />
|
|
67
|
+
)}
|
|
68
|
+
</svg>
|
|
69
|
+
</button>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
document.body.innerHTML = "";
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function renderHeader(overrides: Partial<Parameters<typeof PropertyPanelFlatHeader>[0]> = {}) {
|
|
15
|
+
const host = document.createElement("div");
|
|
16
|
+
document.body.append(host);
|
|
17
|
+
const root = createRoot(host);
|
|
18
|
+
const props = {
|
|
19
|
+
name: "Mono Label",
|
|
20
|
+
meta: ".mono-label · div",
|
|
21
|
+
elementKind: "text" as const,
|
|
22
|
+
hidden: false,
|
|
23
|
+
copied: false,
|
|
24
|
+
onCopy: vi.fn(),
|
|
25
|
+
onClear: vi.fn(),
|
|
26
|
+
showUngroup: false,
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
act(() => {
|
|
30
|
+
root.render(<PropertyPanelFlatHeader {...props} />);
|
|
31
|
+
});
|
|
32
|
+
return { host, root, props };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("PropertyPanelFlatHeader", () => {
|
|
36
|
+
it("renders name, meta, and the mint text-type icon", () => {
|
|
37
|
+
const { host, root } = renderHeader();
|
|
38
|
+
expect(host.textContent).toContain("Mono Label");
|
|
39
|
+
expect(host.textContent).toContain(".mono-label · div");
|
|
40
|
+
const icon = host.querySelector('[data-flat-header-icon="true"]');
|
|
41
|
+
expect(icon?.className).toContain("text-panel-accent");
|
|
42
|
+
act(() => root.unmount());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("colors the media icon cyan and the other icon amber", () => {
|
|
46
|
+
const { host: mediaHost, root: mediaRoot } = renderHeader({ elementKind: "media" });
|
|
47
|
+
expect(mediaHost.querySelector('[data-flat-header-icon="true"]')?.className).toContain(
|
|
48
|
+
"text-panel-media",
|
|
49
|
+
);
|
|
50
|
+
act(() => mediaRoot.unmount());
|
|
51
|
+
|
|
52
|
+
const { host: otherHost, root: otherRoot } = renderHeader({ elementKind: "other" });
|
|
53
|
+
expect(otherHost.querySelector('[data-flat-header-icon="true"]')?.className).toContain(
|
|
54
|
+
"text-panel-container",
|
|
55
|
+
);
|
|
56
|
+
act(() => otherRoot.unmount());
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("fires onCopy and onClear from their action buttons", () => {
|
|
60
|
+
const { host, root, props } = renderHeader();
|
|
61
|
+
const copy = host.querySelector<HTMLButtonElement>(
|
|
62
|
+
'[aria-label="Copy element info to clipboard"]',
|
|
63
|
+
);
|
|
64
|
+
const clear = host.querySelector<HTMLButtonElement>('[aria-label="Clear selection"]');
|
|
65
|
+
act(() => copy?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
66
|
+
act(() => clear?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
67
|
+
expect(props.onCopy).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(props.onClear).toHaveBeenCalledTimes(1);
|
|
69
|
+
act(() => root.unmount());
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("only renders Ungroup when showUngroup is true", () => {
|
|
73
|
+
const { host: without } = renderHeader({ showUngroup: false });
|
|
74
|
+
expect(without.querySelector('[aria-label="Ungroup"]')).toBeNull();
|
|
75
|
+
|
|
76
|
+
const { host: withUngroup } = renderHeader({ showUngroup: true, onUngroup: vi.fn() });
|
|
77
|
+
expect(withUngroup.querySelector('[aria-label="Ungroup"]')).not.toBeNull();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Eye, EyeSlash } from "@phosphor-icons/react";
|
|
2
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
3
|
+
import { ClipboardList, Film, Square, Type, X } from "../../icons/SystemIcons";
|
|
4
|
+
|
|
5
|
+
const ICON_BY_KIND = { text: Type, media: Film, other: Square } as const;
|
|
6
|
+
const ICON_COLOR_BY_KIND = {
|
|
7
|
+
text: "text-panel-accent",
|
|
8
|
+
media: "text-panel-media",
|
|
9
|
+
other: "text-panel-container",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export function PropertyPanelFlatHeader({
|
|
13
|
+
name,
|
|
14
|
+
meta,
|
|
15
|
+
elementKind,
|
|
16
|
+
hidden,
|
|
17
|
+
onToggleHidden,
|
|
18
|
+
copied,
|
|
19
|
+
onCopy,
|
|
20
|
+
onClear,
|
|
21
|
+
onUngroup,
|
|
22
|
+
showUngroup,
|
|
23
|
+
}: {
|
|
24
|
+
name: string;
|
|
25
|
+
meta: string;
|
|
26
|
+
elementKind: "text" | "media" | "other";
|
|
27
|
+
hidden: boolean;
|
|
28
|
+
onToggleHidden?: () => void;
|
|
29
|
+
copied: boolean;
|
|
30
|
+
onCopy: () => void;
|
|
31
|
+
onClear: () => void;
|
|
32
|
+
onUngroup?: () => void;
|
|
33
|
+
showUngroup: boolean;
|
|
34
|
+
}) {
|
|
35
|
+
const track = useTrackDesignInput();
|
|
36
|
+
const Icon = ICON_BY_KIND[elementKind];
|
|
37
|
+
const visibilityLabel = hidden ? "Show element" : "Hide element";
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex items-center gap-2.5 border-b border-panel-hairline px-4 py-3">
|
|
41
|
+
<Icon
|
|
42
|
+
size={15}
|
|
43
|
+
data-flat-header-icon="true"
|
|
44
|
+
className={`flex-shrink-0 ${ICON_COLOR_BY_KIND[elementKind]}`}
|
|
45
|
+
/>
|
|
46
|
+
<div className="flex min-w-0 flex-1 items-baseline gap-2">
|
|
47
|
+
<span className="truncate text-[13px] font-semibold text-panel-text-0">{name}</span>
|
|
48
|
+
<span className="truncate font-mono text-[10px] text-panel-text-4">{meta}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="flex flex-shrink-0 items-center gap-2.5 text-panel-text-3">
|
|
51
|
+
{showUngroup && (
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
aria-label="Ungroup"
|
|
55
|
+
title="Ungroup (⌘⇧G)"
|
|
56
|
+
onClick={() => {
|
|
57
|
+
track("button", "Ungroup");
|
|
58
|
+
onUngroup?.();
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<svg
|
|
62
|
+
width="13"
|
|
63
|
+
height="13"
|
|
64
|
+
viewBox="0 0 16 16"
|
|
65
|
+
fill="none"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
strokeWidth="1.5"
|
|
68
|
+
>
|
|
69
|
+
<rect x="1.5" y="1.5" width="7" height="7" rx="1" />
|
|
70
|
+
<rect x="7.5" y="7.5" width="7" height="7" rx="1" />
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
{onToggleHidden && (
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
aria-label={visibilityLabel}
|
|
78
|
+
title={visibilityLabel}
|
|
79
|
+
onClick={() => {
|
|
80
|
+
track("toggle", "Element visibility");
|
|
81
|
+
onToggleHidden();
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{hidden ? <EyeSlash size={13} weight="bold" /> : <Eye size={13} weight="bold" />}
|
|
85
|
+
</button>
|
|
86
|
+
)}
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
aria-label="Copy element info to clipboard"
|
|
90
|
+
title={copied ? "Copied!" : "Copy element info for any AI agent"}
|
|
91
|
+
onClick={() => {
|
|
92
|
+
track("button", "Copy element info");
|
|
93
|
+
onCopy();
|
|
94
|
+
}}
|
|
95
|
+
className={copied ? "text-panel-accent" : undefined}
|
|
96
|
+
>
|
|
97
|
+
<ClipboardList size={13} />
|
|
98
|
+
</button>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
aria-label="Clear selection"
|
|
102
|
+
onClick={() => {
|
|
103
|
+
track("button", "Clear selection");
|
|
104
|
+
onClear();
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<X size={13} />
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
parseZIndex,
|
|
7
7
|
resolveCrossedNeighbor,
|
|
8
8
|
resolveZOrderChange,
|
|
9
|
+
resolveZOrderReposition,
|
|
9
10
|
type ZOrderAction,
|
|
10
11
|
type ZOrderPatch,
|
|
11
12
|
} from "./canvasContextMenuZOrder";
|
|
@@ -617,3 +618,59 @@ describe("isZOrderActionEnabled", () => {
|
|
|
617
618
|
}
|
|
618
619
|
});
|
|
619
620
|
});
|
|
621
|
+
|
|
622
|
+
describe("resolveZOrderReposition (Layers-panel arbitrary drop)", () => {
|
|
623
|
+
it("multi-position jump with distinct z resolves to ONE between-z write", () => {
|
|
624
|
+
// Render order bottom→top today: target(1), a(3), b(5). Drop target between
|
|
625
|
+
// a and b → single write: z strictly between 3 and 5.
|
|
626
|
+
const { target, byId } = makeFamily("1", [
|
|
627
|
+
["a", "3"],
|
|
628
|
+
["b", "5"],
|
|
629
|
+
]);
|
|
630
|
+
const patches = resolveZOrderReposition(target, [byId.a, target, byId.b]);
|
|
631
|
+
expect(patches).toEqual([{ element: target, zIndex: 4 }]);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("jump to the very top writes one z above the previous top", () => {
|
|
635
|
+
const { target, byId } = makeFamily("1", [
|
|
636
|
+
["a", "3"],
|
|
637
|
+
["b", "5"],
|
|
638
|
+
]);
|
|
639
|
+
const patches = resolveZOrderReposition(target, [byId.a, byId.b, target]);
|
|
640
|
+
expect(patches).toEqual([{ element: target, zIndex: 6 }]);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("no-op drop (unchanged order) returns null", () => {
|
|
644
|
+
const { target, byId } = makeFamily("1", [
|
|
645
|
+
["a", "3"],
|
|
646
|
+
["b", "5"],
|
|
647
|
+
]);
|
|
648
|
+
expect(resolveZOrderReposition(target, [target, byId.a, byId.b])).toBeNull();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("tied z values renumber the scoped set minimally (band-safe)", () => {
|
|
652
|
+
const { target, byId } = makeFamily("2", [
|
|
653
|
+
["a", "2"],
|
|
654
|
+
["b", "2"],
|
|
655
|
+
]);
|
|
656
|
+
// All tied at 2; DOM order target,a,b → render bottom→top target,a,b.
|
|
657
|
+
// Move target to the top: scoped renumber within the band.
|
|
658
|
+
const patches = resolveZOrderReposition(target, [byId.a, byId.b, target]);
|
|
659
|
+
expect(patches).not.toBeNull();
|
|
660
|
+
const z = new Map(patches!.map((p) => [(p.element as HTMLElement).id, p.zIndex]));
|
|
661
|
+
const zOf = (id: string) => z.get(id) ?? 2;
|
|
662
|
+
expect(zOf("a")).toBeLessThan(zOf("b"));
|
|
663
|
+
expect(zOf("b")).toBeLessThan(zOf("target"));
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("rejects elements that are not painting siblings of the target", () => {
|
|
667
|
+
const { target, byId } = makeFamily("1", [["a", "3"]]);
|
|
668
|
+
const stranger = makeEl("stranger", "2");
|
|
669
|
+
expect(resolveZOrderReposition(target, [stranger, target, byId.a])).toBeNull();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("returns null for sets too small to reorder", () => {
|
|
673
|
+
const { target } = makeFamily("1", []);
|
|
674
|
+
expect(resolveZOrderReposition(target, [target])).toBeNull();
|
|
675
|
+
});
|
|
676
|
+
});
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
|
|
45
|
+
import { readLayerRevealPriorZ } from "../../player/lib/timelineElementHelpers";
|
|
45
46
|
|
|
46
47
|
export type ZOrderAction = "bring-forward" | "send-backward" | "bring-to-front" | "send-to-back";
|
|
47
48
|
|
|
@@ -107,8 +108,11 @@ export function parseZIndex(value: string | null | undefined): number {
|
|
|
107
108
|
return Number.isFinite(n) ? n : 0;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
/** Read the effective z-index for an element (inline style preferred).
|
|
111
|
+
/** Read the effective z-index for an element (inline style preferred).
|
|
112
|
+
* Reveal-lift transparent: an active Layers-panel lift reports the TRUE z. */
|
|
111
113
|
export function readEffectiveZIndex(el: HTMLElement): number {
|
|
114
|
+
const prior = readLayerRevealPriorZ(el);
|
|
115
|
+
if (prior != null) return prior;
|
|
112
116
|
const inline = el.style.zIndex;
|
|
113
117
|
if (inline && inline !== "auto") return parseZIndex(inline);
|
|
114
118
|
try {
|
|
@@ -439,6 +443,41 @@ export function resolveZOrderChange(
|
|
|
439
443
|
return realizeOrder(order, desired, target, entries);
|
|
440
444
|
}
|
|
441
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Realize an ARBITRARY repositioning of `target` within a scoped sibling set —
|
|
448
|
+
* the Layers-panel drag, which can jump several siblings in one drop, unlike
|
|
449
|
+
* the menu's four fixed actions. `desiredOrderBottomToTop` is the scoped set
|
|
450
|
+
* (target included at its new slot) in the intended render order. Reuses the
|
|
451
|
+
* menu's minimal-write realization (realizeOrder): one between-z write when a
|
|
452
|
+
* strict gap exists, band-safe scoped renumber otherwise — replacing the old
|
|
453
|
+
* LayersPanel computeReorderZValues path that stamped EVERY sibling.
|
|
454
|
+
*
|
|
455
|
+
* Null when nothing changes, the set is too small, or an element in the
|
|
456
|
+
* desired order is not actually a painting sibling of `target`.
|
|
457
|
+
*/
|
|
458
|
+
export function resolveZOrderReposition(
|
|
459
|
+
target: HTMLElement,
|
|
460
|
+
desiredOrderBottomToTop: readonly HTMLElement[],
|
|
461
|
+
): ZOrderPatch[] | null {
|
|
462
|
+
const { entries } = getFamily(target);
|
|
463
|
+
if (entries.length < 2) return null;
|
|
464
|
+
const byElement = new Map(entries.map((entry) => [entry.element, entry]));
|
|
465
|
+
const desired: RenderEntry[] = [];
|
|
466
|
+
for (const el of desiredOrderBottomToTop) {
|
|
467
|
+
const entry = byElement.get(el);
|
|
468
|
+
if (!entry) return null;
|
|
469
|
+
desired.push(entry);
|
|
470
|
+
}
|
|
471
|
+
if (desired.length < 2 || !desired.some((entry) => entry.element === target)) return null;
|
|
472
|
+
const currentOrder = toRenderOrder(desired);
|
|
473
|
+
// A drop back into the same slot is a no-op. The menu actions guard this via
|
|
474
|
+
// their position checks before realizeOrder; an arbitrary reposition must
|
|
475
|
+
// compare the orders itself — realizeOrder would otherwise "normalize" an
|
|
476
|
+
// end-of-set target to a fresh z value it doesn't need.
|
|
477
|
+
if (currentOrder.every((entry, i) => entry.element === desired[i].element)) return null;
|
|
478
|
+
return realizeOrder(currentOrder, desired, target, entries);
|
|
479
|
+
}
|
|
480
|
+
|
|
442
481
|
/**
|
|
443
482
|
* The sibling a forward/backward step crosses: the visible overlapping
|
|
444
483
|
* neighbor directly above (bring-forward) or below (send-backward) the target
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* No imports from other domEditing* modules — safe to import from anywhere.
|
|
5
5
|
*/
|
|
6
6
|
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
|
|
7
|
+
import { getSourceScopedSelectorIndex } from "../../utils/sourceScopedSelectorIndex";
|
|
7
8
|
import { CURATED_STYLE_PROPERTIES } from "./domEditingTypes";
|
|
8
9
|
|
|
9
10
|
// ─── Type guard ───────────────────────────────────────────────────────────────
|
|
@@ -296,11 +297,9 @@ export function getSelectorIndex(
|
|
|
296
297
|
): number | undefined {
|
|
297
298
|
if (!selector?.startsWith(".")) return undefined;
|
|
298
299
|
|
|
299
|
-
|
|
300
|
-
(candidate)
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
return getSourceScopedSelectorIndex(doc, el, selector, sourceFile, (candidate) =>
|
|
301
|
+
isHtmlElement(candidate)
|
|
302
|
+
? getSourceFileForElement(candidate, activeCompositionPath).sourceFile
|
|
303
|
+
: undefined,
|
|
303
304
|
);
|
|
304
|
-
const index = candidates.indexOf(el);
|
|
305
|
-
return index >= 0 ? index : undefined;
|
|
306
305
|
}
|
|
@@ -246,45 +246,28 @@ export function findElementForSelection(
|
|
|
246
246
|
selection: FindElementSelection,
|
|
247
247
|
activeCompositionPath: string | null = null,
|
|
248
248
|
): HTMLElement | null {
|
|
249
|
+
const sourceMatches = (candidate: Element): candidate is HTMLElement =>
|
|
250
|
+
isHtmlElement(candidate) &&
|
|
251
|
+
(!selection.sourceFile ||
|
|
252
|
+
getSourceFileForElement(candidate, activeCompositionPath).sourceFile ===
|
|
253
|
+
selection.sourceFile);
|
|
254
|
+
const findAll = (selector: string): HTMLElement[] =>
|
|
255
|
+
querySelectorAllSafely(doc, selector).filter(sourceMatches);
|
|
256
|
+
|
|
249
257
|
if (selection.hfId) {
|
|
250
|
-
const byHfId =
|
|
251
|
-
if (
|
|
258
|
+
const byHfId = findAll(`[data-hf-id="${escapeCssString(selection.hfId)}"]`)[0];
|
|
259
|
+
if (byHfId) return byHfId;
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
if (selection.id) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
getSourceFileForElement(byId, activeCompositionPath).sourceFile === selection.sourceFile)
|
|
260
|
-
) {
|
|
261
|
-
return byId;
|
|
262
|
-
}
|
|
263
|
+
// Flattened sub-compositions can repeat authored ids. getElementById returns
|
|
264
|
+
// only the first document match, so filter every id match by source first.
|
|
265
|
+
const byId = findAll(`[id="${escapeCssString(selection.id)}"]`)[0];
|
|
266
|
+
if (byId) return byId;
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
if (!selection.selector) return null;
|
|
266
|
-
|
|
267
|
-
// fallow-ignore-next-line code-duplication
|
|
268
|
-
if (selection.selector.startsWith(".") && selection.selectorIndex != null) {
|
|
269
|
-
const matches = querySelectorAllSafely(doc, selection.selector).filter(
|
|
270
|
-
(candidate): candidate is HTMLElement =>
|
|
271
|
-
isHtmlElement(candidate) &&
|
|
272
|
-
(!selection.sourceFile ||
|
|
273
|
-
getSourceFileForElement(candidate, activeCompositionPath).sourceFile ===
|
|
274
|
-
selection.sourceFile),
|
|
275
|
-
);
|
|
276
|
-
return matches[selection.selectorIndex] ?? null;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// fallow-ignore-next-line code-duplication
|
|
280
|
-
const matches = querySelectorAllSafely(doc, selection.selector).filter(
|
|
281
|
-
(candidate): candidate is HTMLElement =>
|
|
282
|
-
isHtmlElement(candidate) &&
|
|
283
|
-
(!selection.sourceFile ||
|
|
284
|
-
getSourceFileForElement(candidate, activeCompositionPath).sourceFile ===
|
|
285
|
-
selection.sourceFile),
|
|
286
|
-
);
|
|
287
|
-
return matches[0] ?? null;
|
|
270
|
+
return findAll(selection.selector)[selection.selectorIndex ?? 0] ?? null;
|
|
288
271
|
}
|
|
289
272
|
|
|
290
273
|
// fallow-ignore-next-line complexity
|
|
@@ -34,3 +34,30 @@ export interface GsapAnimationEditCallbacks {
|
|
|
34
34
|
/** Unroll a computed (helper/loop) tween into literal tweens so it edits directly. */
|
|
35
35
|
onUnroll?: (animationId: string) => void;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
// User-facing control label for each animation-meta field. The ease control is
|
|
39
|
+
// labelled "Speed" in the card UI, so ease/easeEach map there.
|
|
40
|
+
const ANIMATION_META_LABELS: Record<string, { control: string; name: string }> = {
|
|
41
|
+
duration: { control: "metric", name: "Length" },
|
|
42
|
+
position: { control: "metric", name: "Starts at" },
|
|
43
|
+
ease: { control: "select", name: "Speed" },
|
|
44
|
+
easeEach: { control: "select", name: "Speed" },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Emit design-input telemetry for an `onUpdateMeta` payload, attributing each
|
|
49
|
+
* changed field to the control the user actually touched. Iterates the real keys
|
|
50
|
+
* present rather than falling through to a single placeholder — so a meta field
|
|
51
|
+
* added later is attributed honestly by its own key instead of poisoning another
|
|
52
|
+
* control's usage count.
|
|
53
|
+
*/
|
|
54
|
+
export function trackAnimationMetaUpdate(
|
|
55
|
+
track: (control: string, name: string) => void,
|
|
56
|
+
updates: Record<string, unknown>,
|
|
57
|
+
): void {
|
|
58
|
+
for (const key of Object.keys(updates)) {
|
|
59
|
+
const mapped = ANIMATION_META_LABELS[key];
|
|
60
|
+
if (mapped) track(mapped.control, mapped.name);
|
|
61
|
+
else track("select", key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DomEditSelection } from "./domEditingTypes";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the "live preview" callback the 3D-transform sub-view fires while a
|
|
5
|
+
* value is being dragged: apply a gsap.set() to the matching node inside the
|
|
6
|
+
* preview iframe so the edit is reflected immediately, before it's committed.
|
|
7
|
+
*
|
|
8
|
+
* Extracted so the identical closure exists once — shared by the legacy
|
|
9
|
+
* PropertyPanel Layout section and the flat Layout group (PropertyPanelFlat).
|
|
10
|
+
*/
|
|
11
|
+
// Resolve by id when unique, otherwise by selector + selectorIndex — a bare
|
|
12
|
+
// querySelector(selector) always hits the FIRST match, so dragging on the
|
|
13
|
+
// second of two same-selector siblings would animate the wrong element.
|
|
14
|
+
function resolvePreviewNode(
|
|
15
|
+
doc: Document | null | undefined,
|
|
16
|
+
el: DomEditSelection,
|
|
17
|
+
): Element | null {
|
|
18
|
+
if (!doc) return null;
|
|
19
|
+
if (el.id) return doc.querySelector(`#${el.id}`);
|
|
20
|
+
if (!el.selector) return null;
|
|
21
|
+
return doc.querySelectorAll(el.selector)[el.selectorIndex ?? 0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createGsapLivePreview(iframeRef: { readonly current: HTMLIFrameElement | null }) {
|
|
25
|
+
return (el: DomEditSelection, props: Record<string, number>) => {
|
|
26
|
+
const iframe = iframeRef.current;
|
|
27
|
+
const win = iframe?.contentWindow as
|
|
28
|
+
| { gsap?: { set: (t: Element, v: Record<string, number>) => void } }
|
|
29
|
+
| null
|
|
30
|
+
| undefined;
|
|
31
|
+
const node = resolvePreviewNode(iframe?.contentDocument, el);
|
|
32
|
+
if (win?.gsap && node) win.gsap.set(node, props);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DomEditLayerItem } from "./domEditingTypes";
|
|
2
|
+
import { getElementZIndex } from "../../player/lib/layerOrdering";
|
|
3
|
+
|
|
4
|
+
interface CollapsedState {
|
|
5
|
+
[key: string]: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// ── Pure helpers ──────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
// fallow-ignore-next-line complexity
|
|
11
|
+
export function sortLayersByZIndex(layers: DomEditLayerItem[]): DomEditLayerItem[] {
|
|
12
|
+
if (layers.length <= 1) return layers;
|
|
13
|
+
|
|
14
|
+
const minDepth = layers[0].depth;
|
|
15
|
+
for (let i = 1; i < layers.length; i++) {
|
|
16
|
+
if (layers[i].depth < minDepth) return layers;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const chunks: Array<{ root: DomEditLayerItem; children: DomEditLayerItem[]; domIndex: number }> =
|
|
20
|
+
[];
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < layers.length; i++) {
|
|
23
|
+
if (layers[i].depth === minDepth) {
|
|
24
|
+
const children: DomEditLayerItem[] = [];
|
|
25
|
+
let j = i + 1;
|
|
26
|
+
while (j < layers.length && layers[j].depth > minDepth) {
|
|
27
|
+
children.push(layers[j]);
|
|
28
|
+
j++;
|
|
29
|
+
}
|
|
30
|
+
chunks.push({ root: layers[i], children, domIndex: chunks.length });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (chunks.length <= 1) {
|
|
35
|
+
if (chunks.length === 1 && chunks[0].children.length > 0) {
|
|
36
|
+
const sorted = sortLayersByZIndex(chunks[0].children);
|
|
37
|
+
return [chunks[0].root, ...sorted];
|
|
38
|
+
}
|
|
39
|
+
return layers;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
chunks.sort((a, b) => {
|
|
43
|
+
const zA = getElementZIndex(a.root.element);
|
|
44
|
+
const zB = getElementZIndex(b.root.element);
|
|
45
|
+
if (zA !== zB) return zB - zA;
|
|
46
|
+
return b.domIndex - a.domIndex;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const result: DomEditLayerItem[] = [];
|
|
50
|
+
for (const chunk of chunks) {
|
|
51
|
+
result.push(chunk.root);
|
|
52
|
+
if (chunk.children.length > 0) {
|
|
53
|
+
result.push(...sortLayersByZIndex(chunk.children));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getVisibleLayers(
|
|
60
|
+
layers: DomEditLayerItem[],
|
|
61
|
+
collapsed: CollapsedState,
|
|
62
|
+
): DomEditLayerItem[] {
|
|
63
|
+
if (Object.keys(collapsed).length === 0) return layers;
|
|
64
|
+
|
|
65
|
+
const result: DomEditLayerItem[] = [];
|
|
66
|
+
let skipDepth = -1;
|
|
67
|
+
|
|
68
|
+
for (const layer of layers) {
|
|
69
|
+
if (skipDepth >= 0 && layer.depth > skipDepth) continue;
|
|
70
|
+
skipDepth = -1;
|
|
71
|
+
|
|
72
|
+
result.push(layer);
|
|
73
|
+
|
|
74
|
+
if (collapsed[layer.key] && layer.childCount > 0) {
|
|
75
|
+
skipDepth = layer.depth;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
@@ -105,4 +105,14 @@ describe("manual editing availability", () => {
|
|
|
105
105
|
expect(resolveStudioBooleanEnvFlag({ EMPTY: "" }, ["EMPTY"], true)).toBe(true);
|
|
106
106
|
expect(resolveStudioBooleanEnvFlag({ UNKNOWN: "maybe" }, ["UNKNOWN"], false)).toBe(false);
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
it("defaults the flat inspector flag to false and honors an explicit override", async () => {
|
|
110
|
+
const off = await loadAvailabilityWithEnv({});
|
|
111
|
+
expect(off.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(false);
|
|
112
|
+
|
|
113
|
+
const on = await loadAvailabilityWithEnv({
|
|
114
|
+
VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "true",
|
|
115
|
+
});
|
|
116
|
+
expect(on.STUDIO_FLAT_INSPECTOR_ENABLED).toBe(true);
|
|
117
|
+
});
|
|
108
118
|
});
|
|
@@ -97,4 +97,13 @@ export const STUDIO_SDK_RESOLVER_SHADOW_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
97
97
|
true,
|
|
98
98
|
);
|
|
99
99
|
|
|
100
|
+
// Studio inspector redesign ("Ledger, flat" — design_handoff_studio_inspector):
|
|
101
|
+
// flat identity header/footer/groups behind a flag for incremental review.
|
|
102
|
+
// Default false; enable via VITE_STUDIO_FLAT_INSPECTOR_ENABLED=true.
|
|
103
|
+
export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
|
|
104
|
+
env,
|
|
105
|
+
["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
|
|
106
|
+
false,
|
|
107
|
+
);
|
|
108
|
+
|
|
100
109
|
export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
|