@octanejs/motion 0.1.33 → 0.1.35
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/package.json +19 -5
- package/src/index.ts +57 -5
- package/src/useMotionValue.ts +219 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/motion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -39,18 +39,32 @@
|
|
|
39
39
|
"./react-m": "./src/react-m.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"motion": "^12.42.2"
|
|
42
|
+
"motion": "^12.42.2",
|
|
43
|
+
"motion-dom": "^12.0.0"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
|
-
"octane": "0.1.
|
|
46
|
+
"octane": "0.1.36"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
49
|
+
"@swc/core": "1.15.41",
|
|
50
|
+
"@swc/jest": "0.2.39",
|
|
51
|
+
"@testing-library/dom": "^10.4.1",
|
|
52
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
53
|
+
"@testing-library/react": "16.3.2",
|
|
54
|
+
"@tsrx/react": "^0.2.56",
|
|
48
55
|
"esbuild": "^0.28.1",
|
|
56
|
+
"jest": "30.4.2",
|
|
57
|
+
"jest-environment-jsdom": "30.4.1",
|
|
58
|
+
"jest-fixed-jsdom": "0.0.10",
|
|
59
|
+
"react": "^19.2.7",
|
|
60
|
+
"react-dom": "^19.2.7",
|
|
49
61
|
"vitest": "^4.1.10",
|
|
50
|
-
"octane": "0.1.
|
|
62
|
+
"octane": "0.1.36"
|
|
51
63
|
},
|
|
52
64
|
"scripts": {
|
|
53
65
|
"test": "vitest run --root ../.. --project motion",
|
|
54
|
-
"typecheck": "tsgo --noEmit -p tsconfig.json"
|
|
66
|
+
"typecheck": "tsgo --noEmit -p tsconfig.json",
|
|
67
|
+
"test:upstream": "jest --config tests/upstream-jest.config.cjs --rootDir upstream --runInBand --no-watchman",
|
|
68
|
+
"upstream:verify": "node ./scripts/verify-upstream.mjs"
|
|
55
69
|
}
|
|
56
70
|
}
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,12 @@ import {
|
|
|
27
27
|
resolveVariant,
|
|
28
28
|
splitVariant,
|
|
29
29
|
} from './context';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
isMotionValue,
|
|
32
|
+
isTransformKey,
|
|
33
|
+
applyStyleValue,
|
|
34
|
+
removeTransformFn,
|
|
35
|
+
} from './useMotionValue';
|
|
31
36
|
import { useReducedMotionAtSlot } from './useReducedMotion';
|
|
32
37
|
import {
|
|
33
38
|
layoutAnimationMode,
|
|
@@ -58,6 +63,10 @@ const LAYOUT_GROUP_STATE = Symbol.for('octane-motion:layout-group-state');
|
|
|
58
63
|
const LAZY_STATE = Symbol.for('octane-motion:lazy-state');
|
|
59
64
|
const LAZY_EFFECT = Symbol.for('octane-motion:lazy-effect');
|
|
60
65
|
|
|
66
|
+
// Tracks MotionValue / transform shorthand keys last applied to a host so a
|
|
67
|
+
// rebind can clear stale inline styles without wiping them on unmount (exit
|
|
68
|
+
// clones still need the live visual state).
|
|
69
|
+
const motionStyleKeys = new WeakMap<HTMLElement, string[]>();
|
|
61
70
|
const boxOf = (n: HTMLElement): LayoutBox => {
|
|
62
71
|
const r = n.getBoundingClientRect();
|
|
63
72
|
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
@@ -303,6 +312,7 @@ function createMotionComponent(tag: string, preloadedFeatures: boolean): MotionC
|
|
|
303
312
|
latest.layoutMode = layoutAnimationMode(props.layout);
|
|
304
313
|
|
|
305
314
|
// `initial`: apply instantly on mount (before the animate effect runs).
|
|
315
|
+
// OCTANE DIVERGENCE[motion-initial-only-no-style-materialization][conformance:initial-only-no-style-materialization]
|
|
306
316
|
useLayoutEffect(
|
|
307
317
|
() => {
|
|
308
318
|
if (animationEnabled && resolvedInitial) {
|
|
@@ -359,25 +369,64 @@ function createMotionComponent(tag: string, preloadedFeatures: boolean): MotionC
|
|
|
359
369
|
|
|
360
370
|
// Motion values + static transform shorthands in `style`. MotionValues are
|
|
361
371
|
// subscribed (and update the element without a re-render); shorthands apply once.
|
|
372
|
+
// Re-bind when the style bag's MotionValue identities or shorthand values change
|
|
373
|
+
// so callers can swap `style={{ x }}` across updates (upstream useMotionValue).
|
|
374
|
+
const styleMvDeps: any[] = [];
|
|
375
|
+
if (props.style && typeof props.style === 'object') {
|
|
376
|
+
for (const key in props.style) {
|
|
377
|
+
const v = props.style[key];
|
|
378
|
+
if (isMotionValue(v) || isTransformKey(key)) styleMvDeps.push(key, v);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
362
381
|
useLayoutEffect(
|
|
363
382
|
() => {
|
|
383
|
+
// Clear only keys this effect previously managed that are absent from the
|
|
384
|
+
// new style bag. Patch transform functions in place so animate/layout/drag
|
|
385
|
+
// values on `style.transform` survive a MotionValue rebind.
|
|
386
|
+
// Do not clear in cleanup: on unmount the exit effect clones this node
|
|
387
|
+
// after layout-effect cleanups, and still needs the live transforms.
|
|
364
388
|
const style = props.style;
|
|
389
|
+
const prevKeys = motionStyleKeys.get(node);
|
|
390
|
+
if (prevKeys) {
|
|
391
|
+
for (const key of prevKeys) {
|
|
392
|
+
const next = style && typeof style === 'object' ? style[key] : undefined;
|
|
393
|
+
const stillManaged = next !== undefined && (isMotionValue(next) || isTransformKey(key));
|
|
394
|
+
if (stillManaged) continue;
|
|
395
|
+
// Plain static values stay in the style bag for host/domProps — do not blank
|
|
396
|
+
// them after a MotionValue→static switch (domProps already applied them).
|
|
397
|
+
if (next !== undefined) continue;
|
|
398
|
+
if (isTransformKey(key)) removeTransformFn(node, key);
|
|
399
|
+
else (node.style as any)[key] = '';
|
|
400
|
+
}
|
|
401
|
+
motionStyleKeys.delete(node);
|
|
402
|
+
}
|
|
403
|
+
|
|
365
404
|
if (!style || typeof style !== 'object') return;
|
|
366
|
-
const
|
|
405
|
+
const appliedKeys: string[] = [];
|
|
367
406
|
const cleanups: Array<() => void> = [];
|
|
407
|
+
// Shared bag of currently bound transform shorthands so a compound
|
|
408
|
+
// FLIP patch can keep sibling axes owned by other style MotionValues.
|
|
409
|
+
const transformState: Record<string, any> = {};
|
|
368
410
|
for (const key in style) {
|
|
369
411
|
const v = style[key];
|
|
370
412
|
if (isMotionValue(v)) {
|
|
371
|
-
|
|
413
|
+
function apply(val: any) {
|
|
414
|
+
applyStyleValue(node, key, val, transformState);
|
|
415
|
+
}
|
|
372
416
|
apply(v.get());
|
|
417
|
+
appliedKeys.push(key);
|
|
373
418
|
cleanups.push(v.on('change', apply));
|
|
374
419
|
} else if (isTransformKey(key)) {
|
|
375
420
|
applyStyleValue(node, key, v, transformState);
|
|
421
|
+
appliedKeys.push(key);
|
|
376
422
|
}
|
|
377
423
|
}
|
|
378
|
-
|
|
424
|
+
if (appliedKeys.length) motionStyleKeys.set(node, appliedKeys);
|
|
425
|
+
return function unsubscribeMotionStyles() {
|
|
426
|
+
for (const cleanup of cleanups) cleanup();
|
|
427
|
+
};
|
|
379
428
|
},
|
|
380
|
-
|
|
429
|
+
styleMvDeps,
|
|
381
430
|
MV,
|
|
382
431
|
);
|
|
383
432
|
|
|
@@ -518,6 +567,7 @@ function createMotionComponent(tag: string, preloadedFeatures: boolean): MotionC
|
|
|
518
567
|
// moved/resized vs the previous commit, apply the inverse transform instantly
|
|
519
568
|
// then animate it back to identity. (A single-element FLIP; the full projection
|
|
520
569
|
// tree — nested/shared layout, scale correction — is out of scope.)
|
|
570
|
+
// OCTANE DIVERGENCE[motion-bounded-layout-flip][conformance:bounded-layout-flip]
|
|
521
571
|
useLayoutEffect(
|
|
522
572
|
() => {
|
|
523
573
|
if (!layoutEnabled || !props.layout) {
|
|
@@ -552,6 +602,7 @@ function createMotionComponent(tag: string, preloadedFeatures: boolean): MotionC
|
|
|
552
602
|
|
|
553
603
|
// `layoutId`: shared-element crossfade. On mount, if a same-id element recently
|
|
554
604
|
// unmounted, FLIP from its recorded box to ours.
|
|
605
|
+
// OCTANE DIVERGENCE[motion-bounded-layoutId][conformance:bounded-layoutId]
|
|
555
606
|
useLayoutEffect(
|
|
556
607
|
() => {
|
|
557
608
|
if (!layoutEnabled) return;
|
|
@@ -580,6 +631,7 @@ function createMotionComponent(tag: string, preloadedFeatures: boolean): MotionC
|
|
|
580
631
|
// Unmount: this cleanup runs while the node is still in the DOM. Record shared
|
|
581
632
|
// layout state here—not in the keyed claim effect, whose cleanup also runs on
|
|
582
633
|
// dependency changes—then clone any exiting node outside the block's range.
|
|
634
|
+
// OCTANE DIVERGENCE[motion-exit-cleanup-before-detach][conformance:exit-cleanup-before-detach]
|
|
583
635
|
useLayoutEffect(
|
|
584
636
|
() => () => {
|
|
585
637
|
const n: HTMLElement | null = latest.node;
|
package/src/useMotionValue.ts
CHANGED
|
@@ -47,29 +47,235 @@ const PX_KEYS = new Set(['x', 'y', 'z']);
|
|
|
47
47
|
const DEG_KEYS = new Set(['rotate', 'rotateX', 'rotateY', 'rotateZ', 'skewX', 'skewY']);
|
|
48
48
|
const NO_UNIT = new Set(['opacity', 'zIndex', 'scale', 'scaleX', 'scaleY']);
|
|
49
49
|
|
|
50
|
+
// Motion-style CSS transform function order: translate before scale/rotate so
|
|
51
|
+
// offsets are not scaled when both are present.
|
|
52
|
+
const TRANSFORM_ORDER = [
|
|
53
|
+
'translateX',
|
|
54
|
+
'translateY',
|
|
55
|
+
'translateZ',
|
|
56
|
+
'scale',
|
|
57
|
+
'scaleX',
|
|
58
|
+
'scaleY',
|
|
59
|
+
'rotate',
|
|
60
|
+
'rotateX',
|
|
61
|
+
'rotateY',
|
|
62
|
+
'rotateZ',
|
|
63
|
+
'skewX',
|
|
64
|
+
'skewY',
|
|
65
|
+
];
|
|
66
|
+
|
|
50
67
|
export function isTransformKey(k: string): boolean {
|
|
51
68
|
return k in TRANSFORM_FN;
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
function unitizeTransformValue(key: string, val: any): string {
|
|
72
|
+
if (typeof val !== 'number') return String(val);
|
|
73
|
+
if (PX_KEYS.has(key)) return `${val}px`;
|
|
74
|
+
if (DEG_KEYS.has(key)) return `${val}deg`;
|
|
75
|
+
return `${val}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Locate `fn(...)` with balanced parentheses so nested `calc(...)` survives. */
|
|
79
|
+
function findTransformFnRange(
|
|
80
|
+
transform: string,
|
|
81
|
+
fn: string,
|
|
82
|
+
): { start: number; end: number } | null {
|
|
83
|
+
const needle = fn + '(';
|
|
84
|
+
let from = 0;
|
|
85
|
+
while (from < transform.length) {
|
|
86
|
+
const idx = transform.indexOf(needle, from);
|
|
87
|
+
if (idx === -1) return null;
|
|
88
|
+
// Prefer a token boundary so `scale(` does not match inside `scaleX(`.
|
|
89
|
+
if (idx > 0 && /\S/.test(transform.charAt(idx - 1))) {
|
|
90
|
+
from = idx + 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let depth = 0;
|
|
94
|
+
for (let i = idx + fn.length; i < transform.length; i++) {
|
|
95
|
+
const ch = transform.charAt(i);
|
|
96
|
+
if (ch === '(') depth++;
|
|
97
|
+
else if (ch === ')') {
|
|
98
|
+
depth--;
|
|
99
|
+
if (depth === 0) return { start: idx, end: i + 1 };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function insertTransformFn(current: string, next: string, fn: string): string {
|
|
108
|
+
const orderIdx = TRANSFORM_ORDER.indexOf(fn);
|
|
109
|
+
if (orderIdx === -1) return `${current} ${next}`.trim();
|
|
110
|
+
for (let i = orderIdx + 1; i < TRANSFORM_ORDER.length; i++) {
|
|
111
|
+
const later = findTransformFnRange(current, TRANSFORM_ORDER[i]);
|
|
112
|
+
if (later) {
|
|
113
|
+
const before = current.slice(0, later.start).trimEnd();
|
|
114
|
+
const after = current.slice(later.start).trimStart();
|
|
115
|
+
return before ? `${before} ${next} ${after}` : `${next} ${after}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return `${current} ${next}`.trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Split `fn(a, b)` args at depth-0 commas so nested `calc(...)` survives. */
|
|
122
|
+
function splitTransformArgs(inner: string): string[] {
|
|
123
|
+
const parts: string[] = [];
|
|
124
|
+
let start = 0;
|
|
125
|
+
let depth = 0;
|
|
126
|
+
for (let i = 0; i < inner.length; i++) {
|
|
127
|
+
const ch = inner.charAt(i);
|
|
128
|
+
if (ch === '(') depth++;
|
|
129
|
+
else if (ch === ')') depth--;
|
|
130
|
+
else if (ch === ',' && depth === 0) {
|
|
131
|
+
const part = inner.slice(start, i).trim();
|
|
132
|
+
if (part) parts.push(part);
|
|
133
|
+
start = i + 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const tail = inner.slice(start).trim();
|
|
137
|
+
if (tail) parts.push(tail);
|
|
138
|
+
return parts;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function replaceTransformRange(
|
|
142
|
+
current: string,
|
|
143
|
+
range: { start: number; end: number },
|
|
144
|
+
next: string,
|
|
145
|
+
): string {
|
|
146
|
+
return (current.slice(0, range.start) + next + current.slice(range.end)).trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Layout FLIP writes compound `translate(x, y)` / `scale(sx, sy)`. Decompose those
|
|
151
|
+
* into shorthands when a style MotionValue binds so unbind can remove only that
|
|
152
|
+
* shorthand without corrupting or orphaning the layout contribution.
|
|
153
|
+
*
|
|
154
|
+
* When `transformState` already holds a sibling axis (another bound style
|
|
155
|
+
* MotionValue/static shorthand), prefer that over the FLIP compound value so a
|
|
156
|
+
* single-axis patch does not clobber the sibling until it emits again.
|
|
157
|
+
*/
|
|
158
|
+
function patchCompoundTransform(
|
|
159
|
+
current: string,
|
|
160
|
+
key: string,
|
|
161
|
+
val: any,
|
|
162
|
+
transformState?: Record<string, any>,
|
|
163
|
+
): string | null {
|
|
164
|
+
const unitized = unitizeTransformValue(key, val);
|
|
165
|
+
if (key === 'x' || key === 'y') {
|
|
166
|
+
const range = findTransformFnRange(current, 'translate');
|
|
167
|
+
if (!range) return null;
|
|
168
|
+
const inner = current.slice(range.start + 'translate('.length, range.end - 1);
|
|
169
|
+
const args = splitTransformArgs(inner);
|
|
170
|
+
let x =
|
|
171
|
+
transformState && transformState.x !== undefined
|
|
172
|
+
? unitizeTransformValue('x', transformState.x)
|
|
173
|
+
: args[0] || '0px';
|
|
174
|
+
let y =
|
|
175
|
+
transformState && transformState.y !== undefined
|
|
176
|
+
? unitizeTransformValue('y', transformState.y)
|
|
177
|
+
: args[1] || '0px';
|
|
178
|
+
if (key === 'x') x = unitized;
|
|
179
|
+
else y = unitized;
|
|
180
|
+
return replaceTransformRange(current, range, `translateX(${x}) translateY(${y})`);
|
|
181
|
+
}
|
|
182
|
+
if (key === 'scaleX' || key === 'scaleY') {
|
|
183
|
+
const range = findTransformFnRange(current, 'scale');
|
|
184
|
+
if (!range) return null;
|
|
185
|
+
const inner = current.slice(range.start + 'scale('.length, range.end - 1);
|
|
186
|
+
const args = splitTransformArgs(inner);
|
|
187
|
+
let sx =
|
|
188
|
+
transformState && transformState.scaleX !== undefined
|
|
189
|
+
? unitizeTransformValue('scaleX', transformState.scaleX)
|
|
190
|
+
: args[0] || '1';
|
|
191
|
+
let sy =
|
|
192
|
+
transformState && transformState.scaleY !== undefined
|
|
193
|
+
? unitizeTransformValue('scaleY', transformState.scaleY)
|
|
194
|
+
: args[1] !== undefined
|
|
195
|
+
? args[1]
|
|
196
|
+
: args[0] || '1';
|
|
197
|
+
if (key === 'scaleX') sx = unitized;
|
|
198
|
+
else sy = unitized;
|
|
199
|
+
return replaceTransformRange(current, range, `scaleX(${sx}) scaleY(${sy})`);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Patch one transform function into the live CSS string without wiping others. */
|
|
205
|
+
export function patchTransformFn(
|
|
57
206
|
node: HTMLElement,
|
|
58
207
|
key: string,
|
|
59
208
|
val: any,
|
|
60
|
-
transformState
|
|
209
|
+
transformState?: Record<string, any>,
|
|
61
210
|
): void {
|
|
62
211
|
const fn = TRANSFORM_FN[key];
|
|
63
|
-
if (fn)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
212
|
+
if (!fn) return;
|
|
213
|
+
const next = `${fn}(${unitizeTransformValue(key, val)})`;
|
|
214
|
+
const current = node.style.transform || '';
|
|
215
|
+
if (!current || current === 'none') {
|
|
216
|
+
node.style.transform = next;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const range = findTransformFnRange(current, fn);
|
|
220
|
+
if (range) {
|
|
221
|
+
// Layout FLIP writes asymmetric `scale(sx, sy)`. A style `scale` MotionValue
|
|
222
|
+
// shares the `scale(` token, but replacing that compound with uniform
|
|
223
|
+
// `scale(n)` drops an axis mid-FLIP. Leave asymmetric compounds alone —
|
|
224
|
+
// `scaleX`/`scaleY` still decompose via patchCompoundTransform below.
|
|
225
|
+
if (key === 'scale') {
|
|
226
|
+
const inner = current.slice(range.start + 'scale('.length, range.end - 1);
|
|
227
|
+
const args = splitTransformArgs(inner);
|
|
228
|
+
if (args.length >= 2 && args[0] !== args[1]) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
71
231
|
}
|
|
72
|
-
node.style.transform =
|
|
232
|
+
node.style.transform = replaceTransformRange(current, range, next);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const compound = patchCompoundTransform(current, key, val, transformState);
|
|
236
|
+
if (compound !== null) {
|
|
237
|
+
node.style.transform = compound;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
node.style.transform = insertTransformFn(current, next, fn);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Remove one shorthand transform function from the live CSS string.
|
|
245
|
+
* Do not mutate compound layout FLIP `translate(...)` / `scale(...)` forms —
|
|
246
|
+
* those are owned by layout, not by style MotionValue unbind.
|
|
247
|
+
*/
|
|
248
|
+
export function removeTransformFn(node: HTMLElement, key: string): void {
|
|
249
|
+
const fn = TRANSFORM_FN[key];
|
|
250
|
+
if (!fn) return;
|
|
251
|
+
const current = node.style.transform || '';
|
|
252
|
+
if (!current || current === 'none') return;
|
|
253
|
+
const range = findTransformFnRange(current, fn);
|
|
254
|
+
if (!range) return;
|
|
255
|
+
// Style `scale` shares the `scale(` token with layout FLIP's compound
|
|
256
|
+
// `scale(sx, sy)`. Only remove single-arg style-owned forms; leave compounds
|
|
257
|
+
// for layout (mirrors patchTransformFn's asymmetric-scale guard on bind).
|
|
258
|
+
if (key === 'scale') {
|
|
259
|
+
const inner = current.slice(range.start + 'scale('.length, range.end - 1);
|
|
260
|
+
const args = splitTransformArgs(inner);
|
|
261
|
+
if (args.length >= 2) return;
|
|
262
|
+
}
|
|
263
|
+
const before = current.slice(0, range.start).trimEnd();
|
|
264
|
+
const after = current.slice(range.end).trimStart();
|
|
265
|
+
node.style.transform = before && after ? `${before} ${after}` : before || after;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Apply one style/transform value to the element. Transform shorthands patch the
|
|
269
|
+
// live `transform` string in place so animate/layout/drag values survive rebinds.
|
|
270
|
+
export function applyStyleValue(
|
|
271
|
+
node: HTMLElement,
|
|
272
|
+
key: string,
|
|
273
|
+
val: any,
|
|
274
|
+
transformState?: Record<string, any>,
|
|
275
|
+
): void {
|
|
276
|
+
if (TRANSFORM_FN[key]) {
|
|
277
|
+
if (transformState) transformState[key] = val;
|
|
278
|
+
patchTransformFn(node, key, val, transformState);
|
|
73
279
|
} else {
|
|
74
280
|
(node.style as any)[key] = typeof val === 'number' && !NO_UNIT.has(key) ? `${val}px` : val;
|
|
75
281
|
}
|