@@ -5,24 +5,24 @@ import {
55} from "@webstudio-is/css-engine" ;
66import {
77 Grid ,
8- IconButton ,
98 Label ,
10- Separator ,
119 Tooltip ,
12- ScrollArea ,
1310 theme ,
1411 SectionTitle ,
1512 SectionTitleButton ,
1613 SectionTitleLabel ,
14+ FloatingPanel ,
15+ CssValueListItem ,
16+ SmallIconButton ,
17+ CssValueListArrowFocus ,
1718} from "@webstudio-is/design-system" ;
1819import { MinusIcon , PlusIcon } from "@webstudio-is/icons" ;
1920import type { AnimationKeyframe } from "@webstudio-is/sdk" ;
20- import { Fragment , useMemo , useRef , useState } from "react" ;
21+ import { Fragment , useId , useMemo , useRef , useState } from "react" ;
2122import {
2223 CssValueInput ,
2324 type IntermediateStyleValue ,
2425} from "~/builder/features/style-panel/shared/css-value-input" ;
25- import { useIds } from "~/shared/form-utils" ;
2626import { calcOffsets , findInsertionIndex , moveItem } from "./keyframe-helpers" ;
2727import { CssEditor } from "~/builder/shared/css-editor" ;
2828import type { ComputedStyleDecl } from "~/shared/style-object-model" ;
@@ -35,6 +35,8 @@ const unitOptions = [
3535 } ,
3636] ;
3737
38+ const roundOffset = ( value : number ) => Math . round ( value * 1000 ) / 10 ;
39+
3840const OffsetInput = ( {
3941 id,
4042 value,
@@ -54,9 +56,7 @@ const OffsetInput = ({
5456 < CssValueInput
5557 id = { id }
5658 placeholder = {
57- value === undefined
58- ? `auto (${ Math . round ( placeholder * 1000 ) / 10 } %)`
59- : "auto"
59+ value === undefined ? `auto (${ roundOffset ( placeholder ) } %)` : "auto"
6060 }
6161 getOptions = { ( ) => [ ] }
6262 unitOptions = { unitOptions }
@@ -66,11 +66,7 @@ const OffsetInput = ({
6666 property = { "font-stretch" }
6767 value = {
6868 value !== undefined
69- ? {
70- type : "unit" ,
71- value : Math . round ( value * 1000 ) / 10 ,
72- unit : "%" ,
73- }
69+ ? { type : "unit" , value : roundOffset ( value ) , unit : "%" }
7470 : undefined
7571 }
7672 onChange = { ( styleValue ) => {
@@ -121,91 +117,113 @@ const OffsetInput = ({
121117
122118const Keyframe = ( {
123119 value,
120+ index,
124121 offsetPlaceholder,
125122 onChange,
123+ onDelete,
126124} : {
127125 value : AnimationKeyframe ;
126+ index : number ;
128127 offsetPlaceholder : number ;
129- onChange : (
130- value : AnimationKeyframe | undefined ,
131- isEphemeral : boolean
132- ) => void ;
128+ onChange : ( value : AnimationKeyframe , isEphemeral : boolean ) => void ;
129+ onDelete : ( ) => void ;
133130} ) => {
134- const ids = useIds ( [ "offset" ] ) ;
135- const declarations : Array < ComputedStyleDecl > = useMemo (
131+ const offsetId = useId ( ) ;
132+ const declarations = useMemo (
136133 ( ) =>
137- ( Object . keys ( value . styles ) as Array < CssProperty > ) . map ( ( property ) => {
134+ ( Object . keys ( value . styles ) as CssProperty [ ] ) . map ( ( property ) => {
138135 const styleValue = value . styles [ property ] ;
139136 return {
140137 property,
141138 source : { name : "local" } ,
142139 cascadedValue : styleValue ,
143140 computedValue : styleValue ,
144141 usedValue : styleValue ,
145- } ;
142+ } satisfies ComputedStyleDecl ;
146143 } ) ,
147144 [ value . styles ]
148145 ) ;
149146
150147 return (
151- < >
152- < Grid
153- gap = { 1 }
154- align = { "center" }
155- css = { {
156- gridTemplateColumns : "1fr 1fr auto" ,
157- paddingInline : theme . panel . paddingInline ,
158- } }
159- >
160- < Label htmlFor = { ids . offset } > Offset</ Label >
161- < OffsetInput
162- id = { ids . offset }
163- value = { value . offset }
164- placeholder = { offsetPlaceholder }
165- onChange = { ( offset ) => {
166- onChange ( { ...value , offset } , false ) ;
167- } }
168- />
169- < Tooltip content = "Remove keyframe" >
170- < IconButton onClick = { ( ) => onChange ( undefined , false ) } >
171- < MinusIcon />
172- </ IconButton >
173- </ Tooltip >
174- </ Grid >
175- < Grid >
176- < CssEditor
177- showSearch = { false }
178- showAddStyleInput
179- propertiesPosition = "top"
180- virtualize = { false }
181- declarations = { declarations }
182- onAddDeclarations = { ( addedStyleMap ) => {
183- const styles = { ...value . styles } ;
184- for ( const [ property , value ] of addedStyleMap ) {
185- styles [ property ] = value ;
186- }
187- onChange ( { ...value , styles } , false ) ;
188- } }
189- onDeleteProperty = { ( property , options = { } ) => {
190- if ( options . isEphemeral === true ) {
191- return ;
192- }
193- const styles = { ...value . styles } ;
194- delete styles [ property ] ;
195- onChange ( { ...value , styles } , false ) ;
196- } }
197- onSetProperty = { ( property ) => {
198- return ( newValue , options ) => {
199- const styles = { ...value . styles , [ property ] : newValue } ;
200- onChange ( { ...value , styles } , options ?. isEphemeral ?? false ) ;
201- } ;
202- } }
203- onDeleteAllDeclarations = { ( ) => {
204- onChange ( { ...value , styles : { } } , false ) ;
205- } }
206- />
207- </ Grid >
208- </ >
148+ < FloatingPanel
149+ title = "Keyframe"
150+ content = {
151+ < Grid css = { { paddingBlock : theme . panel . paddingBlock } } >
152+ < Grid
153+ gap = { 1 }
154+ align = "center"
155+ css = { {
156+ gridTemplateColumns : `1fr ${ theme . spacing [ 22 ] } ` ,
157+ paddingInline : theme . panel . paddingInline ,
158+ } }
159+ >
160+ < Label htmlFor = { offsetId } > Offset</ Label >
161+ < OffsetInput
162+ id = { offsetId }
163+ value = { value . offset }
164+ placeholder = { offsetPlaceholder }
165+ onChange = { ( offset ) => {
166+ onChange ( { ...value , offset } , false ) ;
167+ } }
168+ />
169+ </ Grid >
170+
171+ < CssEditor
172+ showSearch = { false }
173+ showAddStyleInput
174+ propertiesPosition = "top"
175+ virtualize = { false }
176+ declarations = { declarations }
177+ onAddDeclarations = { ( addedStyleMap ) => {
178+ const styles = { ...value . styles } ;
179+ for ( const [ property , value ] of addedStyleMap ) {
180+ styles [ property ] = value ;
181+ }
182+ onChange ( { ...value , styles } , false ) ;
183+ } }
184+ onDeleteProperty = { ( property , options = { } ) => {
185+ if ( options . isEphemeral === true ) {
186+ return ;
187+ }
188+ const styles = { ...value . styles } ;
189+ delete styles [ property ] ;
190+ onChange ( { ...value , styles } , false ) ;
191+ } }
192+ onSetProperty = { ( property ) => {
193+ return ( newValue , options ) => {
194+ const styles = { ...value . styles , [ property ] : newValue } ;
195+ onChange ( { ...value , styles } , options ?. isEphemeral ?? false ) ;
196+ } ;
197+ } }
198+ onDeleteAllDeclarations = { ( ) => {
199+ onChange ( { ...value , styles : { } } , false ) ;
200+ } }
201+ />
202+ </ Grid >
203+ }
204+ >
205+ < CssValueListItem
206+ id = { offsetPlaceholder . toString ( ) }
207+ index = { index }
208+ label = {
209+ < Label truncate >
210+ { value . offset
211+ ? `${ roundOffset ( value . offset ) } %`
212+ : `auto (${ roundOffset ( offsetPlaceholder ) } %)` }
213+ </ Label >
214+ }
215+ buttons = {
216+ < Tooltip content = "Remove keyframe" >
217+ < SmallIconButton
218+ variant = "destructive"
219+ tabIndex = { - 1 }
220+ icon = { < MinusIcon /> }
221+ onClick = { onDelete }
222+ />
223+ </ Tooltip >
224+ }
225+ > </ CssValueListItem >
226+ </ FloatingPanel >
209227 ) ;
210228} ;
211229
@@ -232,7 +250,7 @@ export const Keyframes = ({
232250 const offsets = calcOffsets ( keyframes ) ;
233251
234252 return (
235- < Grid gap = { 1 } >
253+ < div >
236254 < SectionTitle
237255 collapsible = { false }
238256 suffix = {
@@ -251,52 +269,42 @@ export const Keyframes = ({
251269 >
252270 < SectionTitleLabel > Keyframes</ SectionTitleLabel >
253271 </ SectionTitle >
254- < ScrollArea >
255- < Grid gap = { 2 } >
256- { keyframes . map ( ( value , index ) => (
257- < Fragment key = { keyRefs . current [ index ] } >
258- { index > 0 && < Separator /> }
259- < Keyframe
260- key = { keyRefs . current [ index ] }
261- value = { value }
262- offsetPlaceholder = { offsets [ index ] }
263- onChange = { ( newValue , isEphemeral ) => {
264- if ( newValue === undefined && isEphemeral ) {
265- onChange ( undefined , true ) ;
266- return ;
267- }
268-
269- if ( newValue === undefined ) {
270- const newValues = [ ...keyframes ] ;
271- newValues . splice ( index , 1 ) ;
272- onChange ( newValues , isEphemeral ) ;
273- return ;
274- }
272+ < CssValueListArrowFocus >
273+ { keyframes . map ( ( value , index ) => (
274+ < Fragment key = { keyRefs . current [ index ] } >
275+ < Keyframe
276+ value = { value }
277+ index = { index }
278+ offsetPlaceholder = { offsets [ index ] }
279+ onChange = { ( newValue , isEphemeral ) => {
280+ let newValues = [ ...keyframes ] ;
281+ newValues [ index ] = newValue ;
275282
276- let newValues = [ ...keyframes ] ;
277- newValues [ index ] = newValue ;
278-
279- const { offset } = newValue ;
280- if ( offset === undefined ) {
281- onChange ( newValues , isEphemeral ) ;
282- return ;
283- }
283+ const { offset } = newValue ;
284+ if ( offset === undefined ) {
285+ onChange ( newValues , isEphemeral ) ;
286+ return ;
287+ }
284288
285- const insertionIndex = findInsertionIndex ( newValues , index ) ;
286- newValues = moveItem ( newValues , index , insertionIndex ) ;
287- keyRefs . current = moveItem (
288- keyRefs . current ,
289- index ,
290- insertionIndex
291- ) ;
289+ const insertionIndex = findInsertionIndex ( newValues , index ) ;
290+ newValues = moveItem ( newValues , index , insertionIndex ) ;
291+ keyRefs . current = moveItem (
292+ keyRefs . current ,
293+ index ,
294+ insertionIndex
295+ ) ;
292296
293- onChange ( newValues , isEphemeral ) ;
294- } }
295- />
296- </ Fragment >
297- ) ) }
298- </ Grid >
299- </ ScrollArea >
300- </ Grid >
297+ onChange ( newValues , isEphemeral ) ;
298+ } }
299+ onDelete = { ( ) => {
300+ const newValues = [ ...keyframes ] ;
301+ newValues . splice ( index , 1 ) ;
302+ onChange ( newValues , false ) ;
303+ } }
304+ />
305+ </ Fragment >
306+ ) ) }
307+ </ CssValueListArrowFocus >
308+ </ div >
301309 ) ;
302310} ;
0 commit comments