@hyperframes/studio 0.7.58 → 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-CtTDO63S.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.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-DeQPzqwH.js → index-FvzmPhfG.js} +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13514 -8528
- 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 +3 -3
- package/src/components/DesignPanelPromoteProvider.tsx +15 -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/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- 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/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -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/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/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.ts +29 -175
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +17 -5
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- 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/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 +4 -1
- package/src/hooks/useTimelineEditing.ts +16 -10
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTimelineGroupEditing.ts +16 -2
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -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/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-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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";
|
|
@@ -5,6 +5,7 @@ import { MetricField } from "./propertyPanelPrimitives";
|
|
|
5
5
|
import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
6
6
|
import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
7
7
|
import { Transform3DCube, type CubePose } from "./Transform3DCube";
|
|
8
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
8
9
|
|
|
9
10
|
// translateZ only foreshortens under a perspective lens. Rather than hardcode one
|
|
10
11
|
// (an arbitrary px value reads wrong at different canvas sizes), derive it from the
|
|
@@ -71,6 +72,7 @@ function Cube3dControl({
|
|
|
71
72
|
onKeyframe?: () => void;
|
|
72
73
|
keyframed?: boolean;
|
|
73
74
|
}) {
|
|
75
|
+
const track = useTrackDesignInput();
|
|
74
76
|
const pose: CubePose = {
|
|
75
77
|
rotationX: gsapRuntimeValues.rotationX ?? 0,
|
|
76
78
|
rotationY: gsapRuntimeValues.rotationY ?? 0,
|
|
@@ -96,6 +98,7 @@ function Cube3dControl({
|
|
|
96
98
|
}
|
|
97
99
|
const axes = Object.keys(changedProps);
|
|
98
100
|
if (axes.length === 0) return;
|
|
101
|
+
track("slider", "3D rotation pose");
|
|
99
102
|
// ONE keyframe for the whole pose change — avoids per-axis commits racing into
|
|
100
103
|
// adjacent duplicate keyframes.
|
|
101
104
|
void onCommitAnimatedProperties(element, changedProps);
|
|
@@ -111,6 +114,7 @@ function Cube3dControl({
|
|
|
111
114
|
scale: 1,
|
|
112
115
|
transformPerspective: 0,
|
|
113
116
|
};
|
|
117
|
+
track("button", "Reset 3D transform");
|
|
114
118
|
void onCommitAnimatedProperties(element, identity);
|
|
115
119
|
};
|
|
116
120
|
// Immediate element feedback while dragging — set the live transform without a
|
|
@@ -168,6 +172,7 @@ function Cube3dControl({
|
|
|
168
172
|
}
|
|
169
173
|
// One commit for all props so the writes can't race read-modify-write on
|
|
170
174
|
// the same script (which dropped a prop and reverted after a seek).
|
|
175
|
+
track("slider", "3D depth");
|
|
171
176
|
void onCommitAnimatedProperties(element, props);
|
|
172
177
|
}}
|
|
173
178
|
onRecenter={recenter}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { ColorField } from "./propertyPanelColor";
|
|
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
|
+
describe("ColorField flat trigger", () => {
|
|
15
|
+
it("renders label and value inline with a small swatch, no boxed border", () => {
|
|
16
|
+
const host = document.createElement("div");
|
|
17
|
+
document.body.append(host);
|
|
18
|
+
const root = createRoot(host);
|
|
19
|
+
act(() => {
|
|
20
|
+
root.render(<ColorField flat label="Color" value="rgb(255, 176, 32)" onCommit={vi.fn()} />);
|
|
21
|
+
});
|
|
22
|
+
const trigger = host.querySelector<HTMLButtonElement>('[data-flat-color-trigger="true"]');
|
|
23
|
+
expect(trigger).not.toBeNull();
|
|
24
|
+
expect(trigger?.className).not.toContain("border-neutral-800");
|
|
25
|
+
expect(host.textContent).toContain("Color");
|
|
26
|
+
act(() => root.unmount());
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./colorValue";
|
|
12
12
|
import { resolveFloatingPanelPosition, type FloatingPosition } from "./floatingPanel";
|
|
13
13
|
import { colorFromCss, FIELD, LABEL } from "./propertyPanelHelpers";
|
|
14
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
14
15
|
|
|
15
16
|
const COLOR_PICKER_SIZE = { width: 292, height: 386 };
|
|
16
17
|
|
|
@@ -121,13 +122,16 @@ export function ColorField({
|
|
|
121
122
|
label,
|
|
122
123
|
value,
|
|
123
124
|
disabled,
|
|
125
|
+
flat,
|
|
124
126
|
onCommit,
|
|
125
127
|
}: {
|
|
126
128
|
label: string;
|
|
127
129
|
value: string;
|
|
128
130
|
disabled?: boolean;
|
|
131
|
+
flat?: boolean;
|
|
129
132
|
onCommit: (nextValue: string) => void;
|
|
130
133
|
}) {
|
|
134
|
+
const track = useTrackDesignInput();
|
|
131
135
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
132
136
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
133
137
|
const [open, setOpen] = useState(false);
|
|
@@ -201,7 +205,9 @@ export function ColorField({
|
|
|
201
205
|
const commitColor = (nextColor: ParsedColor) => {
|
|
202
206
|
setDraftColor(nextColor);
|
|
203
207
|
setHexDraft(toHexColor(nextColor).toUpperCase());
|
|
204
|
-
|
|
208
|
+
const nextValue = formatCssColor(nextColor);
|
|
209
|
+
if (nextValue !== value) track("color", label);
|
|
210
|
+
onCommit(nextValue);
|
|
205
211
|
};
|
|
206
212
|
|
|
207
213
|
const commitHsv = (nextHsv: { hue?: number; saturation?: number; value?: number }) => {
|
|
@@ -349,6 +355,30 @@ export function ColorField({
|
|
|
349
355
|
}
|
|
350
356
|
};
|
|
351
357
|
|
|
358
|
+
if (flat) {
|
|
359
|
+
return (
|
|
360
|
+
<div className="flex min-h-[30px] items-center justify-between">
|
|
361
|
+
<span className="text-[11px] text-panel-text-2">{label}</span>
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
data-flat-color-trigger="true"
|
|
365
|
+
disabled={disabled}
|
|
366
|
+
aria-label={`Pick ${label.toLowerCase()} color`}
|
|
367
|
+
ref={buttonRef}
|
|
368
|
+
onClick={openPicker}
|
|
369
|
+
className="flex items-center gap-2 disabled:cursor-not-allowed"
|
|
370
|
+
>
|
|
371
|
+
<span
|
|
372
|
+
className="h-4 w-4 flex-shrink-0 rounded-[4px]"
|
|
373
|
+
style={{ backgroundColor: value || "transparent" }}
|
|
374
|
+
/>
|
|
375
|
+
<span className="font-mono text-[11px] text-panel-text-0">{value}</span>
|
|
376
|
+
</button>
|
|
377
|
+
{picker}
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
352
382
|
return (
|
|
353
383
|
<div className="grid min-w-0 gap-1.5">
|
|
354
384
|
<span className={LABEL}>{label}</span>
|
|
@@ -11,6 +11,7 @@ import { ChevronDown, ChevronRight, Plus, X } from "../../icons/SystemIcons";
|
|
|
11
11
|
import { LUT_EXT } from "../../utils/mediaTypes";
|
|
12
12
|
import { LABEL } from "./propertyPanelHelpers";
|
|
13
13
|
import { ColorGradingSliderControl } from "./propertyPanelColorGradingSlider";
|
|
14
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
14
15
|
|
|
15
16
|
const LUT_UPLOAD_DIR = "assets/luts";
|
|
16
17
|
|
|
@@ -175,6 +176,7 @@ export function ColorGradingControls({
|
|
|
175
176
|
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
|
|
176
177
|
onCommitColorGrading: (nextGrading: NormalizedHfColorGrading) => void;
|
|
177
178
|
}) {
|
|
179
|
+
const track = useTrackDesignInput();
|
|
178
180
|
const lutInputRef = useRef<HTMLInputElement>(null);
|
|
179
181
|
const [lutOpen, setLutOpen] = useState(false);
|
|
180
182
|
const [detailSettings, setDetailSettings] = useState<"vignette" | "grain" | null>(null);
|
|
@@ -195,7 +197,10 @@ export function ColorGradingControls({
|
|
|
195
197
|
|
|
196
198
|
const applyPreset = (preset: string) => {
|
|
197
199
|
const next = normalizeHfColorGrading({ preset, intensity: 1, lut: grading.lut });
|
|
198
|
-
if (next)
|
|
200
|
+
if (next) {
|
|
201
|
+
track("select", "Preset");
|
|
202
|
+
onCommitColorGrading(next);
|
|
203
|
+
}
|
|
199
204
|
};
|
|
200
205
|
const updateFilterIntensity = (value: number) => {
|
|
201
206
|
onCommitColorGrading({
|
|
@@ -218,7 +223,10 @@ export function ColorGradingControls({
|
|
|
218
223
|
if (!files?.length || !onImportAssets) return;
|
|
219
224
|
const uploaded = await onImportAssets(files, LUT_UPLOAD_DIR);
|
|
220
225
|
const firstLut = uploaded.find((asset) => LUT_EXT.test(asset));
|
|
221
|
-
if (firstLut)
|
|
226
|
+
if (firstLut) {
|
|
227
|
+
track("button", "Import LUT");
|
|
228
|
+
applyLut(firstLut, 1);
|
|
229
|
+
}
|
|
222
230
|
};
|
|
223
231
|
const commitDetailSlider = (slider: DetailSlider, next: number) => {
|
|
224
232
|
onCommitColorGrading({
|
|
@@ -313,6 +321,7 @@ export function ColorGradingControls({
|
|
|
313
321
|
value={selectedLut}
|
|
314
322
|
onChange={(event) => {
|
|
315
323
|
const nextSrc = event.target.value;
|
|
324
|
+
track("select", "Custom LUT");
|
|
316
325
|
applyLut(
|
|
317
326
|
nextSrc || null,
|
|
318
327
|
nextSrc && grading.lut?.src === nextSrc ? grading.lut.intensity : 1,
|