@@ -15,11 +15,13 @@ import { keywordValues } from "@webstudio-is/css-data";
15
15
import { useIds } from "~/shared/form-utils" ;
16
16
17
17
import type {
18
+ DurationUnitValue ,
18
19
RangeUnitValue ,
19
20
ScrollAnimation ,
20
21
ViewAnimation ,
21
22
} from "@webstudio-is/sdk" ;
22
23
import {
24
+ durationUnitValueSchema ,
23
25
RANGE_UNITS ,
24
26
rangeUnitValueSchema ,
25
27
scrollAnimationSchema ,
@@ -37,6 +39,7 @@ import {
37
39
import { Keyframes } from "./animation-keyframes" ;
38
40
import { humanizeString } from "~/shared/string-utils" ;
39
41
import { Link2Icon , Link2UnlinkedIcon } from "@webstudio-is/icons" ;
42
+ import { $availableUnitVariables } from "~/builder/features/style-panel/shared/model" ;
40
43
41
44
const fillModeDescriptions : Record <
42
45
NonNullable < ViewAnimation [ "timing" ] [ "fill" ] > ,
@@ -95,9 +98,11 @@ const RangeValueInput = ({
95
98
id,
96
99
value,
97
100
onChange,
101
+ disabled,
98
102
} : {
99
103
id : string ;
100
104
value : RangeUnitValue ;
105
+ disabled ?: boolean ;
101
106
onChange : ( ( value : undefined , isEphemeral : true ) => void ) &
102
107
( ( value : RangeUnitValue , isEphemeral : boolean ) => void ) ;
103
108
} ) => {
@@ -108,6 +113,7 @@ const RangeValueInput = ({
108
113
return (
109
114
< CssValueInput
110
115
id = { id }
116
+ disabled = { disabled }
111
117
styleSource = "default"
112
118
value = { value }
113
119
/* marginLeft to allow negative values */
@@ -127,12 +133,13 @@ const RangeValueInput = ({
127
133
128
134
onChange ( undefined , true ) ;
129
135
} }
130
- getOptions = { ( ) => [ ] }
136
+ getOptions = { ( ) => $availableUnitVariables . get ( ) }
131
137
onHighlight = { ( ) => {
132
138
/* Nothing to Highlight */
133
139
} }
134
140
onChangeComplete = { ( event ) => {
135
141
const parsedValue = rangeUnitValueSchema . safeParse ( event . value ) ;
142
+
136
143
if ( parsedValue . success ) {
137
144
onChange ( parsedValue . data , false ) ;
138
145
setIntermediateValue ( undefined ) ;
@@ -184,6 +191,7 @@ const EasingInput = ({
184
191
type : "keyword" as const ,
185
192
value,
186
193
} ) ) ,
194
+ ...$availableUnitVariables . get ( ) ,
187
195
] }
188
196
property = "animation-timing-function"
189
197
intermediateValue = { intermediateValue }
@@ -209,6 +217,61 @@ const EasingInput = ({
209
217
) ;
210
218
} ;
211
219
220
+ const DurationInput = ( {
221
+ id,
222
+ value,
223
+ onChange,
224
+ } : {
225
+ id : string ;
226
+ value : DurationUnitValue | undefined ;
227
+ onChange : (
228
+ value : DurationUnitValue | undefined ,
229
+ isEphemeral : boolean
230
+ ) => void ;
231
+ } ) => {
232
+ const [ intermediateValue , setIntermediateValue ] = useState <
233
+ StyleValue | IntermediateStyleValue
234
+ > ( ) ;
235
+
236
+ return (
237
+ < CssValueInput
238
+ id = { id }
239
+ styleSource = "default"
240
+ value = { value }
241
+ placeholder = "auto"
242
+ property = "animation-duration"
243
+ intermediateValue = { intermediateValue }
244
+ onChange = { ( styleValue ) => {
245
+ setIntermediateValue ( styleValue ) ;
246
+ } }
247
+ getOptions = { ( ) => $availableUnitVariables . get ( ) }
248
+ onHighlight = { ( ) => { } }
249
+ onChangeComplete = { ( event ) => {
250
+ const value = durationUnitValueSchema . safeParse ( event . value ) ;
251
+ onChange ( undefined , true ) ;
252
+ if ( value . success ) {
253
+ onChange ( value . data , false ) ;
254
+ setIntermediateValue ( undefined ) ;
255
+ return ;
256
+ }
257
+
258
+ setIntermediateValue ( {
259
+ type : "invalid" ,
260
+ value : toValue ( event . value ) ,
261
+ } ) ;
262
+ } }
263
+ onAbort = { ( ) => {
264
+ onChange ( undefined , true ) ;
265
+ } }
266
+ onReset = { ( ) => {
267
+ setIntermediateValue ( undefined ) ;
268
+ onChange ( undefined , false ) ;
269
+ onChange ( undefined , true ) ;
270
+ } }
271
+ />
272
+ ) ;
273
+ } ;
274
+
212
275
type AnimationPanelContentProps = {
213
276
type : "scroll" | "view" ;
214
277
value : ScrollAnimation | ViewAnimation ;
@@ -249,6 +312,7 @@ export const AnimationPanelContent = ({
249
312
"fill" ,
250
313
"easing" ,
251
314
"name" ,
315
+ "duration" ,
252
316
] as const ) ;
253
317
254
318
const timelineRangeDescriptions =
@@ -259,6 +323,8 @@ export const AnimationPanelContent = ({
259
323
const animationSchema =
260
324
type === "scroll" ? scrollAnimationSchema : viewAnimationSchema ;
261
325
326
+ const isRangeEndEnabled = value . timing . duration === undefined ;
327
+
262
328
const handleChange = ( rawValue : unknown , isEphemeral : boolean ) => {
263
329
if ( rawValue === undefined ) {
264
330
onChange ( undefined , true ) ;
@@ -414,7 +480,9 @@ export const AnimationPanelContent = ({
414
480
>
415
481
< Label htmlFor = { fieldIds . rangeStartName } > Range Start</ Label >
416
482
< div />
417
- < Label htmlFor = { fieldIds . rangeEndName } > Range End</ Label >
483
+ < Label disabled = { ! isRangeEndEnabled } htmlFor = { fieldIds . rangeEndName } >
484
+ Range End
485
+ </ Label >
418
486
419
487
< Select
420
488
id = { fieldIds . rangeStartName }
@@ -515,6 +583,7 @@ export const AnimationPanelContent = ({
515
583
</ Grid >
516
584
< Select
517
585
id = { fieldIds . rangeEndName }
586
+ disabled = { ! isRangeEndEnabled }
518
587
options = { timelineRangeNames }
519
588
getLabel = { humanizeString }
520
589
value = { value . timing . rangeEnd ?. [ 0 ] ?? timelineRangeNames [ 0 ] ! }
@@ -614,6 +683,7 @@ export const AnimationPanelContent = ({
614
683
< div />
615
684
< RangeValueInput
616
685
id = { fieldIds . rangeEndValue }
686
+ disabled = { ! isRangeEndEnabled }
617
687
value = {
618
688
value . timing . rangeEnd ?. [ 1 ] ?? {
619
689
type : "unit" ,
@@ -646,6 +716,39 @@ export const AnimationPanelContent = ({
646
716
/>
647
717
</ Grid >
648
718
719
+ < Grid
720
+ gap = { 1 }
721
+ align = { "center" }
722
+ css = { {
723
+ gridTemplateColumns : "1fr 16px 1fr" ,
724
+ paddingInline : theme . panel . paddingInline ,
725
+ } }
726
+ >
727
+ < Label htmlFor = { fieldIds . duration } > Duration</ Label >
728
+ < div />
729
+ < DurationInput
730
+ id = { fieldIds . duration }
731
+ value = { value . timing . duration }
732
+ onChange = { ( duration , isEphemeral ) => {
733
+ if ( duration === undefined && isEphemeral ) {
734
+ handleChange ( undefined , true ) ;
735
+ return ;
736
+ }
737
+
738
+ handleChange (
739
+ {
740
+ ...value ,
741
+ timing : {
742
+ ...value . timing ,
743
+ duration,
744
+ } ,
745
+ } ,
746
+ isEphemeral
747
+ ) ;
748
+ } }
749
+ />
750
+ </ Grid >
751
+
649
752
< Keyframes
650
753
value = { value . keyframes }
651
754
onChange = { ( keyframes , isEphemeral ) => {
0 commit comments