@@ -7,8 +7,11 @@ import {
77 STUDIO_OFFSET_Y_PROP ,
88 STUDIO_WIDTH_PROP ,
99 STUDIO_HEIGHT_PROP ,
10+ STUDIO_ROTATION_PROP ,
1011 STUDIO_PATH_OFFSET_ATTR ,
1112 STUDIO_BOX_SIZE_ATTR ,
13+ STUDIO_ROTATION_ATTR ,
14+ STUDIO_ROTATION_DRAFT_ATTR ,
1215 STUDIO_ORIGINAL_TRANSLATE_ATTR ,
1316 STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR ,
1417 STUDIO_ORIGINAL_WIDTH_ATTR ,
@@ -24,13 +27,26 @@ import {
2427 STUDIO_ORIGINAL_SCALE_ATTR ,
2528 STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR ,
2629 STUDIO_ORIGINAL_DISPLAY_ATTR ,
30+ STUDIO_ORIGINAL_ROTATE_ATTR ,
31+ STUDIO_ORIGINAL_INLINE_ROTATE_ATTR ,
32+ STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR ,
2733 STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR ,
2834} from "./manualEditsTypes" ;
35+ import {
36+ STUDIO_MOTION_ATTR ,
37+ STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR ,
38+ STUDIO_MOTION_ORIGINAL_OPACITY_ATTR ,
39+ STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR ,
40+ } from "./studioMotionTypes" ;
2941import {
3042 buildPathOffsetPatches ,
3143 buildClearPathOffsetPatches ,
3244 buildBoxSizePatches ,
3345 buildClearBoxSizePatches ,
46+ buildRotationPatches ,
47+ buildClearRotationPatches ,
48+ buildMotionPatches ,
49+ buildClearMotionPatches ,
3450} from "./manualEditsDomPatches" ;
3551
3652/* ── helpers ── */
@@ -210,3 +226,117 @@ describe("buildBoxSizePatches / buildClearBoxSizePatches", () => {
210226 assertClearCoversKeys ( buildBoxSizePatches ( e ) , buildClearBoxSizePatches ( e ) ) ;
211227 } ) ;
212228} ) ;
229+
230+ /* ── Rotation ────────────────────────────────────────────────────────────── */
231+
232+ describe ( "buildRotationPatches / buildClearRotationPatches" , ( ) => {
233+ function populatedRotEl ( ) : HTMLElement {
234+ const e = div ( ) ;
235+ e . style . setProperty ( STUDIO_ROTATION_PROP , "45" ) ;
236+ e . style . setProperty ( "rotate" , "45deg" ) ;
237+ e . style . setProperty ( "transform-origin" , "left center" ) ;
238+ e . style . setProperty ( "display" , "block" ) ;
239+ e . setAttribute ( STUDIO_ORIGINAL_ROTATE_ATTR , "0deg" ) ;
240+ e . setAttribute ( STUDIO_ORIGINAL_INLINE_ROTATE_ATTR , "0deg" ) ;
241+ e . setAttribute ( STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR , "center center" ) ;
242+ e . setAttribute ( STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR , "flex" ) ;
243+ return e ;
244+ }
245+
246+ it ( "populated: captures rotation styles, attrs, and transform-display marker in declaration order" , ( ) => {
247+ const ops = buildRotationPatches ( populatedRotEl ( ) ) ;
248+ expect ( ops ) . toEqual ( [
249+ { type : "inline-style" , property : STUDIO_ROTATION_PROP , value : "45" } ,
250+ { type : "inline-style" , property : "rotate" , value : "45deg" } ,
251+ { type : "inline-style" , property : "transform-origin" , value : "left center" } ,
252+ { type : "inline-style" , property : "display" , value : "block" } ,
253+ { type : "attribute" , property : STUDIO_ROTATION_ATTR , value : "true" } ,
254+ { type : "attribute" , property : STUDIO_ORIGINAL_ROTATE_ATTR , value : "0deg" } ,
255+ { type : "attribute" , property : STUDIO_ORIGINAL_INLINE_ROTATE_ATTR , value : "0deg" } ,
256+ {
257+ type : "attribute" ,
258+ property : STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR ,
259+ value : "center center" ,
260+ } ,
261+ { type : "attribute" , property : STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR , value : "flex" } ,
262+ ] ) ;
263+ } ) ;
264+
265+ it ( "empty: bare element yields only the rotation marker" , ( ) => {
266+ expect ( buildRotationPatches ( div ( ) ) ) . toEqual ( [
267+ { type : "attribute" , property : STUDIO_ROTATION_ATTR , value : "true" } ,
268+ ] ) ;
269+ } ) ;
270+
271+ it ( "clear: restores rotate and transform-origin from orig attrs, nulls draft attr" , ( ) => {
272+ const e = div ( ) ;
273+ e . setAttribute ( STUDIO_ORIGINAL_INLINE_ROTATE_ATTR , "30deg" ) ;
274+ e . setAttribute ( STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR , "top left" ) ;
275+ e . setAttribute ( STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR , "grid" ) ;
276+ const ops = buildClearRotationPatches ( e ) ;
277+ expect ( ops ) . toEqual ( [
278+ { type : "inline-style" , property : STUDIO_ROTATION_PROP , value : null } ,
279+ { type : "inline-style" , property : "rotate" , value : "30deg" } ,
280+ { type : "inline-style" , property : "transform-origin" , value : "top left" } ,
281+ { type : "attribute" , property : STUDIO_ROTATION_ATTR , value : null } ,
282+ { type : "attribute" , property : STUDIO_ROTATION_DRAFT_ATTR , value : null } ,
283+ { type : "attribute" , property : STUDIO_ORIGINAL_ROTATE_ATTR , value : null } ,
284+ { type : "attribute" , property : STUDIO_ORIGINAL_INLINE_ROTATE_ATTR , value : null } ,
285+ { type : "attribute" , property : STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR , value : null } ,
286+ { type : "inline-style" , property : "display" , value : "grid" } ,
287+ { type : "attribute" , property : STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR , value : null } ,
288+ ] ) ;
289+ } ) ;
290+
291+ it ( "build/clear symmetry: clear addresses every {type,property} key that build emits" , ( ) => {
292+ const e = populatedRotEl ( ) ;
293+ assertClearCoversKeys ( buildRotationPatches ( e ) , buildClearRotationPatches ( e ) ) ;
294+ } ) ;
295+ } ) ;
296+
297+ /* ── Motion ──────────────────────────────────────────────────────────────── */
298+
299+ describe ( "buildMotionPatches / buildClearMotionPatches" , ( ) => {
300+ const MOTION_JSON = '{"kind":"gsap-motion","start":0,"duration":1}' ;
301+
302+ function populatedMotionEl ( ) : HTMLElement {
303+ const e = div ( ) ;
304+ e . setAttribute ( STUDIO_MOTION_ATTR , MOTION_JSON ) ;
305+ e . setAttribute ( STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR , "translateX(0)" ) ;
306+ e . setAttribute ( STUDIO_MOTION_ORIGINAL_OPACITY_ATTR , "1" ) ;
307+ e . setAttribute ( STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR , "visible" ) ;
308+ return e ;
309+ }
310+
311+ it ( "populated: captures motion JSON and all three original attrs when motion attr is present" , ( ) => {
312+ const ops = buildMotionPatches ( populatedMotionEl ( ) ) ;
313+ expect ( ops ) . toEqual ( [
314+ { type : "attribute" , property : STUDIO_MOTION_ATTR , value : MOTION_JSON } ,
315+ {
316+ type : "attribute" ,
317+ property : STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR ,
318+ value : "translateX(0)" ,
319+ } ,
320+ { type : "attribute" , property : STUDIO_MOTION_ORIGINAL_OPACITY_ATTR , value : "1" } ,
321+ { type : "attribute" , property : STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR , value : "visible" } ,
322+ ] ) ;
323+ } ) ;
324+
325+ it ( "empty: returns [] when STUDIO_MOTION_ATTR is absent" , ( ) => {
326+ expect ( buildMotionPatches ( div ( ) ) ) . toEqual ( [ ] ) ;
327+ } ) ;
328+
329+ it ( "clear: always nulls all four motion attrs regardless of element state" , ( ) => {
330+ expect ( buildClearMotionPatches ( div ( ) ) ) . toEqual ( [
331+ { type : "attribute" , property : STUDIO_MOTION_ATTR , value : null } ,
332+ { type : "attribute" , property : STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR , value : null } ,
333+ { type : "attribute" , property : STUDIO_MOTION_ORIGINAL_OPACITY_ATTR , value : null } ,
334+ { type : "attribute" , property : STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR , value : null } ,
335+ ] ) ;
336+ } ) ;
337+
338+ it ( "build/clear symmetry: clear addresses every {type,property} key that build emits" , ( ) => {
339+ const e = populatedMotionEl ( ) ;
340+ assertClearCoversKeys ( buildMotionPatches ( e ) , buildClearMotionPatches ( e ) ) ;
341+ } ) ;
342+ } ) ;
0 commit comments