@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,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState, type ChangeEvent } from "react";
|
|
2
2
|
import { Plus, Type } from "../../icons/SystemIcons";
|
|
3
3
|
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
|
4
4
|
import type { ImportedFontAsset } from "./fontAssets";
|
|
@@ -7,25 +7,26 @@ import { MetricField, Section, SelectField } from "./propertyPanelPrimitives";
|
|
|
7
7
|
import { ColorField } from "./propertyPanelColor";
|
|
8
8
|
import { FontFamilyField } from "./propertyPanelFont";
|
|
9
9
|
import { PromotableControl } from "./PromotableControl";
|
|
10
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
10
11
|
|
|
11
12
|
/* ------------------------------------------------------------------ */
|
|
12
13
|
/* Text helpers (used only by text section components) */
|
|
13
14
|
/* ------------------------------------------------------------------ */
|
|
14
15
|
|
|
15
|
-
function formatTextFieldPreview(value: string): string {
|
|
16
|
+
export function formatTextFieldPreview(value: string): string {
|
|
16
17
|
const collapsed = value.trim().replace(/\s+/g, " ");
|
|
17
18
|
if (collapsed.length <= 56) return collapsed;
|
|
18
19
|
return `${collapsed.slice(0, 55)}…`;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function getTextFieldColor(
|
|
22
|
+
export function getTextFieldColor(
|
|
22
23
|
field: { computedStyles: Record<string, string> },
|
|
23
24
|
inheritedStyles: Record<string, string>,
|
|
24
25
|
): string {
|
|
25
26
|
return field.computedStyles.color || inheritedStyles.color || "rgb(0, 0, 0)";
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function getTextStyleValue(
|
|
29
|
+
export function getTextStyleValue(
|
|
29
30
|
field: { computedStyles: Record<string, string> },
|
|
30
31
|
inheritedStyles: Record<string, string>,
|
|
31
32
|
property: string,
|
|
@@ -35,7 +36,7 @@ function getTextStyleValue(
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const ALL_WEIGHTS = ["100", "200", "300", "400", "500", "600", "700", "800", "900"];
|
|
38
|
-
const WEIGHT_LABELS: Record<string, string> = {
|
|
39
|
+
export const WEIGHT_LABELS: Record<string, string> = {
|
|
39
40
|
"100": "100 · Thin",
|
|
40
41
|
"200": "200 · Extra Light",
|
|
41
42
|
"300": "300 · Light",
|
|
@@ -47,7 +48,7 @@ const WEIGHT_LABELS: Record<string, string> = {
|
|
|
47
48
|
"900": "900 · Black",
|
|
48
49
|
};
|
|
49
50
|
|
|
50
|
-
function detectAvailableWeights(fontFamily: string): string[] {
|
|
51
|
+
export function detectAvailableWeights(fontFamily: string): string[] {
|
|
51
52
|
const fonts = document.fonts;
|
|
52
53
|
if (!fonts) return ALL_WEIGHTS;
|
|
53
54
|
const family = fontFamily.split(",")[0]?.trim().replace(/['"]/g, "");
|
|
@@ -59,22 +60,26 @@ function detectAvailableWeights(fontFamily: string): string[] {
|
|
|
59
60
|
return available.length > 0 ? available : ALL_WEIGHTS;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
function TextAreaField({
|
|
63
|
+
export function TextAreaField({
|
|
63
64
|
label,
|
|
64
65
|
value,
|
|
65
66
|
disabled,
|
|
66
67
|
autoFocus,
|
|
68
|
+
flat,
|
|
67
69
|
onCommit,
|
|
68
70
|
}: {
|
|
69
71
|
label: string;
|
|
70
72
|
value: string;
|
|
71
73
|
disabled?: boolean;
|
|
72
74
|
autoFocus?: boolean;
|
|
75
|
+
flat?: boolean;
|
|
73
76
|
onCommit: (nextValue: string) => void;
|
|
74
77
|
}) {
|
|
78
|
+
const track = useTrackDesignInput();
|
|
75
79
|
const [draft, setDraft] = useState(value);
|
|
76
80
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
77
81
|
const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
82
|
+
const interactionChangedRef = useRef(false);
|
|
78
83
|
const focusedRef = useRef(false);
|
|
79
84
|
const valueRef = useRef(value);
|
|
80
85
|
valueRef.current = value;
|
|
@@ -96,15 +101,58 @@ function TextAreaField({
|
|
|
96
101
|
|
|
97
102
|
const commitDraft = (d: string) => {
|
|
98
103
|
if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
|
|
104
|
+
if (interactionChangedRef.current) {
|
|
105
|
+
interactionChangedRef.current = false;
|
|
106
|
+
track("text", label);
|
|
107
|
+
}
|
|
99
108
|
if (d !== valueRef.current) onCommit(d);
|
|
100
109
|
};
|
|
101
110
|
const scheduleCommit = (d: string) => {
|
|
102
111
|
if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
|
|
103
112
|
commitTimerRef.current = setTimeout(() => {
|
|
104
|
-
if (d !== valueRef.current)
|
|
113
|
+
if (d !== valueRef.current) {
|
|
114
|
+
if (interactionChangedRef.current) {
|
|
115
|
+
interactionChangedRef.current = false;
|
|
116
|
+
track("text", label);
|
|
117
|
+
}
|
|
118
|
+
onCommit(d);
|
|
119
|
+
}
|
|
105
120
|
}, 120);
|
|
106
121
|
};
|
|
107
122
|
|
|
123
|
+
const handleFocus = () => {
|
|
124
|
+
focusedRef.current = true;
|
|
125
|
+
};
|
|
126
|
+
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
127
|
+
setDraft(e.target.value);
|
|
128
|
+
interactionChangedRef.current = true;
|
|
129
|
+
scheduleCommit(e.target.value);
|
|
130
|
+
};
|
|
131
|
+
const handleBlur = () => {
|
|
132
|
+
focusedRef.current = false;
|
|
133
|
+
commitDraft(draft);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (flat) {
|
|
137
|
+
return (
|
|
138
|
+
<div className="border-l-2 border-panel-border-input py-0.5 pl-[10px]">
|
|
139
|
+
<div className="mb-[3px] text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
140
|
+
{label}
|
|
141
|
+
</div>
|
|
142
|
+
<textarea
|
|
143
|
+
ref={textareaRef}
|
|
144
|
+
value={draft}
|
|
145
|
+
disabled={disabled}
|
|
146
|
+
rows={2}
|
|
147
|
+
onFocus={handleFocus}
|
|
148
|
+
onChange={handleChange}
|
|
149
|
+
onBlur={handleBlur}
|
|
150
|
+
className="w-full resize-none bg-transparent font-mono text-[11px] leading-normal text-panel-text-0 outline-none disabled:cursor-not-allowed disabled:text-panel-text-4"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
108
156
|
return (
|
|
109
157
|
<label className="grid min-w-0 gap-1.5">
|
|
110
158
|
<span className={LABEL}>{label}</span>
|
|
@@ -114,17 +162,9 @@ function TextAreaField({
|
|
|
114
162
|
value={draft}
|
|
115
163
|
disabled={disabled}
|
|
116
164
|
rows={4}
|
|
117
|
-
onFocus={
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
onChange={(e) => {
|
|
121
|
-
setDraft(e.target.value);
|
|
122
|
-
scheduleCommit(e.target.value);
|
|
123
|
-
}}
|
|
124
|
-
onBlur={() => {
|
|
125
|
-
focusedRef.current = false;
|
|
126
|
-
commitDraft(draft);
|
|
127
|
-
}}
|
|
165
|
+
onFocus={handleFocus}
|
|
166
|
+
onChange={handleChange}
|
|
167
|
+
onBlur={handleBlur}
|
|
128
168
|
className="w-full resize-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
|
|
129
169
|
/>
|
|
130
170
|
</div>
|
|
@@ -143,6 +183,7 @@ function FontWeightField({
|
|
|
143
183
|
fontFamily?: string;
|
|
144
184
|
onCommit: (nextValue: string) => void;
|
|
145
185
|
}) {
|
|
186
|
+
const track = useTrackDesignInput();
|
|
146
187
|
const options = fontFamily ? detectAvailableWeights(fontFamily) : ALL_WEIGHTS;
|
|
147
188
|
const displayOptions = value && !options.includes(value) ? [value, ...options] : options;
|
|
148
189
|
return (
|
|
@@ -152,7 +193,10 @@ function FontWeightField({
|
|
|
152
193
|
<select
|
|
153
194
|
value={value}
|
|
154
195
|
disabled={disabled}
|
|
155
|
-
onChange={(e) =>
|
|
196
|
+
onChange={(e) => {
|
|
197
|
+
track("select", "Weight");
|
|
198
|
+
onCommit(e.target.value);
|
|
199
|
+
}}
|
|
156
200
|
className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
|
|
157
201
|
>
|
|
158
202
|
{displayOptions.map((o) => (
|
|
@@ -262,6 +306,7 @@ function TextFieldEditor({
|
|
|
262
306
|
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
|
263
307
|
onRemoveTextField: (fieldKey: string) => void;
|
|
264
308
|
}) {
|
|
309
|
+
const track = useTrackDesignInput();
|
|
265
310
|
return (
|
|
266
311
|
<div className="space-y-3">
|
|
267
312
|
<div className={showRemove ? "flex min-w-0 items-center justify-between gap-2" : "min-w-0"}>
|
|
@@ -274,7 +319,10 @@ function TextFieldEditor({
|
|
|
274
319
|
{showRemove && (
|
|
275
320
|
<button
|
|
276
321
|
type="button"
|
|
277
|
-
onClick={() =>
|
|
322
|
+
onClick={() => {
|
|
323
|
+
track("button", "Remove text field");
|
|
324
|
+
onRemoveTextField(field.key);
|
|
325
|
+
}}
|
|
278
326
|
className="inline-flex h-7 flex-shrink-0 items-center 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"
|
|
279
327
|
>
|
|
280
328
|
Remove
|
|
@@ -356,6 +404,7 @@ export function TextSection({
|
|
|
356
404
|
onSetTextFieldStyle,
|
|
357
405
|
onAddTextField,
|
|
358
406
|
onRemoveTextField,
|
|
407
|
+
hideOwnHeading = false,
|
|
359
408
|
}: {
|
|
360
409
|
element: DomEditSelection;
|
|
361
410
|
styles: Record<string, string>;
|
|
@@ -365,7 +414,13 @@ export function TextSection({
|
|
|
365
414
|
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
|
366
415
|
onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
|
|
367
416
|
onRemoveTextField: (fieldKey: string) => void;
|
|
417
|
+
/** Skip TextSection's own "Text" Section heading/wrapper — for callers (the
|
|
418
|
+
* flat inspector's multi-field fallback) that already render their own
|
|
419
|
+
* "Text" heading one level up, to avoid a doubled heading. Defaults to
|
|
420
|
+
* false so the legacy (non-flat) call site is unaffected. */
|
|
421
|
+
hideOwnHeading?: boolean;
|
|
368
422
|
}) {
|
|
423
|
+
const track = useTrackDesignInput();
|
|
369
424
|
const hasTextControls = isTextEditableSelection(element);
|
|
370
425
|
const [activeTextFieldKey, setActiveTextFieldKey] = useState<string | null>(
|
|
371
426
|
element.textFields[0]?.key ?? null,
|
|
@@ -386,85 +441,94 @@ export function TextSection({
|
|
|
386
441
|
if (!activeField) return null;
|
|
387
442
|
|
|
388
443
|
if (textFields.length === 1) {
|
|
444
|
+
const content = (
|
|
445
|
+
<TextFieldEditor
|
|
446
|
+
field={activeField}
|
|
447
|
+
styles={styles}
|
|
448
|
+
fontAssets={fontAssets}
|
|
449
|
+
onImportFonts={onImportFonts}
|
|
450
|
+
showRemove={false}
|
|
451
|
+
onSetText={onSetText}
|
|
452
|
+
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
453
|
+
onRemoveTextField={onRemoveTextField}
|
|
454
|
+
/>
|
|
455
|
+
);
|
|
456
|
+
if (hideOwnHeading) return content;
|
|
389
457
|
return (
|
|
390
458
|
<Section title="Text" icon={<Type size={15} />} defaultCollapsed>
|
|
391
|
-
|
|
392
|
-
field={activeField}
|
|
393
|
-
styles={styles}
|
|
394
|
-
fontAssets={fontAssets}
|
|
395
|
-
onImportFonts={onImportFonts}
|
|
396
|
-
showRemove={false}
|
|
397
|
-
onSetText={onSetText}
|
|
398
|
-
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
399
|
-
onRemoveTextField={onRemoveTextField}
|
|
400
|
-
/>
|
|
459
|
+
{content}
|
|
401
460
|
</Section>
|
|
402
461
|
);
|
|
403
462
|
}
|
|
404
463
|
|
|
405
|
-
|
|
406
|
-
<
|
|
407
|
-
<div className="
|
|
408
|
-
<div className="
|
|
409
|
-
<
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
</span>
|
|
447
|
-
</div>
|
|
448
|
-
<span className="flex-shrink-0 rounded-md border border-neutral-700 bg-neutral-950 px-1.5 py-0.5 text-[10px] text-neutral-500">
|
|
449
|
-
{field.tagName}
|
|
464
|
+
const content = (
|
|
465
|
+
<div className="space-y-4">
|
|
466
|
+
<div className="grid gap-1.5">
|
|
467
|
+
<div className="flex min-w-0 flex-wrap items-center justify-between gap-2">
|
|
468
|
+
<span className={LABEL}>Text layers</span>
|
|
469
|
+
<button
|
|
470
|
+
type="button"
|
|
471
|
+
onClick={() => {
|
|
472
|
+
track("button", "Add text field");
|
|
473
|
+
void Promise.resolve(onAddTextField(activeField.key)).then((nextKey) => {
|
|
474
|
+
if (nextKey) setActiveTextFieldKey(nextKey);
|
|
475
|
+
});
|
|
476
|
+
}}
|
|
477
|
+
className="inline-flex h-7 max-w-full 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"
|
|
478
|
+
>
|
|
479
|
+
<Plus size={12} className="flex-shrink-0" />
|
|
480
|
+
<span className="truncate">Add text</span>
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
<div className="grid gap-2">
|
|
484
|
+
{textFields.map((field, index) => {
|
|
485
|
+
const active = field.key === activeField.key;
|
|
486
|
+
return (
|
|
487
|
+
<button
|
|
488
|
+
key={field.key}
|
|
489
|
+
type="button"
|
|
490
|
+
onClick={() => setActiveTextFieldKey(field.key)}
|
|
491
|
+
className={`min-w-0 w-full rounded-xl border px-3 py-2 text-left transition-colors ${
|
|
492
|
+
active
|
|
493
|
+
? "border-studio-accent/50 bg-studio-accent/10"
|
|
494
|
+
: "border-neutral-800 bg-neutral-900/80 hover:border-neutral-700 hover:bg-neutral-900"
|
|
495
|
+
}`}
|
|
496
|
+
>
|
|
497
|
+
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
498
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
499
|
+
<span
|
|
500
|
+
className="h-4 w-4 flex-shrink-0 rounded border border-neutral-700 bg-neutral-950"
|
|
501
|
+
style={{ backgroundColor: getTextFieldColor(field, styles) }}
|
|
502
|
+
/>
|
|
503
|
+
<span className="min-w-0 truncate text-[11px] font-medium text-neutral-100">
|
|
504
|
+
{formatTextFieldPreview(field.value) || `Text ${index + 1}`}
|
|
450
505
|
</span>
|
|
451
506
|
</div>
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
507
|
+
<span className="flex-shrink-0 rounded-md border border-neutral-700 bg-neutral-950 px-1.5 py-0.5 text-[10px] text-neutral-500">
|
|
508
|
+
{field.tagName}
|
|
509
|
+
</span>
|
|
510
|
+
</div>
|
|
511
|
+
</button>
|
|
512
|
+
);
|
|
513
|
+
})}
|
|
456
514
|
</div>
|
|
457
|
-
<TextFieldEditor
|
|
458
|
-
field={activeField}
|
|
459
|
-
styles={styles}
|
|
460
|
-
fontAssets={fontAssets}
|
|
461
|
-
onImportFonts={onImportFonts}
|
|
462
|
-
showRemove={true}
|
|
463
|
-
onSetText={onSetText}
|
|
464
|
-
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
465
|
-
onRemoveTextField={onRemoveTextField}
|
|
466
|
-
/>
|
|
467
515
|
</div>
|
|
516
|
+
<TextFieldEditor
|
|
517
|
+
field={activeField}
|
|
518
|
+
styles={styles}
|
|
519
|
+
fontAssets={fontAssets}
|
|
520
|
+
onImportFonts={onImportFonts}
|
|
521
|
+
showRemove={true}
|
|
522
|
+
onSetText={onSetText}
|
|
523
|
+
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
524
|
+
onRemoveTextField={onRemoveTextField}
|
|
525
|
+
/>
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
if (hideOwnHeading) return content;
|
|
529
|
+
return (
|
|
530
|
+
<Section title="Text" icon={<Type size={15} />}>
|
|
531
|
+
{content}
|
|
468
532
|
</Section>
|
|
469
533
|
);
|
|
470
534
|
}
|
|
@@ -47,6 +47,7 @@ export function StyleSections({
|
|
|
47
47
|
onSetStyle,
|
|
48
48
|
onImportAssets,
|
|
49
49
|
gsapBorderRadius,
|
|
50
|
+
hideFlex = false,
|
|
50
51
|
}: {
|
|
51
52
|
projectId: string;
|
|
52
53
|
element: DomEditSelection;
|
|
@@ -55,6 +56,10 @@ export function StyleSections({
|
|
|
55
56
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
56
57
|
onImportAssets?: (files: FileList) => Promise<string[]>;
|
|
57
58
|
gsapBorderRadius?: { tl: number; tr: number; br: number; bl: number } | null;
|
|
59
|
+
// When true, the Flex `Section` is suppressed. The flat inspector renders
|
|
60
|
+
// its own Flex controls inside the Layout group (LayoutFlexBlock), so the
|
|
61
|
+
// flat path passes this to avoid a double-render. Non-flat callers omit it.
|
|
62
|
+
hideFlex?: boolean;
|
|
58
63
|
}) {
|
|
59
64
|
const styleEditingDisabled = !element.capabilities.canEditStyles;
|
|
60
65
|
const isFlex = styles.display === "flex" || styles.display === "inline-flex";
|
|
@@ -145,10 +150,11 @@ export function StyleSections({
|
|
|
145
150
|
|
|
146
151
|
return (
|
|
147
152
|
<>
|
|
148
|
-
{isFlex && (
|
|
153
|
+
{isFlex && !hideFlex && (
|
|
149
154
|
<Section title="Flex" icon={<Layers size={15} />} defaultCollapsed>
|
|
150
155
|
<div className="space-y-4">
|
|
151
156
|
<SegmentedControl
|
|
157
|
+
trackName="Flex direction"
|
|
152
158
|
disabled={styleEditingDisabled}
|
|
153
159
|
value={styles["flex-direction"] || "row"}
|
|
154
160
|
onChange={(next) => onSetStyle("flex-direction", next)}
|
|
@@ -293,6 +299,7 @@ export function StyleSections({
|
|
|
293
299
|
<div className="grid min-w-0 gap-1.5">
|
|
294
300
|
<span className={LABEL}>Layer blur</span>
|
|
295
301
|
<SliderControl
|
|
302
|
+
trackName="Layer blur"
|
|
296
303
|
value={filterBlurValue}
|
|
297
304
|
min={0}
|
|
298
305
|
max={Math.max(40, Math.ceil(filterBlurValue))}
|
|
@@ -308,6 +315,7 @@ export function StyleSections({
|
|
|
308
315
|
<div className="grid min-w-0 gap-1.5">
|
|
309
316
|
<span className={LABEL}>Backdrop</span>
|
|
310
317
|
<SliderControl
|
|
318
|
+
trackName="Backdrop blur"
|
|
311
319
|
value={backdropBlurValue}
|
|
312
320
|
min={0}
|
|
313
321
|
max={Math.max(60, Math.ceil(backdropBlurValue))}
|
|
@@ -358,6 +366,7 @@ export function StyleSections({
|
|
|
358
366
|
<div className="grid min-w-0 gap-1.5">
|
|
359
367
|
<span className={LABEL}>Mask inset</span>
|
|
360
368
|
<SliderControl
|
|
369
|
+
trackName="Mask inset"
|
|
361
370
|
value={clipInsetValue}
|
|
362
371
|
min={0}
|
|
363
372
|
max={Math.max(120, Math.ceil(clipInsetValue))}
|
|
@@ -406,6 +415,7 @@ export function StyleSections({
|
|
|
406
415
|
<Section title="Transparency" icon={<Eye size={15} />} defaultCollapsed>
|
|
407
416
|
<div className="space-y-4">
|
|
408
417
|
<SliderControl
|
|
418
|
+
trackName="Opacity"
|
|
409
419
|
value={opacityValue}
|
|
410
420
|
min={0}
|
|
411
421
|
max={100}
|
|
@@ -428,6 +438,7 @@ export function StyleSections({
|
|
|
428
438
|
<Section title="Fill" icon={<Palette size={15} />}>
|
|
429
439
|
<div className="space-y-4">
|
|
430
440
|
<SegmentedControl
|
|
441
|
+
trackName="Fill type"
|
|
431
442
|
disabled={styleEditingDisabled}
|
|
432
443
|
value={preferredFillMode}
|
|
433
444
|
onChange={handleFillModeChange}
|
|
@@ -4,7 +4,7 @@ import type { DomEditSelection } from "./domEditing";
|
|
|
4
4
|
import { formatTimingValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
5
5
|
import { MetricField, Section } from "./propertyPanelPrimitives";
|
|
6
6
|
|
|
7
|
-
function parseTimingValue(input: string): number | null {
|
|
7
|
+
export function parseTimingValue(input: string): number | null {
|
|
8
8
|
const cleaned = input.replace(/s$/i, "").trim();
|
|
9
9
|
const parsed = Number.parseFloat(cleaned);
|
|
10
10
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
@@ -25,12 +25,25 @@ export interface PropertyPanelProps {
|
|
|
25
25
|
assets: string[];
|
|
26
26
|
element: DomEditSelection | null;
|
|
27
27
|
multiSelectCount?: number;
|
|
28
|
+
multiSelectedElements?: DomEditSelection[];
|
|
29
|
+
onGroupSelection?: () => void;
|
|
30
|
+
onHideAllSelected?: () => void;
|
|
28
31
|
copiedAgentPrompt: boolean;
|
|
29
32
|
onClearSelection: () => void;
|
|
30
33
|
onUngroup?: () => void;
|
|
31
34
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
32
35
|
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
33
|
-
|
|
36
|
+
/** Commits several data-* attributes on the SAME element in ONE atomic
|
|
37
|
+
* persist call — e.g. a pinned timing range's start+duration together, so
|
|
38
|
+
* a selection change or a partial failure mid-commit can't misdirect one
|
|
39
|
+
* of the two writes or leave them half-applied. Falls back to sequential
|
|
40
|
+
* `onSetAttribute` calls where omitted. */
|
|
41
|
+
onSetAttributes?: (selection: DomEditSelection, attrs: Record<string, string>) => Promise<void>;
|
|
42
|
+
onSetAttributeLive: (
|
|
43
|
+
attr: string,
|
|
44
|
+
value: string | null,
|
|
45
|
+
onSettled?: (ok: boolean) => void,
|
|
46
|
+
) => void | Promise<void>;
|
|
34
47
|
onApplyColorGradingScope?: (
|
|
35
48
|
scope: "source-file" | "project",
|
|
36
49
|
value: string | null,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolveValueTier,
|
|
4
|
+
VALUE_TIER_LABEL_CLASS,
|
|
5
|
+
VALUE_TIER_VALUE_CLASS,
|
|
6
|
+
} from "./propertyPanelValueTier";
|
|
7
|
+
|
|
8
|
+
describe("resolveValueTier", () => {
|
|
9
|
+
it("is 'default' when there is no explicit declaration", () => {
|
|
10
|
+
expect(resolveValueTier(undefined, "400")).toBe("default");
|
|
11
|
+
expect(resolveValueTier("", "400")).toBe("default");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("is 'explicitDefault' when the explicit value equals the default", () => {
|
|
15
|
+
expect(resolveValueTier("400", "400")).toBe("explicitDefault");
|
|
16
|
+
expect(resolveValueTier(" normal ", "normal")).toBe("explicitDefault");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("is 'explicitCustom' when the explicit value differs from the default", () => {
|
|
20
|
+
expect(resolveValueTier("3.96px", "0px")).toBe("explicitCustom");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("value tier class maps", () => {
|
|
25
|
+
it("covers all three tiers for both label and value", () => {
|
|
26
|
+
for (const tier of ["default", "explicitDefault", "explicitCustom"] as const) {
|
|
27
|
+
expect(VALUE_TIER_LABEL_CLASS[tier]).toBeTruthy();
|
|
28
|
+
expect(VALUE_TIER_VALUE_CLASS[tier]).toBeTruthy();
|
|
29
|
+
}
|
|
30
|
+
expect(VALUE_TIER_VALUE_CLASS.explicitCustom).toBe("text-panel-accent");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flat inspector's 3-state value coloring (design_handoff_studio_inspector,
|
|
3
|
+
* verified against Studio Panel Redesign.dc.html #10a): a property row is either
|
|
4
|
+
* unset (no explicit declaration), explicitly declared but equal to its default
|
|
5
|
+
* (no visual "set" signal), or explicitly declared and different from its default
|
|
6
|
+
* (mint value + emphasized label + reset affordance).
|
|
7
|
+
*/
|
|
8
|
+
export type PropertyValueTier = "default" | "explicitDefault" | "explicitCustom";
|
|
9
|
+
|
|
10
|
+
export function resolveValueTier(
|
|
11
|
+
explicitValue: string | undefined,
|
|
12
|
+
defaultValue: string,
|
|
13
|
+
): PropertyValueTier {
|
|
14
|
+
if (explicitValue == null || explicitValue.trim() === "") return "default";
|
|
15
|
+
return explicitValue.trim() === defaultValue.trim() ? "explicitDefault" : "explicitCustom";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const VALUE_TIER_LABEL_CLASS: Record<PropertyValueTier, string> = {
|
|
19
|
+
default: "text-panel-text-3",
|
|
20
|
+
explicitDefault: "text-panel-text-2",
|
|
21
|
+
explicitCustom: "text-panel-text-0",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const VALUE_TIER_VALUE_CLASS: Record<PropertyValueTier, string> = {
|
|
25
|
+
default: "text-panel-text-3",
|
|
26
|
+
explicitDefault: "text-panel-text-0",
|
|
27
|
+
explicitCustom: "text-panel-accent",
|
|
28
|
+
};
|