@hyperframes/studio 0.7.58 → 0.7.60
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-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- 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 +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- 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 +107 -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/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -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/timelineEditingHelpers.ts +2 -2
- 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.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- 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/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- 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 +4 -1
- 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/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- 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 +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- 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/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -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 +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -1,159 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
useEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
type PointerEvent as ReactPointerEvent,
|
|
8
|
-
type RefObject,
|
|
9
|
-
} from "react";
|
|
10
|
-
import {
|
|
11
|
-
HF_COLOR_GRADING_ATTR,
|
|
12
|
-
isHfColorGradingActive,
|
|
13
|
-
normalizeHfColorGrading,
|
|
14
|
-
serializeHfColorGrading,
|
|
15
|
-
type HfColorGradingTarget,
|
|
16
|
-
type NormalizedHfColorGrading,
|
|
17
|
-
} from "@hyperframes/core/color-grading";
|
|
1
|
+
import { type PointerEvent as ReactPointerEvent, type RefObject } from "react";
|
|
2
|
+
import { isHfColorGradingActive } from "@hyperframes/core/color-grading";
|
|
18
3
|
import { Compare, Palette, RotateCcw } from "../../icons/SystemIcons";
|
|
19
|
-
import {
|
|
20
|
-
addStudioPendingEditFlushListener,
|
|
21
|
-
trackStudioPendingEdit,
|
|
22
|
-
} from "../../utils/studioPendingEdits";
|
|
23
4
|
import type { DomEditSelection } from "./domEditing";
|
|
24
5
|
import { ColorGradingControls } from "./propertyPanelColorGradingControls";
|
|
25
|
-
import { stripQueryAndHash } from "./propertyPanelHelpers";
|
|
26
6
|
import { Section } from "./propertyPanelPrimitives";
|
|
27
7
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const RUNTIME_STATUS_REFRESH_DELAYS = [50, 250, 1000, 2500] as const;
|
|
34
|
-
const MEDIA_METADATA_CACHE = new Map<string, MediaMetadata | null>();
|
|
35
|
-
|
|
36
|
-
interface RuntimeColorGradingStatus {
|
|
37
|
-
state: "missing" | "inactive" | "pending" | "active" | "unavailable";
|
|
38
|
-
message: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface MediaMetadata {
|
|
42
|
-
kind: "video" | "image" | "audio" | "unknown";
|
|
43
|
-
color: {
|
|
44
|
-
dynamicRange: "hdr" | "sdr" | "unknown";
|
|
45
|
-
hdrTransfer: "pq" | "hlg" | "unknown" | null;
|
|
46
|
-
label: string;
|
|
47
|
-
isHdr: boolean;
|
|
48
|
-
codecName?: string;
|
|
49
|
-
profile?: string;
|
|
50
|
-
pixelFormat?: string;
|
|
51
|
-
colorSpace?: string;
|
|
52
|
-
colorTransfer?: string;
|
|
53
|
-
colorPrimaries?: string;
|
|
54
|
-
};
|
|
55
|
-
probeError?: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface MediaMetadataResponse {
|
|
59
|
-
path: string;
|
|
60
|
-
metadata: MediaMetadata;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function stripPreviewAssetPath(src: string, projectId: string): string | null {
|
|
64
|
-
let pathname = src;
|
|
65
|
-
try {
|
|
66
|
-
pathname = new URL(src, window.location.href).pathname;
|
|
67
|
-
} catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
const projectMarker = `/api/projects/${encodeURIComponent(projectId)}/preview/`;
|
|
71
|
-
const genericMarker = "/preview/";
|
|
72
|
-
const marker = pathname.includes(projectMarker) ? projectMarker : genericMarker;
|
|
73
|
-
const index = pathname.indexOf(marker);
|
|
74
|
-
if (index < 0) return null;
|
|
75
|
-
const assetPath = decodeURIComponent(pathname.slice(index + marker.length)).replace(/^\/+/, "");
|
|
76
|
-
if (!assetPath || assetPath.startsWith("comp/")) return null;
|
|
77
|
-
return assetPath;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// fallow-ignore-next-line complexity
|
|
81
|
-
function resolveProjectAssetPath(
|
|
82
|
-
sourceFile: string,
|
|
83
|
-
src: string,
|
|
84
|
-
projectId: string,
|
|
85
|
-
): string | null {
|
|
86
|
-
const trimmed = stripQueryAndHash(src.trim());
|
|
87
|
-
if (!trimmed || /^(?:data:|blob:)/i.test(trimmed)) return null;
|
|
88
|
-
if (/^https?:\/\//i.test(trimmed)) return stripPreviewAssetPath(trimmed, projectId);
|
|
89
|
-
if (trimmed.startsWith("/")) {
|
|
90
|
-
return stripPreviewAssetPath(trimmed, projectId);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const sourceDir = sourceFile.includes("/")
|
|
94
|
-
? sourceFile.slice(0, sourceFile.lastIndexOf("/"))
|
|
95
|
-
: "";
|
|
96
|
-
const parts = `${sourceDir}/${trimmed}`.split("/");
|
|
97
|
-
const normalized: string[] = [];
|
|
98
|
-
for (const part of parts) {
|
|
99
|
-
if (!part || part === ".") continue;
|
|
100
|
-
if (part === "..") {
|
|
101
|
-
normalized.pop();
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
normalized.push(part);
|
|
105
|
-
}
|
|
106
|
-
return normalized.join("/") || null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function selectedMediaAssetPath(element: DomEditSelection, projectId: string): string | null {
|
|
110
|
-
if (element.tagName !== "video" && element.tagName !== "img") return null;
|
|
111
|
-
const media = element.element as HTMLImageElement | HTMLVideoElement;
|
|
112
|
-
const src = media.getAttribute("src") || media.currentSrc || "";
|
|
113
|
-
return resolveProjectAssetPath(element.sourceFile || "index.html", src, projectId);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function defaultColorGrading(): NormalizedHfColorGrading {
|
|
117
|
-
const grading = normalizeHfColorGrading("neutral");
|
|
118
|
-
if (!grading) throw new Error("Missing neutral color grading preset");
|
|
119
|
-
return grading;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function readColorGradingFromElement(element: DomEditSelection): NormalizedHfColorGrading {
|
|
123
|
-
return (
|
|
124
|
-
normalizeHfColorGrading(element.dataAttributes[COLOR_GRADING_DATA_KEY]) ?? defaultColorGrading()
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function toBridgeColorGrading(grading: NormalizedHfColorGrading): unknown {
|
|
129
|
-
if (!isHfColorGradingActive(grading)) return null;
|
|
130
|
-
const { enabled: _enabled, ...bridgeGrading } = grading;
|
|
131
|
-
return bridgeGrading;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function readRuntimeColorGradingStatus(
|
|
135
|
-
iframe: HTMLIFrameElement | null | undefined,
|
|
136
|
-
target: HfColorGradingTarget,
|
|
137
|
-
): RuntimeColorGradingStatus {
|
|
138
|
-
try {
|
|
139
|
-
const win = iframe?.contentWindow as
|
|
140
|
-
| (Window & {
|
|
141
|
-
__hf?: {
|
|
142
|
-
colorGrading?: {
|
|
143
|
-
getStatus?: (
|
|
144
|
-
target: HfColorGradingTarget | string | null | undefined,
|
|
145
|
-
) => RuntimeColorGradingStatus;
|
|
146
|
-
};
|
|
147
|
-
};
|
|
148
|
-
})
|
|
149
|
-
| null
|
|
150
|
-
| undefined;
|
|
151
|
-
const status = win?.__hf?.colorGrading?.getStatus?.(target);
|
|
152
|
-
return status ?? { state: "pending", message: "Waiting for runtime" };
|
|
153
|
-
} catch {
|
|
154
|
-
return { state: "unavailable", message: "Preview unavailable" };
|
|
155
|
-
}
|
|
156
|
-
}
|
|
8
|
+
useColorGradingController,
|
|
9
|
+
type MediaMetadata,
|
|
10
|
+
type RuntimeColorGradingStatus,
|
|
11
|
+
} from "./useColorGradingController";
|
|
12
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
157
13
|
|
|
158
14
|
function StatusPill({ status }: { status: RuntimeColorGradingStatus }) {
|
|
159
15
|
const dotClass =
|
|
@@ -213,10 +69,12 @@ function HoldBeforeButton({
|
|
|
213
69
|
disabled: boolean;
|
|
214
70
|
onHoldChange: (holding: boolean) => void;
|
|
215
71
|
}) {
|
|
72
|
+
const track = useTrackDesignInput();
|
|
216
73
|
const startHold = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
217
74
|
if (disabled) return;
|
|
218
75
|
event.preventDefault();
|
|
219
76
|
event.stopPropagation();
|
|
77
|
+
track("toggle", "Compare original");
|
|
220
78
|
onHoldChange(true);
|
|
221
79
|
const release = () => {
|
|
222
80
|
onHoldChange(false);
|
|
@@ -250,7 +108,10 @@ function HoldBeforeButton({
|
|
|
250
108
|
onKeyDown={(event) => {
|
|
251
109
|
if (disabled || (event.key !== " " && event.key !== "Enter")) return;
|
|
252
110
|
event.preventDefault();
|
|
253
|
-
if (!active)
|
|
111
|
+
if (!active) {
|
|
112
|
+
track("toggle", "Compare original");
|
|
113
|
+
onHoldChange(true);
|
|
114
|
+
}
|
|
254
115
|
}}
|
|
255
116
|
onKeyUp={(event) => {
|
|
256
117
|
if (disabled || (event.key !== " " && event.key !== "Enter")) return;
|
|
@@ -283,222 +144,36 @@ export function ColorGradingSection({
|
|
|
283
144
|
assets: string[];
|
|
284
145
|
previewIframeRef?: RefObject<HTMLIFrameElement | null>;
|
|
285
146
|
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
|
|
286
|
-
onSetAttributeLive: (
|
|
147
|
+
onSetAttributeLive: (
|
|
148
|
+
attr: string,
|
|
149
|
+
value: string | null,
|
|
150
|
+
onSettled?: (ok: boolean) => void,
|
|
151
|
+
) => void | Promise<void>;
|
|
287
152
|
onApplyScope?: (
|
|
288
153
|
scope: "source-file" | "project",
|
|
289
154
|
value: string | null,
|
|
290
155
|
) => Promise<{ changedFiles: number; changedElements: number }>;
|
|
291
156
|
}) {
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
latestGradingRef.current = grading;
|
|
313
|
-
compareEnabledRef.current = compareEnabled;
|
|
314
|
-
const target = useMemo(
|
|
315
|
-
(): HfColorGradingTarget => ({
|
|
316
|
-
id: element.id ?? null,
|
|
317
|
-
hfId: element.hfId ?? null,
|
|
318
|
-
selector: element.selector ?? null,
|
|
319
|
-
selectorIndex: element.selectorIndex ?? null,
|
|
320
|
-
}),
|
|
321
|
-
[element.hfId, element.id, element.selector, element.selectorIndex],
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
const refreshRuntimeStatus = useCallback(() => {
|
|
325
|
-
setRuntimeStatus(readRuntimeColorGradingStatus(previewIframeRef?.current, target));
|
|
326
|
-
}, [previewIframeRef, target]);
|
|
327
|
-
|
|
328
|
-
useEffect(() => {
|
|
329
|
-
setMediaMetadata(null);
|
|
330
|
-
if (!selectedAssetPath) return;
|
|
331
|
-
const cacheKey = `${projectId}:${selectedAssetPath}`;
|
|
332
|
-
if (MEDIA_METADATA_CACHE.has(cacheKey)) {
|
|
333
|
-
setMediaMetadata(MEDIA_METADATA_CACHE.get(cacheKey) ?? null);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
const controller = new AbortController();
|
|
337
|
-
fetch(
|
|
338
|
-
`/api/projects/${encodeURIComponent(projectId)}/media/metadata?path=${encodeURIComponent(
|
|
339
|
-
selectedAssetPath,
|
|
340
|
-
)}`,
|
|
341
|
-
{ signal: controller.signal },
|
|
342
|
-
)
|
|
343
|
-
.then((response) => (response.ok ? response.json() : null))
|
|
344
|
-
.then((data: MediaMetadataResponse | null) => {
|
|
345
|
-
if (controller.signal.aborted) return;
|
|
346
|
-
const metadata = data?.metadata ?? null;
|
|
347
|
-
MEDIA_METADATA_CACHE.set(cacheKey, metadata);
|
|
348
|
-
setMediaMetadata(metadata);
|
|
349
|
-
})
|
|
350
|
-
.catch(() => {
|
|
351
|
-
if (!controller.signal.aborted) MEDIA_METADATA_CACHE.set(cacheKey, null);
|
|
352
|
-
});
|
|
353
|
-
return () => controller.abort();
|
|
354
|
-
}, [projectId, selectedAssetPath]);
|
|
355
|
-
|
|
356
|
-
const clearStatusTimers = useCallback(() => {
|
|
357
|
-
for (const timer of statusTimersRef.current) clearTimeout(timer);
|
|
358
|
-
statusTimersRef.current = [];
|
|
359
|
-
}, []);
|
|
360
|
-
|
|
361
|
-
const scheduleRuntimeStatusRefresh = useCallback(() => {
|
|
362
|
-
clearStatusTimers();
|
|
363
|
-
statusTimersRef.current = RUNTIME_STATUS_REFRESH_DELAYS.map((delay) =>
|
|
364
|
-
window.setTimeout(refreshRuntimeStatus, delay),
|
|
365
|
-
);
|
|
366
|
-
}, [clearStatusTimers, refreshRuntimeStatus]);
|
|
367
|
-
|
|
368
|
-
useEffect(() => {
|
|
369
|
-
refreshRuntimeStatus();
|
|
370
|
-
}, [refreshRuntimeStatus]);
|
|
371
|
-
|
|
372
|
-
const persistColorGradingValue = useCallback((value: string | null) => {
|
|
373
|
-
return trackStudioPendingEdit(
|
|
374
|
-
onSetAttributeLiveRef.current(COLOR_GRADING_DATA_KEY, value ?? null),
|
|
375
|
-
);
|
|
376
|
-
}, []);
|
|
377
|
-
|
|
378
|
-
const flushPendingPersist = useCallback(() => {
|
|
379
|
-
if (persistTimerRef.current) {
|
|
380
|
-
clearTimeout(persistTimerRef.current);
|
|
381
|
-
persistTimerRef.current = null;
|
|
382
|
-
}
|
|
383
|
-
if (pendingPersistValueRef.current === undefined) return undefined;
|
|
384
|
-
const value = pendingPersistValueRef.current;
|
|
385
|
-
pendingPersistValueRef.current = undefined;
|
|
386
|
-
return persistColorGradingValue(value);
|
|
387
|
-
}, [persistColorGradingValue]);
|
|
388
|
-
|
|
389
|
-
useEffect(() => addStudioPendingEditFlushListener(flushPendingPersist), [flushPendingPersist]);
|
|
390
|
-
|
|
391
|
-
useEffect(() => {
|
|
392
|
-
return () => {
|
|
393
|
-
clearStatusTimers();
|
|
394
|
-
void flushPendingPersist();
|
|
395
|
-
};
|
|
396
|
-
}, [clearStatusTimers, flushPendingPersist]);
|
|
397
|
-
|
|
398
|
-
const postColorGrading = useCallback(
|
|
399
|
-
(nextGrading: NormalizedHfColorGrading) => {
|
|
400
|
-
postRuntimeControlMessage(previewIframeRef?.current?.contentWindow, "set-color-grading", {
|
|
401
|
-
target,
|
|
402
|
-
grading: toBridgeColorGrading(nextGrading),
|
|
403
|
-
});
|
|
404
|
-
},
|
|
405
|
-
[previewIframeRef, target],
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
const postCompare = useCallback(
|
|
409
|
-
(enabled: boolean) => {
|
|
410
|
-
postRuntimeControlMessage(
|
|
411
|
-
previewIframeRef?.current?.contentWindow,
|
|
412
|
-
"set-color-grading-compare",
|
|
413
|
-
{
|
|
414
|
-
target,
|
|
415
|
-
compare: { enabled, position: 1, lineWidth: 0 },
|
|
416
|
-
},
|
|
417
|
-
);
|
|
418
|
-
},
|
|
419
|
-
[previewIframeRef, target],
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
useEffect(() => {
|
|
423
|
-
const iframe = previewIframeRef?.current;
|
|
424
|
-
if (!iframe) return;
|
|
425
|
-
const refreshAndReplay = () => {
|
|
426
|
-
const nextGrading = latestGradingRef.current;
|
|
427
|
-
const active = isHfColorGradingActive(nextGrading);
|
|
428
|
-
if (active) postColorGrading(nextGrading);
|
|
429
|
-
postCompare(compareEnabledRef.current && active);
|
|
430
|
-
scheduleRuntimeStatusRefresh();
|
|
431
|
-
};
|
|
432
|
-
const onMessage = (event: MessageEvent) => {
|
|
433
|
-
if (event.source !== iframe.contentWindow) return;
|
|
434
|
-
const data = event.data as { source?: unknown; type?: unknown } | null;
|
|
435
|
-
if (data?.source !== "hf-preview" || data.type !== "ready") return;
|
|
436
|
-
if (!acceptStudioRuntimeMessage(data)) return;
|
|
437
|
-
refreshAndReplay();
|
|
438
|
-
};
|
|
439
|
-
iframe.addEventListener("load", refreshAndReplay);
|
|
440
|
-
window.addEventListener("message", onMessage);
|
|
441
|
-
const timer = window.setTimeout(refreshAndReplay, 80);
|
|
442
|
-
return () => {
|
|
443
|
-
iframe.removeEventListener("load", refreshAndReplay);
|
|
444
|
-
window.removeEventListener("message", onMessage);
|
|
445
|
-
window.clearTimeout(timer);
|
|
446
|
-
};
|
|
447
|
-
}, [postColorGrading, postCompare, previewIframeRef, scheduleRuntimeStatusRefresh]);
|
|
448
|
-
|
|
449
|
-
useEffect(
|
|
450
|
-
() => () => {
|
|
451
|
-
postCompare(false);
|
|
452
|
-
},
|
|
453
|
-
[postCompare],
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
const commitColorGrading = useCallback(
|
|
457
|
-
(nextGrading: NormalizedHfColorGrading) => {
|
|
458
|
-
setGrading(nextGrading);
|
|
459
|
-
setRuntimeStatus({ state: "pending", message: "Updating shader" });
|
|
460
|
-
postColorGrading(nextGrading);
|
|
461
|
-
const active = isHfColorGradingActive(nextGrading);
|
|
462
|
-
if (compareEnabledRef.current) {
|
|
463
|
-
postCompare(active);
|
|
464
|
-
if (!active) setCompareEnabled(false);
|
|
465
|
-
}
|
|
466
|
-
scheduleRuntimeStatusRefresh();
|
|
467
|
-
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
|
468
|
-
pendingPersistValueRef.current = isHfColorGradingActive(nextGrading)
|
|
469
|
-
? serializeHfColorGrading(nextGrading)
|
|
470
|
-
: null;
|
|
471
|
-
persistTimerRef.current = setTimeout(() => {
|
|
472
|
-
const value = pendingPersistValueRef.current;
|
|
473
|
-
pendingPersistValueRef.current = undefined;
|
|
474
|
-
persistTimerRef.current = null;
|
|
475
|
-
void persistColorGradingValue(value ?? null);
|
|
476
|
-
}, 350);
|
|
477
|
-
},
|
|
478
|
-
[persistColorGradingValue, postColorGrading, postCompare, scheduleRuntimeStatusRefresh],
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
const commitCompare = useCallback(
|
|
482
|
-
(enabled: boolean) => {
|
|
483
|
-
const nextEnabled = enabled && isHfColorGradingActive(grading);
|
|
484
|
-
setCompareEnabled(nextEnabled);
|
|
485
|
-
if (nextEnabled) postColorGrading(grading);
|
|
486
|
-
postCompare(nextEnabled);
|
|
487
|
-
scheduleRuntimeStatusRefresh();
|
|
488
|
-
},
|
|
489
|
-
[grading, postColorGrading, postCompare, scheduleRuntimeStatusRefresh],
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
const applyToScope = useCallback(async () => {
|
|
493
|
-
if (!onApplyScope || applyBusy) return;
|
|
494
|
-
setApplyBusy(true);
|
|
495
|
-
try {
|
|
496
|
-
const value = isHfColorGradingActive(grading) ? serializeHfColorGrading(grading) : null;
|
|
497
|
-
await onApplyScope(applyScope, value);
|
|
498
|
-
} finally {
|
|
499
|
-
setApplyBusy(false);
|
|
500
|
-
}
|
|
501
|
-
}, [applyBusy, applyScope, grading, onApplyScope]);
|
|
157
|
+
const track = useTrackDesignInput();
|
|
158
|
+
const {
|
|
159
|
+
grading,
|
|
160
|
+
compareEnabled,
|
|
161
|
+
applyScope,
|
|
162
|
+
applyBusy,
|
|
163
|
+
runtimeStatus,
|
|
164
|
+
mediaMetadata,
|
|
165
|
+
commitColorGrading,
|
|
166
|
+
commitCompare,
|
|
167
|
+
setApplyScope,
|
|
168
|
+
applyToScope,
|
|
169
|
+
resetGrading,
|
|
170
|
+
} = useColorGradingController({
|
|
171
|
+
projectId,
|
|
172
|
+
element,
|
|
173
|
+
previewIframeRef,
|
|
174
|
+
onSetAttributeLive,
|
|
175
|
+
onApplyScope,
|
|
176
|
+
});
|
|
502
177
|
|
|
503
178
|
return (
|
|
504
179
|
<Section
|
|
@@ -516,7 +191,8 @@ export function ColorGradingSection({
|
|
|
516
191
|
type="button"
|
|
517
192
|
onClick={(event) => {
|
|
518
193
|
event.stopPropagation();
|
|
519
|
-
|
|
194
|
+
track("button", "Reset color grading");
|
|
195
|
+
resetGrading();
|
|
520
196
|
}}
|
|
521
197
|
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1"
|
|
522
198
|
title="Reset color grading"
|
|
@@ -537,7 +213,10 @@ export function ColorGradingSection({
|
|
|
537
213
|
<div className="mt-4 grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-2">
|
|
538
214
|
<select
|
|
539
215
|
value={applyScope}
|
|
540
|
-
onChange={(event) =>
|
|
216
|
+
onChange={(event) => {
|
|
217
|
+
track("select", "Apply scope");
|
|
218
|
+
setApplyScope(event.currentTarget.value as typeof applyScope);
|
|
219
|
+
}}
|
|
541
220
|
disabled={applyBusy}
|
|
542
221
|
className="w-full min-w-0 rounded-md bg-panel-input px-3 py-2 text-[11px] font-medium text-panel-text-1 outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
543
222
|
title="Choose where to copy these color grading settings"
|
|
@@ -550,6 +229,7 @@ export function ColorGradingSection({
|
|
|
550
229
|
disabled={applyBusy}
|
|
551
230
|
onClick={(event) => {
|
|
552
231
|
event.stopPropagation();
|
|
232
|
+
track("button", "Apply color grading scope");
|
|
553
233
|
void applyToScope();
|
|
554
234
|
}}
|
|
555
235
|
className="h-8 rounded-md bg-panel-input px-3 text-[11px] font-medium text-panel-text-2 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-50"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Minus, Plus, RotateCcw, Settings } from "../../icons/SystemIcons";
|
|
3
3
|
import { LABEL } from "./propertyPanelHelpers";
|
|
4
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
4
5
|
|
|
5
6
|
const SLIDER_THUMB_SIZE = 10;
|
|
6
7
|
const SLIDER_THUMB_RADIUS = SLIDER_THUMB_SIZE / 2;
|
|
@@ -59,9 +60,11 @@ export function ColorGradingSliderControl({
|
|
|
59
60
|
onClick: () => void;
|
|
60
61
|
};
|
|
61
62
|
}) {
|
|
63
|
+
const track = useTrackDesignInput();
|
|
62
64
|
const [draftState, setDraftState] = useState<{ value: number; source: number } | null>(null);
|
|
63
65
|
const [inputDraft, setInputDraft] = useState<{ value: string; source: number } | null>(null);
|
|
64
66
|
const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
67
|
+
const interactionChangedRef = useRef(false);
|
|
65
68
|
const valueRef = useRef(value);
|
|
66
69
|
valueRef.current = value;
|
|
67
70
|
|
|
@@ -92,9 +95,13 @@ export function ColorGradingSliderControl({
|
|
|
92
95
|
(nextValue: number) => {
|
|
93
96
|
const clamped = setLocalDraft(nextValue);
|
|
94
97
|
if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
|
|
98
|
+
if (interactionChangedRef.current) {
|
|
99
|
+
interactionChangedRef.current = false;
|
|
100
|
+
track("slider", label);
|
|
101
|
+
}
|
|
95
102
|
if (clamped !== valueRef.current) onCommit(clamped);
|
|
96
103
|
},
|
|
97
|
-
[onCommit, setLocalDraft],
|
|
104
|
+
[label, onCommit, setLocalDraft, track],
|
|
98
105
|
);
|
|
99
106
|
|
|
100
107
|
const scheduleCommit = useCallback(
|
|
@@ -115,6 +122,7 @@ export function ColorGradingSliderControl({
|
|
|
115
122
|
const commitInputDraft = useCallback(() => {
|
|
116
123
|
const parsed = parseNumericInput(inputValue, scale);
|
|
117
124
|
if (parsed === null) {
|
|
125
|
+
interactionChangedRef.current = false;
|
|
118
126
|
setInputDraft(null);
|
|
119
127
|
return;
|
|
120
128
|
}
|
|
@@ -123,6 +131,7 @@ export function ColorGradingSliderControl({
|
|
|
123
131
|
|
|
124
132
|
const nudge = useCallback(
|
|
125
133
|
(direction: -1 | 1) => {
|
|
134
|
+
interactionChangedRef.current = true;
|
|
126
135
|
commitDraft(draft + step * direction);
|
|
127
136
|
},
|
|
128
137
|
[commitDraft, draft, step],
|
|
@@ -166,6 +175,7 @@ export function ColorGradingSliderControl({
|
|
|
166
175
|
aria-label={`Reset ${label}`}
|
|
167
176
|
onClick={(event) => {
|
|
168
177
|
event.stopPropagation();
|
|
178
|
+
track("button", `Reset ${label}`);
|
|
169
179
|
onReset();
|
|
170
180
|
}}
|
|
171
181
|
className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded text-panel-text-5 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
|
|
@@ -205,7 +215,10 @@ export function ColorGradingSliderControl({
|
|
|
205
215
|
value={draft}
|
|
206
216
|
disabled={disabled}
|
|
207
217
|
aria-label={label}
|
|
208
|
-
onChange={(event) =>
|
|
218
|
+
onChange={(event) => {
|
|
219
|
+
interactionChangedRef.current = true;
|
|
220
|
+
scheduleCommit(Number(event.currentTarget.value));
|
|
221
|
+
}}
|
|
209
222
|
onMouseUp={() => commitDraft(draft)}
|
|
210
223
|
onTouchEnd={() => commitDraft(draft)}
|
|
211
224
|
onBlur={() => commitDraft(draft)}
|
|
@@ -223,9 +236,10 @@ export function ColorGradingSliderControl({
|
|
|
223
236
|
max={max / scale}
|
|
224
237
|
step={step / scale}
|
|
225
238
|
disabled={disabled}
|
|
226
|
-
onChange={(event) =>
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
onChange={(event) => {
|
|
240
|
+
interactionChangedRef.current = true;
|
|
241
|
+
setInputDraft({ value: event.currentTarget.value, source: valueRef.current });
|
|
242
|
+
}}
|
|
229
243
|
onBlur={commitInputDraft}
|
|
230
244
|
onKeyDown={(event) => {
|
|
231
245
|
if (event.key === "Enter") {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
SliderControl,
|
|
17
17
|
} from "./propertyPanelPrimitives";
|
|
18
18
|
import { ColorField } from "./propertyPanelColor";
|
|
19
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
19
20
|
|
|
20
21
|
/* ------------------------------------------------------------------ */
|
|
21
22
|
/* Asset path helpers */
|
|
@@ -87,6 +88,7 @@ export function ImageFillField({
|
|
|
87
88
|
onCommit: (nextValue: string) => void;
|
|
88
89
|
onImportAssets?: (files: FileList) => Promise<string[]>;
|
|
89
90
|
}) {
|
|
91
|
+
const track = useTrackDesignInput();
|
|
90
92
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
91
93
|
const [uploading, setUploading] = useState(false);
|
|
92
94
|
const imageAssets = useMemo(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
|
|
@@ -102,7 +104,10 @@ export function ImageFillField({
|
|
|
102
104
|
try {
|
|
103
105
|
const uploaded = await onImportAssets(files);
|
|
104
106
|
const nextImage = uploaded.find((a) => IMAGE_EXT.test(a));
|
|
105
|
-
if (nextImage)
|
|
107
|
+
if (nextImage) {
|
|
108
|
+
track("button", "Upload image");
|
|
109
|
+
onCommit(`url("${toProjectRootAssetPath(nextImage)}")`);
|
|
110
|
+
}
|
|
106
111
|
} finally {
|
|
107
112
|
setUploading(false);
|
|
108
113
|
}
|
|
@@ -156,6 +161,7 @@ export function ImageFillField({
|
|
|
156
161
|
disabled={disabled}
|
|
157
162
|
onChange={(e) => {
|
|
158
163
|
const next = e.target.value;
|
|
164
|
+
track("select", "Project asset");
|
|
159
165
|
if (!next) {
|
|
160
166
|
onCommit("none");
|
|
161
167
|
return;
|
|
@@ -205,6 +211,7 @@ export function GradientField({
|
|
|
205
211
|
disabled?: boolean;
|
|
206
212
|
onCommit: (nextValue: string) => void;
|
|
207
213
|
}) {
|
|
214
|
+
const track = useTrackDesignInput();
|
|
208
215
|
const previewRef = useRef<HTMLDivElement | null>(null);
|
|
209
216
|
const parsed = parseGradient(value) ?? buildDefaultGradientModel(fallbackColor);
|
|
210
217
|
|
|
@@ -226,11 +233,13 @@ export function GradientField({
|
|
|
226
233
|
? Math.min(100, (parsed.stops.at(-1)?.position ?? 90) + 10)
|
|
227
234
|
: 100,
|
|
228
235
|
);
|
|
236
|
+
track("button", "Add gradient stop");
|
|
229
237
|
commit(nextGradient);
|
|
230
238
|
};
|
|
231
239
|
|
|
232
240
|
const removeStop = (index: number) => {
|
|
233
241
|
if (parsed.stops.length <= 2) return;
|
|
242
|
+
track("button", `Remove gradient stop ${index + 1}`);
|
|
234
243
|
commit({ ...parsed, stops: parsed.stops.filter((_, i) => i !== index) });
|
|
235
244
|
};
|
|
236
245
|
|
|
@@ -263,6 +272,7 @@ export function GradientField({
|
|
|
263
272
|
</div>
|
|
264
273
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
265
274
|
<SegmentedControl
|
|
275
|
+
trackName="Gradient type"
|
|
266
276
|
disabled={disabled}
|
|
267
277
|
value={parsed.kind}
|
|
268
278
|
onChange={(next) => patch({ kind: next as GradientModel["kind"] })}
|
|
@@ -277,7 +287,10 @@ export function GradientField({
|
|
|
277
287
|
type="checkbox"
|
|
278
288
|
checked={parsed.repeating}
|
|
279
289
|
disabled={disabled}
|
|
280
|
-
onChange={(e) =>
|
|
290
|
+
onChange={(e) => {
|
|
291
|
+
track("toggle", "Repeat gradient");
|
|
292
|
+
patch({ repeating: e.target.checked });
|
|
293
|
+
}}
|
|
281
294
|
className="h-4 w-4 rounded border-neutral-700 bg-neutral-950 text-panel-accent focus:ring-panel-accent"
|
|
282
295
|
/>
|
|
283
296
|
Repeat
|
|
@@ -285,15 +298,16 @@ export function GradientField({
|
|
|
285
298
|
<button
|
|
286
299
|
type="button"
|
|
287
300
|
disabled={disabled}
|
|
288
|
-
onClick={() =>
|
|
301
|
+
onClick={() => {
|
|
302
|
+
track("button", "Reverse gradient");
|
|
289
303
|
commit({
|
|
290
304
|
...parsed,
|
|
291
305
|
stops: [...parsed.stops].reverse().map((stop) => ({
|
|
292
306
|
...stop,
|
|
293
307
|
position: 100 - stop.position,
|
|
294
308
|
})),
|
|
295
|
-
})
|
|
296
|
-
}
|
|
309
|
+
});
|
|
310
|
+
}}
|
|
297
311
|
className="inline-flex h-7 items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-950 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white disabled:cursor-not-allowed disabled:text-neutral-600"
|
|
298
312
|
>
|
|
299
313
|
<RotateCcw size={12} />
|
|
@@ -306,6 +320,7 @@ export function GradientField({
|
|
|
306
320
|
<div className="grid gap-1.5">
|
|
307
321
|
<span className={LABEL}>{parsed.kind === "linear" ? "Angle" : "Start angle"}</span>
|
|
308
322
|
<SliderControl
|
|
323
|
+
trackName={parsed.kind === "linear" ? "Angle" : "Start angle"}
|
|
309
324
|
value={parsed.angle}
|
|
310
325
|
min={0}
|
|
311
326
|
max={360}
|
|
@@ -342,6 +357,7 @@ export function GradientField({
|
|
|
342
357
|
<div className="grid min-w-0 gap-1.5">
|
|
343
358
|
<span className={LABEL}>Center X</span>
|
|
344
359
|
<SliderControl
|
|
360
|
+
trackName="Center X"
|
|
345
361
|
value={parsed.centerX}
|
|
346
362
|
min={0}
|
|
347
363
|
max={100}
|
|
@@ -355,6 +371,7 @@ export function GradientField({
|
|
|
355
371
|
<div className="grid min-w-0 gap-1.5">
|
|
356
372
|
<span className={LABEL}>Center Y</span>
|
|
357
373
|
<SliderControl
|
|
374
|
+
trackName="Center Y"
|
|
358
375
|
value={parsed.centerY}
|
|
359
376
|
min={0}
|
|
360
377
|
max={100}
|