@humanspeak/svelte-motion 0.0.13 → 0.1.0

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/README.md CHANGED
@@ -35,6 +35,17 @@ This package includes support for `MotionConfig`, which allows you to set defaul
35
35
  </MotionConfig>
36
36
  ```
37
37
 
38
+ ### Layout Animations (FLIP)
39
+
40
+ Svelte Motion supports minimal layout animations via FLIP using the `layout` prop:
41
+
42
+ ```svelte
43
+ <motion.div layout transition={{ duration: 0.25 }} />
44
+ ```
45
+
46
+ - **`layout`**: smoothly animates translation and scale between layout changes (size and position).
47
+ - **`layout="position"`**: only animates translation (no scale).
48
+
38
49
  #### Current Limitations
39
50
 
40
51
  Some Motion features are not yet implemented:
@@ -43,9 +54,7 @@ Some Motion features are not yet implemented:
43
54
  - `features` configuration
44
55
  - Performance optimizations like `transformPagePoint`
45
56
  - Advanced transition controls
46
- - `layout`/`layoutId` (FLIP) — prototype planned
47
-
48
- We're actively working on adding these features. Check our GitHub issues for progress updates or to contribute.
57
+ - Shared layout / `layoutId` (planned)
49
58
 
50
59
  ## External Dependencies
51
60
 
@@ -63,8 +72,9 @@ This package carefully selects its dependencies to provide a robust and maintain
63
72
 
64
73
  | Motion | Demo / Route | REPL |
65
74
  | -------------------------------------------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------- |
66
- | [React - Enter Animation](https://examples.motion.dev/react/enter-animation) | `/tests/motion/enter-animation` | [View Example](https://svelte.dev/playground/7f60c347729f4ea48b1a4590c9dedc02?version=5.20.2) |
67
- | HTML Content (0→100 counter) | `/tests/motion/html-content` | [View Example](https://svelte.dev/playground/31cd72df4a3242b4b4589501a25e774f?version=5.38.10) |
75
+ | [React - Enter Animation](https://examples.motion.dev/react/enter-animation) | `/tests/motion/enter-animation` | [View Example](https://svelte.dev/playground/7f60c347729f4ea48b1a4590c9dedc02?version=5.38.10) |
76
+ | [HTML Content (0→100 counter)](https://examples.motion.dev/react/html-content) | `/tests/motion/html-content` | [View Example](https://svelte.dev/playground/31cd72df4a3242b4b4589501a25e774f?version=5.38.10) |
77
+ | [Aspect Ratio](https://examples.motion.dev/react/aspect-ratio) | `/tests/motion/aspect-ratio` | [View Example](https://svelte.dev/playground/1bf60e745fae44f5becb4c830fde9b6e?version=5.38.10) |
68
78
  | [Random - Shiny Button](https://www.youtube.com/watch?v=jcpLprT5F0I) by [@verse\_](https://x.com/verse_) | `/tests/random/shiny-button` | [View Example](https://svelte.dev/playground/96f9e0bf624f4396adaf06c519147450?version=5.38.10) |
69
79
  | [Fancy Like Button](https://github.com/DRlFTER/fancyLikeButton) | `/tests/random/fancy-like-button` | [View Example](https://svelte.dev/playground/c34b7e53d41c48b0ab1eaf21ca120c6e?version=5.38.10) |
70
80
 
@@ -24,6 +24,7 @@
24
24
  style: styleProp,
25
25
  class: classProp,
26
26
  whileTap: whileTapProp,
27
+ layout: layoutProp,
27
28
  ...rest
28
29
  }: Props = $props()
29
30
  let element: HTMLElement | null = $state(null)
@@ -76,6 +77,102 @@
76
77
  }
77
78
  }
78
79
 
80
+ // Minimal layout animation using FLIP when `layout` is enabled.
81
+ // When layout === 'position' we only translate.
82
+ // When layout === true we also scale to smoothly interpolate size changes.
83
+ let lastRect: DOMRect | null = null
84
+ $effect(() => {
85
+ if (!(element && layoutProp && isLoaded === 'ready')) return
86
+
87
+ // Initialize last rect on first ready frame
88
+ const measure = () => {
89
+ const prev = element!.style.transform
90
+ element!.style.transform = 'none'
91
+ const rect = element!.getBoundingClientRect()
92
+ element!.style.transform = prev
93
+ return rect
94
+ }
95
+ lastRect = measure()
96
+ // Hint compositor for smoother FLIP transforms
97
+ element!.style.willChange = 'transform'
98
+ element!.style.transformOrigin = '0 0'
99
+
100
+ let rafId: number | null = null
101
+ const runFlip = () => {
102
+ if (!lastRect) {
103
+ lastRect = measure()
104
+ return
105
+ }
106
+ const prev = element!.style.transform
107
+ element!.style.transform = 'none'
108
+ const next = element!.getBoundingClientRect()
109
+ element!.style.transform = prev
110
+ // Use top-left corner for FLIP to match transformOrigin '0 0'
111
+ const dx = lastRect.left - next.left
112
+ const dy = lastRect.top - next.top
113
+ const sx = next.width > 0 ? lastRect.width / next.width : 1
114
+ const sy = next.height > 0 ? lastRect.height / next.height : 1
115
+
116
+ const shouldTranslate = Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5
117
+ const shouldScale =
118
+ layoutProp !== 'position' && (Math.abs(1 - sx) > 0.01 || Math.abs(1 - sy) > 0.01)
119
+
120
+ if (shouldTranslate || shouldScale) {
121
+ const keyframes: Record<string, unknown> = {}
122
+ if (shouldTranslate) {
123
+ keyframes.x = [dx, 0]
124
+ keyframes.y = [dy, 0]
125
+ }
126
+ if (shouldScale) {
127
+ keyframes.scaleX = [sx, 1]
128
+ keyframes.scaleY = [sy, 1]
129
+ ;(keyframes as Record<string, unknown>).transformOrigin = '0 0'
130
+ }
131
+ // Apply the inverse transform immediately so we don't flash the new layout
132
+ // before the animation starts (classic FLIP: First, Last, Invert, Play)
133
+ const parts: string[] = []
134
+ if (shouldTranslate) parts.push(`translate(${dx}px, ${dy}px)`)
135
+ if (shouldScale) parts.push(`scale(${sx}, ${sy})`)
136
+ element!.style.transformOrigin = '0 0'
137
+ element!.style.transform = parts.join(' ')
138
+ animate(
139
+ element!,
140
+ keyframes as unknown as import('motion').DOMKeyframesDefinition,
141
+ (mergedTransition ?? {}) as import('motion').AnimationOptions
142
+ )
143
+ }
144
+ lastRect = next
145
+ }
146
+
147
+ const scheduleFlip = () => {
148
+ if (rafId) cancelAnimationFrame(rafId)
149
+ rafId = requestAnimationFrame(() => {
150
+ rafId = null
151
+ runFlip()
152
+ })
153
+ }
154
+ const ro = new ResizeObserver(() => scheduleFlip())
155
+ ro.observe(element)
156
+ // Also observe attribute/class changes and nearby DOM mutations that commonly cause reflow/reposition
157
+ const mo = new MutationObserver(() => scheduleFlip())
158
+ mo.observe(element, { attributes: true, attributeFilter: ['class', 'style'] })
159
+ if (element.parentElement) {
160
+ mo.observe(element.parentElement, { childList: true, subtree: false, attributes: true })
161
+ }
162
+
163
+ return () => {
164
+ ro.disconnect()
165
+ mo.disconnect()
166
+ lastRect = null
167
+ // Reset compositor hints on teardown
168
+ if (element) {
169
+ element.style.willChange = ''
170
+ element.style.transformOrigin = ''
171
+ element.style.transform = ''
172
+ }
173
+ if (rafId) cancelAnimationFrame(rafId)
174
+ }
175
+ })
79
176
  // Merge style for before/after ready so styles carry through post-anim
80
177
  // Merge styles directly in markup; keep effect solely for readiness logic
81
178
 
package/dist/types.d.ts CHANGED
@@ -66,6 +66,8 @@ export type MotionProps = {
66
66
  style?: string;
67
67
  /** CSS classes */
68
68
  class?: string;
69
+ /** Enable FLIP layout animations; "position" limits to translation only */
70
+ layout?: boolean | 'position';
69
71
  };
70
72
  /**
71
73
  * Configuration properties for motion/animation components.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.0.13",
3
+ "version": "0.1.0",
4
4
  "description": "A lightweight animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -14,6 +14,7 @@
14
14
  "micro-interactions",
15
15
  "performance"
16
16
  ],
17
+ "homepage": "https://motion.svelte.page",
17
18
  "bugs": {
18
19
  "url": "https://github.com/humanspeak/svelte-motion/issues"
19
20
  },
@@ -51,14 +52,15 @@
51
52
  }
52
53
  },
53
54
  "dependencies": {
54
- "motion": "^12.23.13"
55
+ "motion": "^12.23.15"
55
56
  },
56
57
  "devDependencies": {
58
+ "@changesets/cli": "^2.27.8",
57
59
  "@eslint/compat": "^1.3.2",
58
60
  "@eslint/js": "^9.35.0",
59
61
  "@playwright/test": "^1.55.0",
60
62
  "@sveltejs/adapter-auto": "^6.1.0",
61
- "@sveltejs/kit": "^2.41.0",
63
+ "@sveltejs/kit": "^2.42.1",
62
64
  "@sveltejs/package": "^2.5.2",
63
65
  "@sveltejs/vite-plugin-svelte": "^6.2.0",
64
66
  "@tailwindcss/aspect-ratio": "^0.4.2",
@@ -77,11 +79,12 @@
77
79
  "husky": "^9.1.7",
78
80
  "jsdom": "^27.0.0",
79
81
  "prettier": "^3.6.2",
80
- "prettier-plugin-organize-imports": "^4.2.0",
82
+ "prettier-plugin-organize-imports": "^4.3.0",
83
+ "prettier-plugin-sort-json": "^4.1.1",
81
84
  "prettier-plugin-svelte": "^3.4.0",
82
85
  "prettier-plugin-tailwindcss": "^0.6.14",
83
- "publint": "^0.3.12",
84
- "svelte": "^5.38.10",
86
+ "publint": "^0.3.13",
87
+ "svelte": "^5.39.2",
85
88
  "svelte-check": "^4.3.1",
86
89
  "tailwind-merge": "^3.3.1",
87
90
  "tailwind-variants": "^3.1.1",
@@ -90,7 +93,7 @@
90
93
  "tsx": "^4.20.5",
91
94
  "typescript": "^5.9.2",
92
95
  "typescript-eslint": "^8.44.0",
93
- "vite": "^7.1.5",
96
+ "vite": "^7.1.6",
94
97
  "vite-tsconfig-paths": "^5.1.4",
95
98
  "vitest": "^3.2.4"
96
99
  },
@@ -118,6 +121,9 @@
118
121
  "build": "vite build && npm run package",
119
122
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
120
123
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
124
+ "cs:add": "changeset",
125
+ "cs:publish": "pnpm run build && changeset publish",
126
+ "cs:version": "changeset version && pnpm i --lockfile-only",
121
127
  "dev": "vite dev",
122
128
  "format": "prettier --write .",
123
129
  "generate": "tsx scripts/generate-html.ts",
@@ -126,6 +132,7 @@
126
132
  "lint:fix": "npm run format && eslint . --fix",
127
133
  "package": "svelte-kit sync && svelte-package && publint",
128
134
  "preview": "vite preview",
135
+ "release": "pnpm run cs:version && pnpm run cs:publish",
129
136
  "test": "vitest run --coverage",
130
137
  "test:all": "npm run test && npm run test:e2e",
131
138
  "test:e2e": "playwright test",