@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
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { resolveEditingSections } from "@hyperframes/core/editing";
|
|
3
|
+
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
|
4
|
+
import { slugifyDesignInput } from "../../utils/designInputTracking";
|
|
5
|
+
import type { DomEditSelection } from "./domEditing";
|
|
6
|
+
import { isTextEditableSelection } from "./domEditing";
|
|
7
|
+
import type { PropertyPanelProps } from "./propertyPanelHelpers";
|
|
8
|
+
import { formatPxMetricValue } from "./propertyPanelHelpers";
|
|
9
|
+
import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
|
|
10
|
+
import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter";
|
|
11
|
+
import { FlatGroupHeader } from "./propertyPanelFlatPrimitives";
|
|
12
|
+
import { FlatTextSection } from "./propertyPanelFlatTextSection";
|
|
13
|
+
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
|
|
14
|
+
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
|
|
15
|
+
import { FlatMotionSection } from "./propertyPanelFlatMotionSection";
|
|
16
|
+
import { FlatMediaSection } from "./propertyPanelFlatMediaSection";
|
|
17
|
+
import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation";
|
|
18
|
+
import { createGsapLivePreview } from "./gsapLivePreview";
|
|
19
|
+
import { formatTextFieldPreview } from "./propertyPanelSections";
|
|
20
|
+
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
|
|
21
|
+
import { useColorGradingController } from "./useColorGradingController";
|
|
22
|
+
import {
|
|
23
|
+
FlatColorGradingAccessory,
|
|
24
|
+
FlatColorGradingSection,
|
|
25
|
+
} from "./propertyPanelFlatColorGradingSection";
|
|
26
|
+
|
|
27
|
+
type EditingSections = ReturnType<typeof resolveEditingSections>;
|
|
28
|
+
|
|
29
|
+
type FlatGroupDescriptor = {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
summary?: string;
|
|
33
|
+
accessory?: ReactNode;
|
|
34
|
+
content: ReactNode;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Type-only fallback for the Motion effect-card callbacks. Used solely to
|
|
38
|
+
// satisfy FlatMotionSection's required-callback shape when the effect list is
|
|
39
|
+
// gated off (showEffects === false, so none of these are ever invoked). Keeps
|
|
40
|
+
// the gated-off path free of `!` non-null assertions — the real, narrowed
|
|
41
|
+
// handlers flow through only when the double-gate below passes.
|
|
42
|
+
const EMPTY_GSAP_EFFECT_HANDLERS = {
|
|
43
|
+
onAddAnimation: () => {},
|
|
44
|
+
onUpdateProperty: () => {},
|
|
45
|
+
onUpdateMeta: () => {},
|
|
46
|
+
onDeleteAnimation: () => {},
|
|
47
|
+
onAddProperty: () => {},
|
|
48
|
+
onRemoveProperty: () => {},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The flat "Ledger" inspector shell (design_handoff_studio_inspector).
|
|
53
|
+
*
|
|
54
|
+
* Extracted from PropertyPanel so that file stays under the 600-LOC gate
|
|
55
|
+
* (same one-directional-import precedent as FlatTextSection). Rendered only
|
|
56
|
+
* when STUDIO_FLAT_INSPECTOR_ENABLED is on; owns the one-open group state.
|
|
57
|
+
*
|
|
58
|
+
* The Text/Style/Layout/Motion/Media/Grade groups share the one-open accordion.
|
|
59
|
+
*/
|
|
60
|
+
// fallow-ignore-next-line complexity
|
|
61
|
+
export function PropertyPanelFlat({
|
|
62
|
+
element,
|
|
63
|
+
styles,
|
|
64
|
+
sections,
|
|
65
|
+
sourceLabel,
|
|
66
|
+
gsapAnimations = [],
|
|
67
|
+
gsapBorderRadius,
|
|
68
|
+
fontAssets = [],
|
|
69
|
+
showEditableSections,
|
|
70
|
+
selectedElementHidden,
|
|
71
|
+
selectedElementId,
|
|
72
|
+
clipboardCopied,
|
|
73
|
+
onCopyElementInfo,
|
|
74
|
+
projectId,
|
|
75
|
+
projectDir,
|
|
76
|
+
assets,
|
|
77
|
+
previewIframeRef,
|
|
78
|
+
onClearSelection,
|
|
79
|
+
onUngroup,
|
|
80
|
+
onSetStyle,
|
|
81
|
+
onSetAttribute,
|
|
82
|
+
onSetAttributes,
|
|
83
|
+
onSetAttributeLive,
|
|
84
|
+
onApplyColorGradingScope,
|
|
85
|
+
onSetHtmlAttribute,
|
|
86
|
+
onRemoveBackground,
|
|
87
|
+
onSetText,
|
|
88
|
+
onSetTextFieldStyle,
|
|
89
|
+
onAddTextField,
|
|
90
|
+
onRemoveTextField,
|
|
91
|
+
onAskAgent,
|
|
92
|
+
onToggleElementHidden,
|
|
93
|
+
onImportAssets,
|
|
94
|
+
onImportFonts,
|
|
95
|
+
recordingState,
|
|
96
|
+
recordingDuration,
|
|
97
|
+
onToggleRecording,
|
|
98
|
+
displayX,
|
|
99
|
+
displayY,
|
|
100
|
+
displayW,
|
|
101
|
+
displayH,
|
|
102
|
+
displayR,
|
|
103
|
+
manualOffsetEditingDisabled,
|
|
104
|
+
manualSizeEditingDisabled,
|
|
105
|
+
manualRotationEditingDisabled,
|
|
106
|
+
commitManualOffset,
|
|
107
|
+
commitManualSize,
|
|
108
|
+
commitManualRotation,
|
|
109
|
+
gsapAnimId,
|
|
110
|
+
navKeyframes,
|
|
111
|
+
currentTime,
|
|
112
|
+
animIdForProp,
|
|
113
|
+
gsapRuntimeValues,
|
|
114
|
+
// Renamed: PropertyPanel.tsx still computes/passes these for its own legacy
|
|
115
|
+
// (non-flat) panel, but the flat path recomputes its own basis below via
|
|
116
|
+
// deriveElementTiming so it agrees with Motion's Timing row — ignore the
|
|
117
|
+
// parent's naive `elDuration ?? 1` fallback.
|
|
118
|
+
elStart: _elStart,
|
|
119
|
+
elDuration: _elDuration,
|
|
120
|
+
onCommitAnimatedProperty,
|
|
121
|
+
onCommitAnimatedProperties,
|
|
122
|
+
onSeekToTime,
|
|
123
|
+
onRemoveKeyframe,
|
|
124
|
+
onConvertToKeyframes,
|
|
125
|
+
gsapMultipleTimelines,
|
|
126
|
+
gsapUnsupportedTimelinePattern,
|
|
127
|
+
onUpdateGsapProperty,
|
|
128
|
+
onUpdateGsapMeta,
|
|
129
|
+
onDeleteGsapAnimation,
|
|
130
|
+
onAddGsapProperty,
|
|
131
|
+
onRemoveGsapProperty,
|
|
132
|
+
onUpdateGsapFromProperty,
|
|
133
|
+
onAddGsapFromProperty,
|
|
134
|
+
onRemoveGsapFromProperty,
|
|
135
|
+
onAddGsapAnimation,
|
|
136
|
+
onSetArcPath,
|
|
137
|
+
onUpdateArcSegment,
|
|
138
|
+
onUnroll,
|
|
139
|
+
onUpdateKeyframeEase,
|
|
140
|
+
onSetAllKeyframeEases,
|
|
141
|
+
}: Pick<
|
|
142
|
+
PropertyPanelProps,
|
|
143
|
+
| "projectId"
|
|
144
|
+
| "projectDir"
|
|
145
|
+
| "assets"
|
|
146
|
+
| "previewIframeRef"
|
|
147
|
+
| "onClearSelection"
|
|
148
|
+
| "onUngroup"
|
|
149
|
+
| "onSetStyle"
|
|
150
|
+
| "onSetAttribute"
|
|
151
|
+
| "onSetAttributes"
|
|
152
|
+
| "onSetAttributeLive"
|
|
153
|
+
| "onApplyColorGradingScope"
|
|
154
|
+
| "onSetHtmlAttribute"
|
|
155
|
+
| "onRemoveBackground"
|
|
156
|
+
| "onSetText"
|
|
157
|
+
| "onSetTextFieldStyle"
|
|
158
|
+
| "onAddTextField"
|
|
159
|
+
| "onRemoveTextField"
|
|
160
|
+
| "onAskAgent"
|
|
161
|
+
| "onToggleElementHidden"
|
|
162
|
+
| "onImportAssets"
|
|
163
|
+
| "onImportFonts"
|
|
164
|
+
| "fontAssets"
|
|
165
|
+
| "gsapAnimations"
|
|
166
|
+
| "gsapMultipleTimelines"
|
|
167
|
+
| "gsapUnsupportedTimelinePattern"
|
|
168
|
+
| "onUpdateGsapProperty"
|
|
169
|
+
| "onUpdateGsapMeta"
|
|
170
|
+
| "onDeleteGsapAnimation"
|
|
171
|
+
| "onAddGsapProperty"
|
|
172
|
+
| "onRemoveGsapProperty"
|
|
173
|
+
| "onUpdateGsapFromProperty"
|
|
174
|
+
| "onAddGsapFromProperty"
|
|
175
|
+
| "onRemoveGsapFromProperty"
|
|
176
|
+
| "onAddGsapAnimation"
|
|
177
|
+
| "onSetArcPath"
|
|
178
|
+
| "onUpdateArcSegment"
|
|
179
|
+
| "onUnroll"
|
|
180
|
+
| "onUpdateKeyframeEase"
|
|
181
|
+
| "onSetAllKeyframeEases"
|
|
182
|
+
| "recordingState"
|
|
183
|
+
| "recordingDuration"
|
|
184
|
+
| "onToggleRecording"
|
|
185
|
+
> &
|
|
186
|
+
// Layout-group values (Plan 3a Task 5). All are derived locals or handlers in
|
|
187
|
+
// PropertyPanel; compose their exact shapes from FlatLayoutSection's own props
|
|
188
|
+
// via Pick so a signature change there propagates here instead of drifting.
|
|
189
|
+
Pick<
|
|
190
|
+
Parameters<typeof FlatLayoutSection>[0],
|
|
191
|
+
| "displayX"
|
|
192
|
+
| "displayY"
|
|
193
|
+
| "displayW"
|
|
194
|
+
| "displayH"
|
|
195
|
+
| "displayR"
|
|
196
|
+
| "manualOffsetEditingDisabled"
|
|
197
|
+
| "manualSizeEditingDisabled"
|
|
198
|
+
| "manualRotationEditingDisabled"
|
|
199
|
+
| "commitManualOffset"
|
|
200
|
+
| "commitManualSize"
|
|
201
|
+
| "commitManualRotation"
|
|
202
|
+
| "gsapAnimId"
|
|
203
|
+
| "navKeyframes"
|
|
204
|
+
| "animIdForProp"
|
|
205
|
+
| "gsapRuntimeValues"
|
|
206
|
+
| "elStart"
|
|
207
|
+
| "elDuration"
|
|
208
|
+
| "onCommitAnimatedProperty"
|
|
209
|
+
| "onCommitAnimatedProperties"
|
|
210
|
+
| "onSeekToTime"
|
|
211
|
+
| "onRemoveKeyframe"
|
|
212
|
+
| "onConvertToKeyframes"
|
|
213
|
+
> & {
|
|
214
|
+
element: DomEditSelection;
|
|
215
|
+
styles: Record<string, string>;
|
|
216
|
+
sections: EditingSections;
|
|
217
|
+
sourceLabel: string;
|
|
218
|
+
gsapBorderRadius: { tl: number; tr: number; br: number; bl: number } | null;
|
|
219
|
+
showEditableSections: boolean;
|
|
220
|
+
selectedElementHidden: boolean;
|
|
221
|
+
selectedElementId: string | null;
|
|
222
|
+
clipboardCopied: boolean;
|
|
223
|
+
onCopyElementInfo: () => void;
|
|
224
|
+
currentTime: number;
|
|
225
|
+
}) {
|
|
226
|
+
// Lazy initializer: pick whichever group actually renders for this element
|
|
227
|
+
// (Text if text-editable, else Style if style-editable, else none open) so a
|
|
228
|
+
// style-only element doesn't start with everything collapsed. Only runs on
|
|
229
|
+
// mount — PropertyPanel.tsx keys <PropertyPanelFlat> by element identity so
|
|
230
|
+
// switching the selection re-mounts this component and re-derives the
|
|
231
|
+
// default instead of preserving stale state across unrelated elements.
|
|
232
|
+
const [openGroupId, setOpenGroupId] = useState<string>(() =>
|
|
233
|
+
isTextEditableSelection(element)
|
|
234
|
+
? "text"
|
|
235
|
+
: showEditableSections
|
|
236
|
+
? "style"
|
|
237
|
+
: sections.media
|
|
238
|
+
? "media"
|
|
239
|
+
: "layout",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Tracks which group(s) are actively transitioning this toggle cycle, so
|
|
243
|
+
// their header/body gets the fast entrance animation (hf-flat-group-enter)
|
|
244
|
+
// and no one else's does. Deliberately NOT derived from remounting alone:
|
|
245
|
+
// FlatGroupHeader instances are keyed by group id and React normally
|
|
246
|
+
// preserves them across re-renders, but toggling a non-adjacent group still
|
|
247
|
+
// shifts the untouched collapsed siblings between the before/after-open
|
|
248
|
+
// slices below, and Chromium restarts a CSS animation on that kind of
|
|
249
|
+
// position shift even though nothing about the sibling actually changed.
|
|
250
|
+
// Gating on these ids (cleared shortly after the 120ms CSS animation
|
|
251
|
+
// finishes) keeps the animation scoped to only the groups that actually
|
|
252
|
+
// just toggled. Two ids, not one: the clicked (newly-opening/closing) group
|
|
253
|
+
// AND whichever group was open immediately before the click and got
|
|
254
|
+
// implicitly closed by it — both freshly-mounted headers need to animate.
|
|
255
|
+
const [justToggledIds, setJustToggledIds] = useState<string[]>([]);
|
|
256
|
+
const justToggledTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
return () => {
|
|
259
|
+
if (justToggledTimeoutRef.current) clearTimeout(justToggledTimeoutRef.current);
|
|
260
|
+
};
|
|
261
|
+
}, []);
|
|
262
|
+
|
|
263
|
+
// Grade group state. Called unconditionally (React rules-of-hooks) even when
|
|
264
|
+
// sections.colorGrading is false — unlike the legacy ColorGradingSection,
|
|
265
|
+
// which is only mounted when the section is active, PropertyPanelFlat is not
|
|
266
|
+
// remounted per-section so the hook must run every render. Shares one state
|
|
267
|
+
// object between the group's header accessory (compare/status/reset) and its
|
|
268
|
+
// body (the FlatColorGradingSection controls).
|
|
269
|
+
const colorGradingController = useColorGradingController({
|
|
270
|
+
projectId,
|
|
271
|
+
element,
|
|
272
|
+
previewIframeRef,
|
|
273
|
+
onSetAttributeLive,
|
|
274
|
+
onApplyScope: onApplyColorGradingScope,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const isTextEditable = isTextEditableSelection(element);
|
|
278
|
+
const elementKind = sections.media ? "media" : element.textFields.length > 0 ? "text" : "other";
|
|
279
|
+
const toggleOpen = (groupId: string) => {
|
|
280
|
+
// Capture what was open BEFORE this click (this render's closure over
|
|
281
|
+
// openGroupId), so the group that's about to be implicitly closed can be
|
|
282
|
+
// tracked too — not just the one the user clicked.
|
|
283
|
+
const previousOpenGroupId = openGroupId;
|
|
284
|
+
setOpenGroupId((current) => (current === groupId ? "" : groupId));
|
|
285
|
+
const implicitlyClosedId =
|
|
286
|
+
previousOpenGroupId && previousOpenGroupId !== groupId ? previousOpenGroupId : null;
|
|
287
|
+
setJustToggledIds(implicitlyClosedId ? [groupId, implicitlyClosedId] : [groupId]);
|
|
288
|
+
if (justToggledTimeoutRef.current) clearTimeout(justToggledTimeoutRef.current);
|
|
289
|
+
justToggledTimeoutRef.current = setTimeout(() => setJustToggledIds([]), 200);
|
|
290
|
+
};
|
|
291
|
+
// Basis for the Layout keyframe gutter (X/Y/W/H/Angle + 3D Transform) —
|
|
292
|
+
// must agree with Motion's Timing row (FlatTimingRow), which infers the
|
|
293
|
+
// range from animations when there's no explicit data-duration. Computed
|
|
294
|
+
// here (not threaded from PropertyPanel) both to keep that file under its
|
|
295
|
+
// 600-LOC gate and because element/gsapAnimations are already in scope.
|
|
296
|
+
const { start: elStart, duration: elDuration } = deriveElementTiming(element, gsapAnimations);
|
|
297
|
+
// Trivial percentage→time seek, derived here rather than threaded from
|
|
298
|
+
// PropertyPanel (keeps that file under its 600-LOC gate).
|
|
299
|
+
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
|
300
|
+
// Playhead position within the SAME corrected elStart/elDuration basis as
|
|
301
|
+
// seekFromKfPct above — recomputed here (not threaded as `currentPct` from
|
|
302
|
+
// PropertyPanel, which still derives it against its own naive basis for the
|
|
303
|
+
// legacy panel) so KeyframeNavigation's diamond active-state and prev/next
|
|
304
|
+
// arrow targeting agree with where a keyframe click actually seeks to
|
|
305
|
+
// (follow-up fix to 684ec4e87, which corrected the seek basis but left this
|
|
306
|
+
// one still naive).
|
|
307
|
+
const currentPct = elDuration > 0 ? ((currentTime - elStart) / elDuration) * 100 : 0;
|
|
308
|
+
|
|
309
|
+
// Motion group double-gate — reproduces the legacy PropertyPanel gate exactly:
|
|
310
|
+
// • Timing (sections.timing) shows via resolveEditingSections, same as today.
|
|
311
|
+
// • The effect-card list shows only when STUDIO_GSAP_PANEL_ENABLED is on AND
|
|
312
|
+
// all five edit handlers are present (identical to PropertyPanel's legacy
|
|
313
|
+
// `<GsapAnimationSection>` guard).
|
|
314
|
+
// Computing the narrowed handler bundle inside the `&&`-guarded ternary lets
|
|
315
|
+
// TypeScript prove each handler non-undefined without a `!` assertion; the
|
|
316
|
+
// noop bundle only fills the type when the gate is off (never invoked, since
|
|
317
|
+
// FlatMotionSection guards every call behind showEffects).
|
|
318
|
+
const showMotionTiming = Boolean(sections.timing);
|
|
319
|
+
const gsapEffectHandlers =
|
|
320
|
+
STUDIO_GSAP_PANEL_ENABLED &&
|
|
321
|
+
onUpdateGsapProperty &&
|
|
322
|
+
onUpdateGsapMeta &&
|
|
323
|
+
onDeleteGsapAnimation &&
|
|
324
|
+
onAddGsapProperty &&
|
|
325
|
+
onAddGsapAnimation
|
|
326
|
+
? {
|
|
327
|
+
onAddAnimation: onAddGsapAnimation,
|
|
328
|
+
onUpdateProperty: onUpdateGsapProperty,
|
|
329
|
+
onUpdateMeta: onUpdateGsapMeta,
|
|
330
|
+
onDeleteAnimation: onDeleteGsapAnimation,
|
|
331
|
+
onAddProperty: onAddGsapProperty,
|
|
332
|
+
onRemoveProperty: onRemoveGsapProperty ?? (() => {}),
|
|
333
|
+
onUpdateFromProperty: onUpdateGsapFromProperty,
|
|
334
|
+
onAddFromProperty: onAddGsapFromProperty,
|
|
335
|
+
onRemoveFromProperty: onRemoveGsapFromProperty,
|
|
336
|
+
onSetArcPath,
|
|
337
|
+
onUpdateArcSegment,
|
|
338
|
+
onUnroll,
|
|
339
|
+
onUpdateKeyframeEase,
|
|
340
|
+
onSetAllKeyframeEases,
|
|
341
|
+
}
|
|
342
|
+
: null;
|
|
343
|
+
const showMotionEffects = gsapEffectHandlers !== null;
|
|
344
|
+
const showMotionGroup = showMotionTiming || showMotionEffects;
|
|
345
|
+
|
|
346
|
+
// Ordered group descriptors — one per FlatGroup this panel renders, gated by
|
|
347
|
+
// the same conditions the inline JSX used. Split below into before-open/
|
|
348
|
+
// open/after-open regions for the one-open accordion.
|
|
349
|
+
const groups: FlatGroupDescriptor[] = [];
|
|
350
|
+
if (isTextEditable) {
|
|
351
|
+
groups.push({
|
|
352
|
+
id: "text",
|
|
353
|
+
title: "Text",
|
|
354
|
+
summary: formatTextFieldPreview(element.textFields[0]?.value ?? ""),
|
|
355
|
+
content: (
|
|
356
|
+
<FlatTextSection
|
|
357
|
+
element={element}
|
|
358
|
+
styles={styles}
|
|
359
|
+
fontAssets={fontAssets}
|
|
360
|
+
onImportFonts={onImportFonts}
|
|
361
|
+
onSetText={onSetText}
|
|
362
|
+
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
363
|
+
onAddTextField={onAddTextField}
|
|
364
|
+
onRemoveTextField={onRemoveTextField}
|
|
365
|
+
/>
|
|
366
|
+
),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (showEditableSections) {
|
|
370
|
+
// Number.isFinite guard (not `|| 1`): opacity 0 is a real value — an
|
|
371
|
+
// invisible element must summarize as 0%, not 100%.
|
|
372
|
+
const opacityValue = parseFloat(styles.opacity ?? "1");
|
|
373
|
+
const opacityPct = Math.round((Number.isFinite(opacityValue) ? opacityValue : 1) * 100);
|
|
374
|
+
groups.push({
|
|
375
|
+
id: "style",
|
|
376
|
+
title: "Style",
|
|
377
|
+
summary: `fill ${styles["background-image"] && styles["background-image"] !== "none" ? "image/gradient" : styles["background-color"] ? "set" : "none"} · ${opacityPct}%`,
|
|
378
|
+
content: (
|
|
379
|
+
<FlatStyleSection
|
|
380
|
+
projectId={projectId}
|
|
381
|
+
element={element}
|
|
382
|
+
styles={styles}
|
|
383
|
+
assets={assets}
|
|
384
|
+
onSetStyle={onSetStyle}
|
|
385
|
+
onImportAssets={onImportAssets}
|
|
386
|
+
gsapBorderRadius={gsapBorderRadius}
|
|
387
|
+
/>
|
|
388
|
+
),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
groups.push({
|
|
392
|
+
id: "layout",
|
|
393
|
+
title: "Layout",
|
|
394
|
+
// No scrub accessory: FlatRow/CommitField has no pointer-drag scrubbing
|
|
395
|
+
// (wheel/arrow keys only) — advertising "drag values to scrub" here lies.
|
|
396
|
+
summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} · ${Math.round(displayW)}×${Math.round(displayH)}`,
|
|
397
|
+
content: (
|
|
398
|
+
<FlatLayoutSection
|
|
399
|
+
element={element}
|
|
400
|
+
styles={styles}
|
|
401
|
+
onSetStyle={onSetStyle}
|
|
402
|
+
disabled={!element.capabilities.canEditStyles}
|
|
403
|
+
displayX={displayX}
|
|
404
|
+
displayY={displayY}
|
|
405
|
+
displayW={displayW}
|
|
406
|
+
displayH={displayH}
|
|
407
|
+
displayR={displayR}
|
|
408
|
+
manualOffsetEditingDisabled={manualOffsetEditingDisabled}
|
|
409
|
+
manualSizeEditingDisabled={manualSizeEditingDisabled}
|
|
410
|
+
manualRotationEditingDisabled={manualRotationEditingDisabled}
|
|
411
|
+
commitManualOffset={commitManualOffset}
|
|
412
|
+
commitManualSize={commitManualSize}
|
|
413
|
+
commitManualRotation={commitManualRotation}
|
|
414
|
+
gsapAnimId={gsapAnimId}
|
|
415
|
+
navKeyframes={navKeyframes}
|
|
416
|
+
currentPct={currentPct}
|
|
417
|
+
seekFromKfPct={seekFromKfPct}
|
|
418
|
+
animIdForProp={animIdForProp}
|
|
419
|
+
resolveAnimIdForProp={animIdForProp}
|
|
420
|
+
gsapRuntimeValues={gsapRuntimeValues}
|
|
421
|
+
gsapKeyframes={navKeyframes}
|
|
422
|
+
elStart={elStart}
|
|
423
|
+
elDuration={elDuration}
|
|
424
|
+
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
425
|
+
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
426
|
+
onSeekToTime={onSeekToTime}
|
|
427
|
+
onRemoveKeyframe={onRemoveKeyframe}
|
|
428
|
+
onConvertToKeyframes={onConvertToKeyframes}
|
|
429
|
+
onLivePreviewProps={createGsapLivePreview(previewIframeRef ?? { current: null })}
|
|
430
|
+
/>
|
|
431
|
+
),
|
|
432
|
+
});
|
|
433
|
+
if (showMotionGroup) {
|
|
434
|
+
groups.push({
|
|
435
|
+
id: "motion",
|
|
436
|
+
title: "Motion",
|
|
437
|
+
summary: `${gsapAnimations.length} effect${gsapAnimations.length === 1 ? "" : "s"}`,
|
|
438
|
+
content: (
|
|
439
|
+
<FlatMotionSection
|
|
440
|
+
element={element}
|
|
441
|
+
animations={gsapAnimations}
|
|
442
|
+
showTiming={showMotionTiming}
|
|
443
|
+
showEffects={showMotionEffects}
|
|
444
|
+
multipleTimelines={gsapMultipleTimelines}
|
|
445
|
+
unsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
|
|
446
|
+
onSetAttribute={onSetAttribute}
|
|
447
|
+
onSetAttributes={onSetAttributes}
|
|
448
|
+
{...(gsapEffectHandlers ?? EMPTY_GSAP_EFFECT_HANDLERS)}
|
|
449
|
+
/>
|
|
450
|
+
),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (sections.colorGrading) {
|
|
454
|
+
groups.push({
|
|
455
|
+
id: "grade",
|
|
456
|
+
title: "Grade",
|
|
457
|
+
accessory: <FlatColorGradingAccessory state={colorGradingController} />,
|
|
458
|
+
summary: `${colorGradingController.grading.preset ?? "neutral"} · ${Math.round(colorGradingController.grading.intensity * 100)}%`,
|
|
459
|
+
content: (
|
|
460
|
+
<FlatColorGradingSection
|
|
461
|
+
grading={colorGradingController.grading}
|
|
462
|
+
assets={assets}
|
|
463
|
+
onImportAssets={onImportAssets}
|
|
464
|
+
onCommitColorGrading={colorGradingController.commitColorGrading}
|
|
465
|
+
applyScope={colorGradingController.applyScope}
|
|
466
|
+
applyBusy={colorGradingController.applyBusy}
|
|
467
|
+
onSetApplyScope={colorGradingController.setApplyScope}
|
|
468
|
+
onApplyToScope={() => void colorGradingController.applyToScope()}
|
|
469
|
+
onApplyScopeAvailable={Boolean(onApplyColorGradingScope)}
|
|
470
|
+
mediaMetadata={colorGradingController.mediaMetadata}
|
|
471
|
+
/>
|
|
472
|
+
),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (sections.media) {
|
|
476
|
+
groups.push({
|
|
477
|
+
id: "media",
|
|
478
|
+
title: "Media",
|
|
479
|
+
summary: element.tagName,
|
|
480
|
+
content: (
|
|
481
|
+
<FlatMediaSection
|
|
482
|
+
projectDir={projectDir}
|
|
483
|
+
element={element}
|
|
484
|
+
styles={styles}
|
|
485
|
+
onSetStyle={onSetStyle}
|
|
486
|
+
onSetAttribute={onSetAttribute}
|
|
487
|
+
onSetHtmlAttribute={onSetHtmlAttribute}
|
|
488
|
+
onRemoveBackground={onRemoveBackground}
|
|
489
|
+
/>
|
|
490
|
+
),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Fixed-headers + scrollable-open-section layout (design_handoff
|
|
495
|
+
// scrollable-open-section, replaces the prior sticky-stacking mechanism):
|
|
496
|
+
// collapsed headers before/after the open group render in normal document
|
|
497
|
+
// flow and never move. Only the open group's own body content scrolls, in
|
|
498
|
+
// a dedicated region between the two fixed header stacks. When no group is
|
|
499
|
+
// open, every group is just a collapsed header — there's no scrollable
|
|
500
|
+
// middle region at all, since nothing is expanded.
|
|
501
|
+
const openIndex = groups.findIndex((g) => g.id === openGroupId);
|
|
502
|
+
const beforeOpen = openIndex === -1 ? groups : groups.slice(0, openIndex);
|
|
503
|
+
const openGroup = openIndex === -1 ? null : groups[openIndex];
|
|
504
|
+
const afterOpen = openIndex === -1 ? [] : groups.slice(openIndex + 1);
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
<DesignPanelInputProvider ui="flat">
|
|
508
|
+
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
|
|
509
|
+
<DesignPanelInputProvider section="header">
|
|
510
|
+
<PropertyPanelFlatHeader
|
|
511
|
+
name={element.label}
|
|
512
|
+
meta={`${sourceLabel} · ${element.tagName}`}
|
|
513
|
+
elementKind={elementKind}
|
|
514
|
+
hidden={selectedElementHidden}
|
|
515
|
+
onToggleHidden={
|
|
516
|
+
selectedElementId && onToggleElementHidden
|
|
517
|
+
? () => void onToggleElementHidden(selectedElementId, !selectedElementHidden)
|
|
518
|
+
: undefined
|
|
519
|
+
}
|
|
520
|
+
copied={clipboardCopied}
|
|
521
|
+
onCopy={onCopyElementInfo}
|
|
522
|
+
onClear={onClearSelection}
|
|
523
|
+
onUngroup={onUngroup}
|
|
524
|
+
showUngroup={Boolean(onUngroup && element.dataAttributes["hf-group"] != null)}
|
|
525
|
+
/>
|
|
526
|
+
</DesignPanelInputProvider>
|
|
527
|
+
<div data-flat-panel-body="true" className="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
|
528
|
+
{beforeOpen.map((g) => (
|
|
529
|
+
<DesignPanelInputProvider key={g.id} section={slugifyDesignInput(g.title)}>
|
|
530
|
+
<FlatGroupHeader
|
|
531
|
+
title={g.title}
|
|
532
|
+
isOpen={false}
|
|
533
|
+
onToggleOpen={() => toggleOpen(g.id)}
|
|
534
|
+
summary={g.summary}
|
|
535
|
+
animateEntrance={justToggledIds.includes(g.id)}
|
|
536
|
+
/>
|
|
537
|
+
</DesignPanelInputProvider>
|
|
538
|
+
))}
|
|
539
|
+
{openGroup && (
|
|
540
|
+
<DesignPanelInputProvider section={slugifyDesignInput(openGroup.title)}>
|
|
541
|
+
<div data-flat-group-open="true" className="flex min-h-0 flex-1 flex-col">
|
|
542
|
+
<FlatGroupHeader
|
|
543
|
+
title={openGroup.title}
|
|
544
|
+
isOpen
|
|
545
|
+
onToggleOpen={() => toggleOpen(openGroup.id)}
|
|
546
|
+
accessory={openGroup.accessory}
|
|
547
|
+
animateEntrance={justToggledIds.includes(openGroup.id)}
|
|
548
|
+
/>
|
|
549
|
+
<div
|
|
550
|
+
className={`${justToggledIds.includes(openGroup.id) ? "hf-flat-group-enter " : ""}min-h-0 flex-1 overflow-y-auto border-b border-panel-hairline bg-panel-bg-inset px-4 py-3 shadow-[inset_0_2px_4px_-1px_rgba(0,0,0,0.5)]`}
|
|
551
|
+
>
|
|
552
|
+
{openGroup.content}
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</DesignPanelInputProvider>
|
|
556
|
+
)}
|
|
557
|
+
{afterOpen.map((g) => (
|
|
558
|
+
<DesignPanelInputProvider key={g.id} section={slugifyDesignInput(g.title)}>
|
|
559
|
+
<FlatGroupHeader
|
|
560
|
+
title={g.title}
|
|
561
|
+
isOpen={false}
|
|
562
|
+
onToggleOpen={() => toggleOpen(g.id)}
|
|
563
|
+
summary={g.summary}
|
|
564
|
+
animateEntrance={justToggledIds.includes(g.id)}
|
|
565
|
+
/>
|
|
566
|
+
</DesignPanelInputProvider>
|
|
567
|
+
))}
|
|
568
|
+
</div>
|
|
569
|
+
<DesignPanelInputProvider section="footer">
|
|
570
|
+
<PropertyPanelFlatFooter
|
|
571
|
+
onAskAgent={onAskAgent}
|
|
572
|
+
recordingState={recordingState}
|
|
573
|
+
recordingDuration={recordingDuration}
|
|
574
|
+
onToggleRecording={onToggleRecording}
|
|
575
|
+
/>
|
|
576
|
+
</DesignPanelInputProvider>
|
|
577
|
+
</div>
|
|
578
|
+
</DesignPanelInputProvider>
|
|
579
|
+
);
|
|
580
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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 { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter";
|
|
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 renderFooter(overrides: Partial<Parameters<typeof PropertyPanelFlatFooter>[0]> = {}) {
|
|
15
|
+
const host = document.createElement("div");
|
|
16
|
+
document.body.append(host);
|
|
17
|
+
const root = createRoot(host);
|
|
18
|
+
act(() => {
|
|
19
|
+
root.render(<PropertyPanelFlatFooter {...overrides} />);
|
|
20
|
+
});
|
|
21
|
+
return { host, root };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("PropertyPanelFlatFooter", () => {
|
|
25
|
+
it("renders the ask-agent affordance and fires onAskAgent on click", () => {
|
|
26
|
+
const onAskAgent = vi.fn();
|
|
27
|
+
const { host, root } = renderFooter({ onAskAgent });
|
|
28
|
+
expect(host.textContent).toContain("Ask agent about this element");
|
|
29
|
+
const askButton = host.querySelector<HTMLButtonElement>('[data-flat-footer-ask="true"]');
|
|
30
|
+
act(() => askButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
31
|
+
expect(onAskAgent).toHaveBeenCalledTimes(1);
|
|
32
|
+
act(() => root.unmount());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("shows the idle record affordance and toggles recording on click", () => {
|
|
36
|
+
const onToggleRecording = vi.fn();
|
|
37
|
+
const { host, root } = renderFooter({ recordingState: "idle", onToggleRecording });
|
|
38
|
+
const recordButton = host.querySelector<HTMLButtonElement>('[data-flat-footer-record="true"]');
|
|
39
|
+
expect(recordButton?.title).toBe("Record gesture (R)");
|
|
40
|
+
act(() => recordButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
41
|
+
expect(onToggleRecording).toHaveBeenCalledTimes(1);
|
|
42
|
+
act(() => root.unmount());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("shows the recording duration while recording", () => {
|
|
46
|
+
const { host, root } = renderFooter({
|
|
47
|
+
recordingState: "recording",
|
|
48
|
+
recordingDuration: 2.4,
|
|
49
|
+
onToggleRecording: vi.fn(),
|
|
50
|
+
});
|
|
51
|
+
const recordButton = host.querySelector<HTMLButtonElement>('[data-flat-footer-record="true"]');
|
|
52
|
+
expect(recordButton?.title).toBe("Stop recording 2.4s");
|
|
53
|
+
act(() => root.unmount());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Plan 10 (sticky-footer-gap): the root must carry an opaque background —
|
|
57
|
+
// it previously had none at all, letting scrolled panel content show
|
|
58
|
+
// through. Regression coverage for the definite fix from the brief.
|
|
59
|
+
it("has an opaque bg-panel-bg background on its root element", () => {
|
|
60
|
+
const { host, root } = renderFooter({ onAskAgent: vi.fn() });
|
|
61
|
+
const footerRoot = host.firstElementChild as HTMLElement;
|
|
62
|
+
expect(footerRoot.className).toContain("bg-panel-bg");
|
|
63
|
+
act(() => root.unmount());
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Plan 11 (scrollable-open-section): the prior sticky-stacking mechanism —
|
|
67
|
+
// and the Plan 10 hairline-sealing hack it required at this exact boundary
|
|
68
|
+
// (an absolutely-positioned overlay patching a Chromium sticky-offset
|
|
69
|
+
// rounding gap) — is gone now that nothing above the footer is
|
|
70
|
+
// `position: sticky`. Live browser verification (p11 report) confirmed the
|
|
71
|
+
// boundary renders as a single clean hairline without it: whatever
|
|
72
|
+
// immediately precedes the footer (a collapsed FlatGroupHeader, or the open
|
|
73
|
+
// group's scrollable body wrapper) already draws its own border-b in normal
|
|
74
|
+
// document flow, so the footer needs no border or seal of its own.
|
|
75
|
+
it("renders no seal overlay and no border of its own — the boundary line comes from whatever precedes it", () => {
|
|
76
|
+
const { host, root } = renderFooter({ onAskAgent: vi.fn() });
|
|
77
|
+
const footerRoot = host.firstElementChild as HTMLElement;
|
|
78
|
+
expect(footerRoot.className).not.toContain("border-t");
|
|
79
|
+
expect(footerRoot.className).not.toContain("border-b");
|
|
80
|
+
expect(host.querySelector('[data-flat-footer-seal="true"]')).toBeNull();
|
|
81
|
+
act(() => root.unmount());
|
|
82
|
+
});
|
|
83
|
+
});
|