├── .eslintignore ├── .eslintrc.json ├── .github └── pull_request_template.md ├── .gitignore ├── .prettierrc ├── .yarnrc.yml ├── LICENSE ├── README.md ├── docs ├── mdx-components.js ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── src │ ├── app │ │ ├── GlobalStylings.tsx │ │ ├── _meta.ts │ │ ├── docs │ │ │ └── [[...mdxPath]] │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── opengraph-image.png │ │ └── page.tsx │ └── content │ │ ├── _meta.ts │ │ └── index.mdx └── tsconfig.json ├── mobxmotion ├── .npmignore ├── README.md ├── demo │ ├── Demo.tsx │ ├── app.tsx │ ├── index.html │ └── index.tsx ├── package.json ├── src │ ├── AnimatedSpring.ts │ ├── MobxMotion.tsx │ ├── Spring.ts │ ├── components.tsx │ ├── hooks.ts │ ├── index.ts │ ├── springConfig.ts │ ├── springStep.ts │ ├── springs.ts │ ├── style.ts │ └── utils.ts ├── tsconfig.build.json ├── tsconfig.json ├── vite.config.build.ts ├── vite.config.demo.ts └── vitest.config.ts ├── package.json ├── vercel.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .prettierrc.ts 4 | .eslintrc.json 5 | env.d.ts 6 | vite.config.ts -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | // By extending from a plugin config, we can get recommended rules without having to add them manually. 4 | "eslint:recommended", 5 | "plugin:import/recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | // This disables the formatting rules in ESLint that Prettier is going to be responsible for handling. 8 | // Make sure it's always the last config, so it gets the chance to override other configs. 9 | "eslint-config-prettier" 10 | ], 11 | 12 | "settings": { 13 | // Tells eslint how to resolve imports 14 | "import/resolver": { 15 | "node": { 16 | "paths": ["src"], 17 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 18 | } 19 | } 20 | }, 21 | "parserOptions": { 22 | "sourceType": "module" 23 | }, 24 | "rules": { 25 | "no-console" : 2 26 | } 27 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull request template 2 | 3 | ***REMOVE THIS - START*** 4 | 5 | 1. Title should be something descriptive about what you're changes do. (It will default to whatever you put as your commit message.) 6 | 2. Make sure to point to `dev` branch; 7 | 3. Mark your pull request as `DRAFT` until it will be ready to review; 8 | 4. Before marking a PR ready to review, make sure that: 9 | 10 | a. You have done your changes in a separate branch. Branches MUST have descriptive names that start with either the `fix/` or `feature/` prefixes. Good examples are: `fix/signin-issue` or `feature/issue-templates`. 11 | 12 | b. `npm test` doesn't throw any error. 13 | 14 | 5. Describe your changes, link to the issue and add images if relevant under each #TODO next comments; 15 | 6. MAKE SURE TO CLEAN ALL THIS 6 POINTS BEFORE SUBMITTING 16 | 17 | ***REMOVE THIS - END*** 18 | 19 | ## Describe your changes 20 | TODO: Add a short summary of your changes and impact: 21 | 22 | ## Link to issue this resolves 23 | TODO: Add your issue no. or link to the issue. Example: 24 | Fixes: #100 25 | 26 | ## Screenshot of changes(if relevant) 27 | TODO: Add images: 28 | 29 | 🙏🙏 !! THANK YOU !! 🚀🚀 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .yarn/* 27 | 28 | .next/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "printWidth": 120, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Adam Pietrasiak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mobxmotion 2 | 3 | `mobxmotion` allows you to animate elements using `mobx` without re-rendering the parent component. 4 | 5 | `mobxmotion.div` is a drop-in replacement for a normal `div`, except its `style` prop can include getters. 6 | 7 | If a getter is present, the given style value will be re-evaluated and updated when mobx detects a change, but the parent component will not re-render. 8 | 9 | ```bash 10 | yarn add mobxmotion 11 | # or 12 | npm install mobxmotion 13 | # or 14 | pnpm add mobxmotion 15 | ``` 16 | 17 | ## Example 18 | 19 | Let's say we have some observable `x` value and we want to use it to set the `transform` style of a `div`. 20 | 21 | ```tsx 22 | import { mobxmotion } from "mobxmotion"; 23 | import { observable } from "mobx"; 24 | 25 | const x = observable.box(0); 26 | 27 | export function Demo() { 28 | useSomeHeavyComputation(); // Render is expensive 29 | 30 | return ( 31 | 40 | 47 | 48 | ); 49 | } 50 | ``` 51 | 52 | When observable `x` changes, `style.transform` will be re-evaluated and updated, but the component itself will not re-render. 53 | 54 | ## Example with spring animation 55 | 56 | In the example above, when `x` changes, the component will instantly move to the new position. 57 | 58 | We can easily use spring animation to make the movement smooth. 59 | 60 | The only change we need to make is to pass `x` value through `$spring` function inside the getter. 61 | 62 | Change this: 63 | 64 | ```tsx 65 | style={{ 66 | get transform() { 67 | const transformX = x.get(); 68 | 69 | return `translate(${transformX}px, 0)`; 70 | }, 71 | }} 72 | ``` 73 | 74 | To this: 75 | 76 | ```tsx 77 | style={{ 78 | get transform() { 79 | const transformX = $spring(x.get()); 80 | 81 | return `translate(${transformX}px, 0)`; 82 | }, 83 | }} 84 | ``` 85 | 86 | And that's it! Now when `x` changes, transform will be animated to the new value. Component will not re-render during the animation. 87 | 88 | The full example with spring animation: 89 | 90 | ```tsx 91 | import { mobxmotion, $spring } from "mobxmotion"; 92 | import { observable } from "mobx"; 93 | 94 | const x = observable.box(0); 95 | 96 | export function Demo() { 97 | return ( 98 | 107 | 114 | 115 | ); 116 | } 117 | ``` 118 | 119 | `$spring` also accepts 2nd argument, `springConfig` which allows you to customize the spring animation. 120 | 121 | ```tsx 122 | const SPRING_CONFIG: SpringConfigInput = { 123 | stiffness: 100, 124 | damping: 10, 125 | mass: 1, 126 | }; 127 | 128 | // Inside the getter: 129 | const transformX = $spring(x.get(), SPRING_CONFIG); 130 | ``` 131 | 132 | It is recommended to define spring config outside of the getter to make it faster for the animator to determine if the config needs to be updated. 133 | 134 | > [!NOTE] 135 | > 136 | > `$spring` calls inside getters have to follow same rules as React hooks. 137 | > 138 | > They cannot be called conditionally, they have to be called on every getter call. You can use multiple `$spring` calls in one getter, but you have to call it the same number of times in every getter call. 139 | 140 | --- 141 | 142 | All the other props are exactly the same as in normal `div`. 143 | 144 | You can still use regular styles, without getters, as you would do with a normal `div`. 145 | 146 | Every existing `div` can be replaced with `mobxmotion.div` without any changes to the code. 147 | 148 | You can also use `mobxmotion.p`, `mobxmotion.span`, `mobxmotion.button`, etc. for every `HTML` and `SVG` element. 149 | 150 | ## License 151 | 152 | MIT License 153 | 154 | Copyright (c) 2024 Adam Pietrasiak 155 | 156 | Permission is hereby granted, free of charge, to any person obtaining a copy 157 | of this software and associated documentation files (the "Software"), to deal 158 | in the Software without restriction, including without limitation the rights 159 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 160 | copies of the Software, and to permit persons to whom the Software is 161 | furnished to do so, subject to the following conditions: 162 | 163 | The above copyright notice and this permission notice shall be included in all 164 | copies or substantial portions of the Software. 165 | 166 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 167 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 168 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 169 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 170 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 171 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 172 | SOFTWARE. 173 | -------------------------------------------------------------------------------- /docs/mdx-components.js: -------------------------------------------------------------------------------- 1 | import { useMDXComponents as getDocsMDXComponents } from 'nextra-theme-docs' 2 | 3 | const docsComponents = getDocsMDXComponents() 4 | 5 | export const useMDXComponents = components => ({ 6 | ...docsComponents, 7 | ...components 8 | }) 9 | -------------------------------------------------------------------------------- /docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextra from "nextra"; 2 | 3 | const withNextra = nextra({ 4 | latex: true, 5 | search: { 6 | codeblocks: false, 7 | }, 8 | contentDirBasePath: "/docs", 9 | }); 10 | 11 | export default withNextra({ 12 | reactStrictMode: true, 13 | typescript: { 14 | ignoreBuildErrors: true, 15 | }, 16 | eslint: { 17 | ignoreDuringBuilds: true, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobxmotion-docs", 3 | "license": "MIT", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next --turbopack", 8 | "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "next": "^15.0.2", 13 | "nextra": "^4.2.17", 14 | "nextra-theme-docs": "^4.2.17", 15 | "react": "18.3.1", 16 | "react-dom": "18.3.1" 17 | }, 18 | "devDependencies": { 19 | "pagefind": "^1.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/app/GlobalStylings.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export function GlobalStylings() { 6 | useEffect(() => { 7 | // Reflect.set(window, "mobxmotion", mobxmotion); 8 | }); 9 | return null; 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/app/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | display: "hidden", 4 | }, 5 | docs: { 6 | type: "page", 7 | title: "Documentation", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /docs/src/app/docs/[[...mdxPath]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { generateStaticParamsFor, importPage } from 'nextra/pages' 2 | import { useMDXComponents as getMDXComponents } from '../../../../mdx-components' 3 | 4 | export const generateStaticParams = generateStaticParamsFor('mdxPath') 5 | 6 | export async function generateMetadata(props) { 7 | const params = await props.params 8 | const { metadata } = await importPage(params.mdxPath) 9 | return metadata 10 | } 11 | 12 | const Wrapper = getMDXComponents().wrapper 13 | 14 | export default async function Page(props) { 15 | const params = await props.params 16 | const result = await importPage(params.mdxPath) 17 | const { default: MDXContent, toc, metadata } = result 18 | return ( 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "nextra-theme-docs/style.css"; 2 | 3 | import { Banner, Head } from "nextra/components"; 4 | /* eslint-env node */ 5 | import { Footer, Layout, Navbar } from "nextra-theme-docs"; 6 | 7 | import { GlobalStylings } from "./GlobalStylings"; 8 | import { getPageMap } from "nextra/page-map"; 9 | 10 | export const metadata = { 11 | metadataBase: new URL("https://mobxmotion.com"), 12 | title: { 13 | template: "%s - mobxmotion", 14 | }, 15 | description: "mobxmotion: joyful styling for React and Styled Components", 16 | applicationName: "mobxmotion", 17 | generator: "Next.js", 18 | appleWebApp: { 19 | title: "mobxmotion", 20 | }, 21 | // other: { 22 | // "msapplication-TileImage": "/ms-icon-144x144.png", 23 | // "msapplication-TileColor": "#fff", 24 | // }, 25 | twitter: { 26 | site: "https://mobxmotion.com", 27 | card: "summary_large_image", 28 | }, 29 | openGraph: { 30 | type: "website", 31 | url: "https://mobxmotion.com", 32 | title: "mobxmotion", 33 | description: "mobxmotion: joyful styling for React and Styled Components", 34 | images: [ 35 | { 36 | url: "https://mobxmotion.com/opengraph-image.png", 37 | width: 1200, 38 | height: 630, 39 | alt: "mobxmotion", 40 | }, 41 | ], 42 | }, 43 | }; 44 | 45 | export default async function RootLayout({ children }) { 46 | const navbar = ( 47 | 50 | mobxmotion 51 | 52 | } 53 | projectLink="https://github.com/pie6k/mobxmotion" 54 | /> 55 | ); 56 | const pageMap = await getPageMap(); 57 | return ( 58 | 59 | 60 | 61 | Nextra 2 Alpha} 63 | navbar={navbar} 64 | footer={
MIT {new Date().getFullYear()} © mobxmotion.
} 65 | editLink="Edit this page on GitHub" 66 | docsRepositoryBase="https://github.com/pie6k/mobxmotion/blob/main/docs" 67 | sidebar={{ defaultMenuCollapseLevel: 1 }} 68 | pageMap={pageMap} 69 | > 70 | 71 | {children} 72 |
73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /docs/src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pie6k/mobxmotion/de1f51b9790cb1a09c814c58388e405a0a9cfa89/docs/src/app/opengraph-image.png -------------------------------------------------------------------------------- /docs/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default async function IndexPage() { 4 | redirect("/docs"); 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/content/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: "", 3 | }; 4 | -------------------------------------------------------------------------------- /docs/src/content/index.mdx: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | `mobxmotion` allows you to animate elements using `mobx` without re-rendering the parent component. 4 | 5 | `mobxmotion.div` is a drop-in replacement for a normal `div`, except its `style` prop can include getters. 6 | 7 | If a getter is present, the given style value will be re-evaluated and updated when mobx detects a change, but the parent component will not re-render. 8 | 9 | ```bash 10 | yarn add mobxmotion 11 | # or 12 | npm install mobxmotion 13 | # or 14 | pnpm add mobxmotion 15 | ``` 16 | 17 | ## Example 18 | 19 | Let's say we have some observable `x` value and we want to use it to set the `transform` style of a `div`. 20 | 21 | ```tsx 22 | import { mobxmotion } from "mobxmotion"; 23 | import { observable } from "mobx"; 24 | 25 | const x = observable.box(0); 26 | 27 | export function Demo() { 28 | useSomeHeavyComputation(); // Render is expensive 29 | 30 | return ( 31 | 40 | 47 | 48 | ); 49 | } 50 | ``` 51 | 52 | When observable `x` changes, `style.transform` will be re-evaluated and updated, but the component itself will not re-render. 53 | 54 | ## Example with spring animation 55 | 56 | In the example above, when `x` changes, the component will instantly move to the new position. 57 | 58 | We can easily use spring animation to make the movement smooth. 59 | 60 | The only change we need to make is to pass `x` value through `$spring` function inside the getter. 61 | 62 | Change this: 63 | 64 | ```tsx 65 | style={{ 66 | get transform() { 67 | const transformX = x.get(); 68 | 69 | return `translate(${transformX}px, 0)`; 70 | }, 71 | }} 72 | ``` 73 | 74 | To this: 75 | 76 | ```tsx 77 | style={{ 78 | get transform() { 79 | const transformX = $spring(x.get()); 80 | 81 | return `translate(${transformX}px, 0)`; 82 | }, 83 | }} 84 | ``` 85 | 86 | And that's it! Now when `x` changes, transform will be animated to the new value. Component will not re-render during the animation. 87 | 88 | The full example with spring animation: 89 | 90 | ```tsx 91 | import { mobxmotion, $spring } from "mobxmotion"; 92 | import { observable } from "mobx"; 93 | 94 | const x = observable.box(0); 95 | 96 | export function Demo() { 97 | return ( 98 | 107 | 114 | 115 | ); 116 | } 117 | ``` 118 | 119 | `$spring` also accepts 2nd argument, `springConfig` which allows you to customize the spring animation. 120 | 121 | ```tsx 122 | const SPRING_CONFIG: SpringConfigInput = { 123 | stiffness: 100, 124 | damping: 10, 125 | mass: 1, 126 | }; 127 | 128 | // Inside the getter: 129 | const transformX = $spring(x.get(), SPRING_CONFIG); 130 | ``` 131 | 132 | It is recommended to define spring config outside of the getter to make it faster for the animator to determine if the config needs to be updated. 133 | 134 | > [!NOTE] 135 | > 136 | > `$spring` calls inside getters have to follow same rules as React hooks. 137 | > 138 | > They cannot be called conditionally, they have to be called on every getter call. You can use multiple `$spring` calls in one getter, but you have to call it the same number of times in every getter call. 139 | 140 | --- 141 | 142 | All the other props are exactly the same as in normal `div`. 143 | 144 | You can still use regular styles, without getters, as you would do with a normal `div`. 145 | 146 | Every existing `div` can be replaced with `mobxmotion.div` without any changes to the code. 147 | 148 | You can also use `mobxmotion.p`, `mobxmotion.span`, `mobxmotion.button`, etc. for every `HTML` and `SVG` element. 149 | 150 | ## License 151 | 152 | MIT License 153 | 154 | Copyright (c) 2024 Adam Pietrasiak 155 | 156 | Permission is hereby granted, free of charge, to any person obtaining a copy 157 | of this software and associated documentation files (the "Software"), to deal 158 | in the Software without restriction, including without limitation the rights 159 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 160 | copies of the Software, and to permit persons to whom the Software is 161 | furnished to do so, subject to the following conditions: 162 | 163 | The above copyright notice and this permission notice shall be included in all 164 | copies or substantial portions of the Software. 165 | 166 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 167 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 168 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 169 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 170 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 171 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 172 | SOFTWARE. 173 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "noEmit": true, 13 | "incremental": true, 14 | "module": "esnext", 15 | "esModuleInterop": true, 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ] 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | ".next/types/**/*.ts", 29 | "**/*.ts", 30 | "**/*.tsx" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /mobxmotion/.npmignore: -------------------------------------------------------------------------------- 1 | # Source files 2 | src/ 3 | *.ts 4 | *.tsx 5 | 6 | # Development configs 7 | .vscode/ 8 | .idea/ 9 | *.config.ts 10 | .eslintrc* 11 | .prettierrc* 12 | .editorconfig 13 | .gitignore 14 | .npmrc 15 | .yarnrc* 16 | .yarn/ 17 | 18 | # Test files 19 | __tests__/ 20 | *.test.ts 21 | *.test.tsx 22 | coverage/ 23 | .nyc_output/ 24 | 25 | # Development scripts 26 | scripts/ 27 | demo/ 28 | examples/ 29 | 30 | # Build tools 31 | vite.config.* 32 | tsconfig.json 33 | jest.config.* 34 | vitest.config.* 35 | 36 | # Documentation 37 | docs/ 38 | *.md 39 | !README.md 40 | !LICENSE 41 | 42 | # Misc 43 | .DS_Store 44 | *.log 45 | .env* 46 | .cache/ 47 | .temp/ 48 | .tmp/ -------------------------------------------------------------------------------- /mobxmotion/README.md: -------------------------------------------------------------------------------- 1 | # mobxmotion 2 | 3 | `mobxmotion` allows you to animate elements using `mobx` without re-rendering the parent component. 4 | 5 | `mobxmotion.div` is a drop-in replacement for a normal `div`, except its `style` prop can include getters. 6 | 7 | If a getter is present, the given style value will be re-evaluated and updated when mobx detects a change, but the parent component will not re-render. 8 | 9 | ```bash 10 | yarn add mobxmotion 11 | # or 12 | npm install mobxmotion 13 | # or 14 | pnpm add mobxmotion 15 | ``` 16 | 17 | ## Example 18 | 19 | Let's say we have some observable `x` value and we want to use it to set the `transform` style of a `div`. 20 | 21 | ```tsx 22 | import { mobxmotion } from "mobxmotion"; 23 | import { observable } from "mobx"; 24 | 25 | const x = observable.box(0); 26 | 27 | export function Demo() { 28 | useSomeHeavyComputation(); // Render is expensive 29 | 30 | return ( 31 | 40 | 47 | 48 | ); 49 | } 50 | ``` 51 | 52 | When observable `x` changes, `style.transform` will be re-evaluated and updated, but the component itself will not re-render. 53 | 54 | ## Example with spring animation 55 | 56 | In the example above, when `x` changes, the component will instantly move to the new position. 57 | 58 | We can easily use spring animation to make the movement smooth. 59 | 60 | The only change we need to make is to pass `x` value through `$spring` function inside the getter. 61 | 62 | Change this: 63 | 64 | ```tsx 65 | style={{ 66 | get transform() { 67 | const transformX = x.get(); 68 | 69 | return `translate(${transformX}px, 0)`; 70 | }, 71 | }} 72 | ``` 73 | 74 | To this: 75 | 76 | ```tsx 77 | style={{ 78 | get transform() { 79 | const transformX = $spring(x.get()); 80 | 81 | return `translate(${transformX}px, 0)`; 82 | }, 83 | }} 84 | ``` 85 | 86 | And that's it! Now when `x` changes, transform will be animated to the new value. Component will not re-render during the animation. 87 | 88 | The full example with spring animation: 89 | 90 | ```tsx 91 | import { mobxmotion, $spring } from "mobxmotion"; 92 | import { observable } from "mobx"; 93 | 94 | const x = observable.box(0); 95 | 96 | export function Demo() { 97 | return ( 98 | 107 | 114 | 115 | ); 116 | } 117 | ``` 118 | 119 | > [!NOTE] 120 | > 121 | > `$spring` calls inside getters have to follow same rules as React hooks. 122 | > 123 | > They cannot be called conditionally, they have to be called on every getter call. You can use multiple `$spring` calls in one getter, but you have to call it the same number of times in every getter call. 124 | 125 | --- 126 | 127 | All the other props are exactly the same as in normal `div`. 128 | 129 | You can still use regular styles, without getters, as you would do with a normal `div`. 130 | 131 | Every existing `div` can be replaced with `mobxmotion.div` without any changes to the code. 132 | 133 | You can also use `mobxmotion.p`, `mobxmotion.span`, `mobxmotion.button`, etc. for every `HTML` and `SVG` element. 134 | 135 | ## License 136 | 137 | MIT License 138 | 139 | Copyright (c) 2024 Adam Pietrasiak 140 | 141 | Permission is hereby granted, free of charge, to any person obtaining a copy 142 | of this software and associated documentation files (the "Software"), to deal 143 | in the Software without restriction, including without limitation the rights 144 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 145 | copies of the Software, and to permit persons to whom the Software is 146 | furnished to do so, subject to the following conditions: 147 | 148 | The above copyright notice and this permission notice shall be included in all 149 | copies or substantial portions of the Software. 150 | 151 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 152 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 153 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 154 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 155 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 156 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 157 | SOFTWARE. 158 | -------------------------------------------------------------------------------- /mobxmotion/demo/Demo.tsx: -------------------------------------------------------------------------------- 1 | import { $spring, mobxmotion } from "@/index"; 2 | import React, { useEffect, useRef } from "react"; 3 | import { configure, observable, runInAction } from "mobx"; 4 | 5 | configure({ 6 | enforceActions: "always", 7 | }); 8 | 9 | const value = observable.box(0); 10 | 11 | function randomInt(min: number, max: number) { 12 | return Math.floor(Math.random() * (max - min + 1)) + min; 13 | } 14 | 15 | function useRendersCount() { 16 | const renders = useRef(1); 17 | 18 | useEffect(() => { 19 | renders.current++; 20 | }, []); 21 | 22 | return renders.current; 23 | } 24 | 25 | export function Demo() { 26 | const rendersCount = useRendersCount(); 27 | 28 | return ( 29 | 37 | 46 |
Renders: {rendersCount}
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /mobxmotion/demo/app.tsx: -------------------------------------------------------------------------------- 1 | import { Demo } from "./Demo"; 2 | import React from "react"; 3 | import { createRoot } from "react-dom/client"; 4 | 5 | const rootElement = document.getElementById("root"); 6 | 7 | if (rootElement) { 8 | const root = createRoot(rootElement); 9 | root.render(); 10 | } 11 | -------------------------------------------------------------------------------- /mobxmotion/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stylings Demo 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mobxmotion/demo/index.tsx: -------------------------------------------------------------------------------- 1 | import "./app"; 2 | -------------------------------------------------------------------------------- /mobxmotion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobxmotion", 3 | "homepage": "https://mobxmotion.com", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/pie6k/mobxmotion.git" 8 | }, 9 | "version": "1.0.4", 10 | "type": "module", 11 | "author": { 12 | "name": "Adam Pietrasiak" 13 | }, 14 | "description": "Performant, zero-re-render animations for React using MobX.", 15 | "keywords": [ 16 | "react", 17 | "mobx", 18 | "animation", 19 | "spring", 20 | "motion", 21 | "animations" 22 | ], 23 | "main": "./dist/index.cjs.js", 24 | "module": "./dist/index.es.js", 25 | "types": "./dist/index.d.ts", 26 | "exports": { 27 | ".": { 28 | "import": "./dist/index.es.js", 29 | "require": "./dist/index.cjs.js" 30 | } 31 | }, 32 | "scripts": { 33 | "dev": "vite --config vite.config.demo.ts", 34 | "build": "vite --config vite.config.build.ts build", 35 | "preview": "vite preview", 36 | "prettier": "prettier --write \"src/**/*.{ts,tsx}\"", 37 | "test": "vitest" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^22.13.11", 41 | "@types/react": "*", 42 | "@types/react-dom": "*", 43 | "mobx": "*", 44 | "prettier": "^3.0.3", 45 | "react": "*", 46 | "react-dom": "*", 47 | "typescript": "*", 48 | "vite": "^4.4.9", 49 | "vite-plugin-dts": "^4.5.3", 50 | "vitest": "^3.0.9" 51 | }, 52 | "files": [ 53 | "dist" 54 | ], 55 | "peerDependencies": { 56 | "mobx": "*", 57 | "react": "*" 58 | }, 59 | "packageManager": "yarn@4.4.1" 60 | } 61 | -------------------------------------------------------------------------------- /mobxmotion/src/AnimatedSpring.ts: -------------------------------------------------------------------------------- 1 | import { Spring } from "./Spring"; 2 | import { SpringConfigInput } from "./springConfig"; 3 | import { raf } from "./utils"; 4 | 5 | export class AnimatedSpring extends Spring { 6 | constructor( 7 | initialValue: number, 8 | config?: SpringConfigInput, 9 | private targetWindow?: Window | null, 10 | ) { 11 | super(initialValue, config); 12 | } 13 | 14 | setTargetValue(value: number) { 15 | if (this.targetValue === value) return; 16 | 17 | const wasAtRest = this.isAtRest; 18 | 19 | super.setTargetValue(value); 20 | 21 | if (wasAtRest && !this.isAtRest && !this.isAnimating) { 22 | this.animateWhileNotAtRest(); 23 | } 24 | } 25 | 26 | private isAnimating = false; 27 | 28 | private async animateWhileNotAtRest() { 29 | const targetWindow = this.targetWindow ?? window; 30 | if (this.isAnimating) { 31 | console.warn("Spring is already animating"); 32 | return; 33 | } 34 | 35 | this.isAnimating = true; 36 | 37 | let lastFrameTime = await raf(targetWindow); 38 | 39 | while (!this.isAtRest) { 40 | if (!this.isAnimating) break; 41 | 42 | const time = await raf(targetWindow); 43 | 44 | if (!this.isAnimating) break; 45 | 46 | const deltaTime = time - lastFrameTime; 47 | 48 | lastFrameTime = time; 49 | 50 | super.advanceTimeBy(deltaTime); 51 | } 52 | 53 | this.isAnimating = false; 54 | } 55 | 56 | stop() { 57 | this.isAnimating = false; 58 | super.stop(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mobxmotion/src/MobxMotion.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, HTMLAttributes, RefObject, forwardRef, useEffect, useState } from "react"; 2 | import { StyleWithTransforms, applyStyleAndVariables, computeStyleAndVariables, getChangedProperties } from "./style"; 3 | import { reaction, untracked } from "mobx"; 4 | import { useInnerForwardRef, useIsomorphicLayoutEffect } from "./hooks"; 5 | 6 | import { SpringsManager } from "./springs"; 7 | import { shallowEqual } from "./utils"; 8 | 9 | type HTMLOrSVGElement = HTMLElement | SVGElement; 10 | 11 | export interface MobxMotionProps 12 | extends Omit, "style"> { 13 | ref?: RefObject; 14 | as?: keyof HTMLElementTagNameMap; 15 | style?: StyleWithTransforms; 16 | } 17 | 18 | export type MobxMotionComponent = FunctionComponent>; 19 | 20 | export function createMobxMotionComponent( 21 | componentName: keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap, 22 | ): MobxMotionComponent { 23 | const MobxMotion = forwardRef>(function MobxMotion( 24 | { style, as = componentName, ...props }, 25 | forwardedRef, 26 | ) { 27 | const ref = useInnerForwardRef(forwardedRef); 28 | const [springsManager] = useState(() => new SpringsManager(ref)); 29 | 30 | useIsomorphicLayoutEffect(() => { 31 | if (!style) { 32 | springsManager.clear(); 33 | return; 34 | } 35 | 36 | springsManager.clearUnused(Object.keys(style)); 37 | 38 | const element = ref.current; 39 | 40 | if (!element) return; 41 | 42 | return reaction( 43 | () => { 44 | return computeStyleAndVariables(style, springsManager); 45 | }, 46 | (stylesAndVariables, previousStylesAndVariables) => { 47 | const changedProperties = getChangedProperties(stylesAndVariables, previousStylesAndVariables); 48 | 49 | applyStyleAndVariables(element, changedProperties); 50 | }, 51 | { equals: shallowEqual }, 52 | ); 53 | }, [style]); 54 | 55 | useEffect(() => { 56 | return () => { 57 | // not unmounting 58 | if (ref.current) return; 59 | 60 | springsManager.clear(); 61 | }; 62 | }, []); 63 | 64 | const Element = as as "div"; 65 | 66 | return ( 67 | } 69 | {...(props as MobxMotionProps)} 70 | style={untracked(() => { 71 | if (!style) return undefined; 72 | 73 | return computeStyleAndVariables(style, springsManager); 74 | })} 75 | /> 76 | ); 77 | }); 78 | 79 | MobxMotion.displayName = `MobxMotion__${componentName}`; 80 | 81 | return MobxMotion; 82 | } 83 | -------------------------------------------------------------------------------- /mobxmotion/src/Spring.ts: -------------------------------------------------------------------------------- 1 | import { SpringConfig, SpringConfigInput, resolveSpringConfigInput, validateSpringConfig } from "./springConfig"; 2 | 3 | import { createAtom } from "mobx"; 4 | import { stepSpring } from "./springStep"; 5 | 6 | function getIsValidNumber(input: number) { 7 | if (typeof input !== "number") return false; 8 | 9 | if (!isFinite(input)) return false; 10 | 11 | if (isNaN(input)) return false; 12 | 13 | return true; 14 | } 15 | 16 | export function calculatePrecission(from: number, to: number, precisionBase: number) { 17 | if (from === to) { 18 | return 1; 19 | } 20 | 21 | const diff = Math.max(1, Math.abs(from - to)); 22 | 23 | const precisionDividre = 1 / precisionBase; 24 | 25 | return Math.max(diff, 1) / precisionDividre; 26 | } 27 | 28 | /** 29 | * Physics based spring, that is not based on browser timers, but instead requires manually advancing time. 30 | * 31 | * This allows generating animation in-advance, while being able to modify target values at any point, which is not possible with one-shoot "generateSpringFrames" libs 32 | * 33 | * Note: this will be called extremely often and needs to be well optimized 34 | */ 35 | export class Spring { 36 | public config: SpringConfig; 37 | 38 | public time: number = 0; // Current time along the spring curve in ms (zero-based) 39 | 40 | private valueAtom = createAtom("NumberSpring"); 41 | 42 | private _value: number; // the current value of the spring 43 | 44 | get value() { 45 | this.valueAtom.reportObserved(); 46 | return this._value; 47 | } 48 | 49 | set value(value: number) { 50 | if (this._value === value) return; 51 | 52 | this._value = value; 53 | this.valueAtom.reportChanged(); 54 | } 55 | 56 | public currentVelocity: number = 0; // the current velocity of the spring 57 | 58 | public targetValue: number; 59 | 60 | /** 61 | * Precission is automatically set basing on offset between from<>to 62 | */ 63 | private precission: number = 1; 64 | 65 | constructor(initialValue: number, input: SpringConfigInput = {}) { 66 | this._value = initialValue; 67 | this.targetValue = initialValue; 68 | this.config = resolveSpringConfigInput(input); 69 | 70 | validateSpringConfig(this.config); 71 | } 72 | 73 | /** 74 | * If the spring has reached its `toValue`, or if its velocity is below the 75 | * `restVelocityThreshold`, it is considered at rest. If `stop()` is called 76 | * during a simulation, both `isAnimating` and `isAtRest` will be false. 77 | */ 78 | get isAtRest(): boolean { 79 | return this.targetValue === this.value && this.currentVelocity === 0; 80 | } 81 | 82 | snapToTarget(target = this.targetValue) { 83 | this.value = target; 84 | this.targetValue = target; 85 | this.currentVelocity = 0; 86 | } 87 | 88 | setTargetValue(targetValue: number) { 89 | if (this.targetValue === targetValue) return; 90 | 91 | if (!getIsValidNumber(targetValue)) { 92 | throw new Error("Invalid target value"); 93 | } 94 | 95 | this.precission = calculatePrecission(this.value, targetValue, this.config.precision); 96 | 97 | this.targetValue = targetValue; 98 | } 99 | 100 | private lastConfigUpdate?: SpringConfigInput; 101 | 102 | /** 103 | * Updates the spring config with the given values. Values not explicitly 104 | * supplied will be reused from the existing config. 105 | */ 106 | updateConfig(updatedConfig: Partial) { 107 | if (this.lastConfigUpdate === updatedConfig) return; 108 | 109 | const newConfig: SpringConfig = { 110 | ...this.config, 111 | ...updatedConfig, 112 | }; 113 | 114 | validateSpringConfig(newConfig); 115 | 116 | this.config = newConfig; 117 | this.lastConfigUpdate = updatedConfig; 118 | } 119 | 120 | advanceTimeTo(time: number) { 121 | const dt = time - this.time; 122 | 123 | this.advanceTimeBy(dt); 124 | } 125 | 126 | advanceTimeBy(dt: number) { 127 | if (dt === 0) return; 128 | if (dt < 0) throw new Error("Can't go back in time"); 129 | 130 | this.time += dt; 131 | 132 | if (this.isAtRest) { 133 | return; 134 | } 135 | 136 | // Hello, Einstein! 137 | if (this.config.mass === 0) { 138 | this.snapToTarget(); 139 | return; 140 | } 141 | 142 | [this.value, this.currentVelocity] = stepSpring( 143 | dt, 144 | this._value, 145 | this.currentVelocity, 146 | this.targetValue, 147 | this.config.stiffness, 148 | this.config.damping, 149 | this.config.mass, 150 | this.config.clamp, 151 | this.precission, 152 | ); 153 | } 154 | 155 | stop() { 156 | this.setTargetValue(this._value); 157 | this.snapToTarget(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /mobxmotion/src/components.tsx: -------------------------------------------------------------------------------- 1 | import { createMobxMotionComponent } from "./MobxMotion"; 2 | 3 | export const mobxmotion = { 4 | a: createMobxMotionComponent("a"), 5 | abbr: createMobxMotionComponent("abbr"), 6 | address: createMobxMotionComponent("address"), 7 | area: createMobxMotionComponent("area"), 8 | article: createMobxMotionComponent("article"), 9 | aside: createMobxMotionComponent("aside"), 10 | audio: createMobxMotionComponent("audio"), 11 | b: createMobxMotionComponent("b"), 12 | base: createMobxMotionComponent("base"), 13 | bdi: createMobxMotionComponent("bdi"), 14 | bdo: createMobxMotionComponent("bdo"), 15 | blockquote: createMobxMotionComponent("blockquote"), 16 | body: createMobxMotionComponent("body"), 17 | br: createMobxMotionComponent("br"), 18 | button: createMobxMotionComponent("button"), 19 | canvas: createMobxMotionComponent("canvas"), 20 | caption: createMobxMotionComponent("caption"), 21 | cite: createMobxMotionComponent("cite"), 22 | code: createMobxMotionComponent("code"), 23 | col: createMobxMotionComponent("col"), 24 | colgroup: createMobxMotionComponent("colgroup"), 25 | data: createMobxMotionComponent("data"), 26 | datalist: createMobxMotionComponent("datalist"), 27 | dd: createMobxMotionComponent("dd"), 28 | del: createMobxMotionComponent("del"), 29 | details: createMobxMotionComponent("details"), 30 | dfn: createMobxMotionComponent("dfn"), 31 | dialog: createMobxMotionComponent("dialog"), 32 | div: createMobxMotionComponent("div"), 33 | dl: createMobxMotionComponent("dl"), 34 | dt: createMobxMotionComponent("dt"), 35 | em: createMobxMotionComponent("em"), 36 | embed: createMobxMotionComponent("embed"), 37 | fieldset: createMobxMotionComponent("fieldset"), 38 | figcaption: createMobxMotionComponent("figcaption"), 39 | figure: createMobxMotionComponent("figure"), 40 | footer: createMobxMotionComponent("footer"), 41 | form: createMobxMotionComponent("form"), 42 | h1: createMobxMotionComponent("h1"), 43 | h2: createMobxMotionComponent("h2"), 44 | h3: createMobxMotionComponent("h3"), 45 | h4: createMobxMotionComponent("h4"), 46 | h5: createMobxMotionComponent("h5"), 47 | h6: createMobxMotionComponent("h6"), 48 | head: createMobxMotionComponent("head"), 49 | header: createMobxMotionComponent("header"), 50 | hr: createMobxMotionComponent("hr"), 51 | html: createMobxMotionComponent("html"), 52 | i: createMobxMotionComponent("i"), 53 | iframe: createMobxMotionComponent("iframe"), 54 | img: createMobxMotionComponent("img"), 55 | input: createMobxMotionComponent("input"), 56 | ins: createMobxMotionComponent("ins"), 57 | kbd: createMobxMotionComponent("kbd"), 58 | label: createMobxMotionComponent("label"), 59 | legend: createMobxMotionComponent("legend"), 60 | li: createMobxMotionComponent("li"), 61 | link: createMobxMotionComponent("link"), 62 | main: createMobxMotionComponent("main"), 63 | map: createMobxMotionComponent("map"), 64 | mark: createMobxMotionComponent("mark"), 65 | menu: createMobxMotionComponent("menu"), 66 | meta: createMobxMotionComponent("meta"), 67 | meter: createMobxMotionComponent("meter"), 68 | nav: createMobxMotionComponent("nav"), 69 | noscript: createMobxMotionComponent("noscript"), 70 | object: createMobxMotionComponent("object"), 71 | ol: createMobxMotionComponent("ol"), 72 | optgroup: createMobxMotionComponent("optgroup"), 73 | option: createMobxMotionComponent("option"), 74 | output: createMobxMotionComponent("output"), 75 | p: createMobxMotionComponent("p"), 76 | picture: createMobxMotionComponent("picture"), 77 | pre: createMobxMotionComponent("pre"), 78 | progress: createMobxMotionComponent("progress"), 79 | q: createMobxMotionComponent("q"), 80 | rp: createMobxMotionComponent("rp"), 81 | rt: createMobxMotionComponent("rt"), 82 | ruby: createMobxMotionComponent("ruby"), 83 | s: createMobxMotionComponent("s"), 84 | samp: createMobxMotionComponent("samp"), 85 | script: createMobxMotionComponent("script"), 86 | section: createMobxMotionComponent("section"), 87 | select: createMobxMotionComponent("select"), 88 | small: createMobxMotionComponent("small"), 89 | source: createMobxMotionComponent("source"), 90 | span: createMobxMotionComponent("span"), 91 | strong: createMobxMotionComponent("strong"), 92 | style: createMobxMotionComponent("style"), 93 | sub: createMobxMotionComponent("sub"), 94 | summary: createMobxMotionComponent("summary"), 95 | sup: createMobxMotionComponent("sup"), 96 | table: createMobxMotionComponent("table"), 97 | tbody: createMobxMotionComponent("tbody"), 98 | td: createMobxMotionComponent("td"), 99 | template: createMobxMotionComponent("template"), 100 | textarea: createMobxMotionComponent("textarea"), 101 | tfoot: createMobxMotionComponent("tfoot"), 102 | th: createMobxMotionComponent("th"), 103 | thead: createMobxMotionComponent("thead"), 104 | time: createMobxMotionComponent("time"), 105 | title: createMobxMotionComponent("title"), 106 | tr: createMobxMotionComponent("tr"), 107 | track: createMobxMotionComponent("track"), 108 | u: createMobxMotionComponent("u"), 109 | ul: createMobxMotionComponent("ul"), 110 | var: createMobxMotionComponent("var"), 111 | video: createMobxMotionComponent("video"), 112 | wbr: createMobxMotionComponent("wbr"), 113 | // SVG elements 114 | circle: createMobxMotionComponent("circle"), 115 | clipPath: createMobxMotionComponent("clipPath"), 116 | defs: createMobxMotionComponent("defs"), 117 | desc: createMobxMotionComponent("desc"), 118 | ellipse: createMobxMotionComponent("ellipse"), 119 | feBlend: createMobxMotionComponent("feBlend"), 120 | feColorMatrix: createMobxMotionComponent("feColorMatrix"), 121 | feComponentTransfer: createMobxMotionComponent("feComponentTransfer"), 122 | feComposite: createMobxMotionComponent("feComposite"), 123 | feConvolveMatrix: createMobxMotionComponent("feConvolveMatrix"), 124 | feDiffuseLighting: createMobxMotionComponent("feDiffuseLighting"), 125 | feDisplacementMap: createMobxMotionComponent("feDisplacementMap"), 126 | feDistantLight: createMobxMotionComponent("feDistantLight"), 127 | feDropShadow: createMobxMotionComponent("feDropShadow"), 128 | feFlood: createMobxMotionComponent("feFlood"), 129 | feFuncA: createMobxMotionComponent("feFuncA"), 130 | feFuncB: createMobxMotionComponent("feFuncB"), 131 | feFuncG: createMobxMotionComponent("feFuncG"), 132 | feFuncR: createMobxMotionComponent("feFuncR"), 133 | feGaussianBlur: createMobxMotionComponent("feGaussianBlur"), 134 | feImage: createMobxMotionComponent("feImage"), 135 | feMerge: createMobxMotionComponent("feMerge"), 136 | feMergeNode: createMobxMotionComponent("feMergeNode"), 137 | feMorphology: createMobxMotionComponent("feMorphology"), 138 | feOffset: createMobxMotionComponent("feOffset"), 139 | fePointLight: createMobxMotionComponent("fePointLight"), 140 | feSpecularLighting: createMobxMotionComponent("feSpecularLighting"), 141 | feSpotLight: createMobxMotionComponent("feSpotLight"), 142 | feTile: createMobxMotionComponent("feTile"), 143 | feTurbulence: createMobxMotionComponent("feTurbulence"), 144 | filter: createMobxMotionComponent("filter"), 145 | foreignObject: createMobxMotionComponent("foreignObject"), 146 | g: createMobxMotionComponent("g"), 147 | image: createMobxMotionComponent("image"), 148 | line: createMobxMotionComponent("line"), 149 | linearGradient: createMobxMotionComponent("linearGradient"), 150 | marker: createMobxMotionComponent("marker"), 151 | mask: createMobxMotionComponent("mask"), 152 | metadata: createMobxMotionComponent("metadata"), 153 | mpath: createMobxMotionComponent("mpath"), 154 | path: createMobxMotionComponent("path"), 155 | pattern: createMobxMotionComponent("pattern"), 156 | polygon: createMobxMotionComponent("polygon"), 157 | polyline: createMobxMotionComponent("polyline"), 158 | radialGradient: createMobxMotionComponent("radialGradient"), 159 | rect: createMobxMotionComponent("rect"), 160 | set: createMobxMotionComponent("set"), 161 | stop: createMobxMotionComponent("stop"), 162 | switch: createMobxMotionComponent("switch"), 163 | symbol: createMobxMotionComponent("symbol"), 164 | text: createMobxMotionComponent("text"), 165 | textPath: createMobxMotionComponent("textPath"), 166 | tspan: createMobxMotionComponent("tspan"), 167 | use: createMobxMotionComponent("use"), 168 | view: createMobxMotionComponent("view"), 169 | animate: createMobxMotionComponent("animate"), 170 | animateMotion: createMobxMotionComponent("animateMotion"), 171 | animateTransform: createMobxMotionComponent("animateTransform"), 172 | // big: createMobxMotionComponent("big"), 173 | // center: createMobxMotionComponent("center"), 174 | hgroup: createMobxMotionComponent("hgroup"), 175 | // keygen: createMobxMotionComponent("keygen"), 176 | // menuitem: createMobxMotionComponent("menuitem"), 177 | // noindex: createMobxMotionComponent("noindex"), 178 | // param: createMobxMotionComponent("param"), 179 | search: createMobxMotionComponent("svg"), 180 | slot: createMobxMotionComponent("slot"), 181 | svg: createMobxMotionComponent("svg"), 182 | // webview: createMobxMotionComponent("webview"), 183 | }; 184 | 185 | export type MobxMotionComponents = typeof mobxmotion; 186 | -------------------------------------------------------------------------------- /mobxmotion/src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useLayoutEffect } from "react"; 2 | import { ForwardedRef, RefObject, useMemo } from "react"; 3 | 4 | export function applyValueToForwardedRef(forwardedRef: ForwardedRef, value: T) { 5 | if (typeof forwardedRef === "function") { 6 | forwardedRef(value); 7 | } else if (forwardedRef != null) { 8 | forwardedRef.current = value; 9 | } 10 | } 11 | 12 | export function useInnerForwardRef(forwardedRef: ForwardedRef) { 13 | const innerRefObject = useMemo>(() => { 14 | let currentValue: T | null = null; 15 | 16 | return { 17 | get current() { 18 | return currentValue; 19 | }, 20 | set current(value) { 21 | currentValue = value; 22 | applyValueToForwardedRef(forwardedRef, value); 23 | }, 24 | }; 25 | }, [forwardedRef]); 26 | 27 | return innerRefObject; 28 | } 29 | 30 | const IS_SERVER = typeof window === "undefined"; 31 | 32 | export function useIsomorphicLayoutEffect(effect: () => void, deps: DependencyList) { 33 | if (IS_SERVER) return; 34 | 35 | useLayoutEffect(() => { 36 | effect(); 37 | }, deps); 38 | } 39 | -------------------------------------------------------------------------------- /mobxmotion/src/index.ts: -------------------------------------------------------------------------------- 1 | export { mobxmotion } from "./components"; 2 | export { type MobxMotionComponent, type MobxMotionProps } from "./MobxMotion"; 3 | export { type SpringConfigInput } from "./springConfig"; 4 | export { $spring } from "./springs"; 5 | -------------------------------------------------------------------------------- /mobxmotion/src/springConfig.ts: -------------------------------------------------------------------------------- 1 | export interface SpringConfig { 2 | /** 3 | * It tells how much "target value" wants to bring current value to itself. 4 | * 5 | * It means the further away the current value is from the target value, the bigger force will be applied if stiffness is big. 6 | * 7 | * F = DISTANCE TO TARGET * STIFFNESS 8 | */ 9 | stiffness: number; 10 | /** 11 | * "Stopping force" that works against current movement (velocity). It's like air-resistance. 12 | * The faster the object moves, the more resistance it will have. The slower it moves, the less resistance it will have. 13 | * F = CURRENT VELOCITY * DAMPING 14 | * 15 | * It can be thought of as "stopping easing" - if it's bigger, fast moving objects will stop faster. 16 | * It will barely affect "getting up to speed" as slowly moving object will not have much 'resistance' - you need mass for that. 17 | */ 18 | damping: number; 19 | /** 20 | * The bigger the mass, the more object wants to keep its "current velocity". 21 | * 22 | * It means it will "get up to speed" more slowly, but will also "slow down" more slowly. 23 | * 24 | * It can be thought of as "starting easing" 25 | */ 26 | mass: number; 27 | 28 | /** 29 | * If true, value will never overshoot the target value. It will stop at the target value 30 | */ 31 | clamp: boolean; 32 | 33 | /** 34 | * If set, value will be rounded to this precision 35 | */ 36 | precision: number; 37 | } 38 | 39 | export type SpringConfigInput = Partial; 40 | 41 | export const DEFAULT_SPRING_CONFIG: SpringConfig = { 42 | stiffness: 300, 43 | damping: 30, 44 | mass: 1, 45 | clamp: false, 46 | precision: 0.002, 47 | }; 48 | 49 | export function resolveSpringConfigInput(input?: SpringConfigInput): SpringConfig { 50 | return { 51 | ...DEFAULT_SPRING_CONFIG, 52 | ...input, 53 | }; 54 | } 55 | 56 | export function validateSpringConfig(input: SpringConfig) { 57 | if (input.mass! < 0) { 58 | throw new Error("Mass value must be greater or equal 0"); 59 | } 60 | 61 | if (input.stiffness! <= 0) { 62 | throw new Error("Stiffness value must be greater than 0"); 63 | } 64 | 65 | if (input.precision! <= 0) { 66 | throw new Error("Precision must be greater than 0"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /mobxmotion/src/springStep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Actual physics emulation happens here. This is heavily optimized for performance. 3 | * 4 | * Note: for better precision we sample steps in 1ms intervals, but we only return the last step, so if we calculate 1 frame in 60fps (16ms) - we'll actually perform 16 steps. 5 | * This is because it is possible that spring will move over 'central' point in one 16fps step, so force should change direction in the meanwhile, otherwise spring would keep accelerating on the other side. 6 | * It would be not a 'critical' bug as spring would still settle, but it would ignore subtle 'wiggle' at the end of the animation. 7 | */ 8 | 9 | /** 10 | * Each step returns new x and new velocitiy. As those are called thousands of times per second, we reuse the same array to avoid GC and constant array creation. 11 | * Important: as a result of this, result of this function should instantly be destructured and never mutated, eg const [newX, newV] = stepSpringOne(...) 12 | */ 13 | let reusedResult: [number, number] = [0, 0]; 14 | 15 | function returnReused(x: number, v: number) { 16 | reusedResult[0] = x; 17 | reusedResult[1] = v; 18 | return reusedResult; 19 | } 20 | 21 | const USE_PRECISE_EMULATION = true; 22 | 23 | function stepSpringBy( 24 | deltaMS: number, 25 | currentX: number, 26 | currentVelocity: number, 27 | targetX: number, 28 | stiffness: number, 29 | damping: number, 30 | mass: number, 31 | clamp: boolean, 32 | precision: number, 33 | ): [newX: number, newV: number] { 34 | const deltaS = deltaMS / 1000; 35 | 36 | /** 37 | * The further we are from target, the more force spring tension will apply 38 | */ 39 | const springTensionForce = -(currentX - targetX) * stiffness; 40 | /** 41 | * The faster we are moving, the more force friction force will be applied 42 | */ 43 | const frictionForce = -currentVelocity * damping; 44 | 45 | // the bigger the mass, the less 'raw' force will actually affect the movement 46 | const finalForce = (springTensionForce + frictionForce) / mass; 47 | 48 | const newVelocity = currentVelocity + finalForce * deltaS; 49 | const newX = currentX + newVelocity * deltaS; 50 | 51 | if (clamp) { 52 | if (currentX < targetX && newX > targetX) { 53 | return returnReused(targetX, 0); 54 | } 55 | 56 | if (currentX > targetX && newX < targetX) { 57 | return returnReused(targetX, 0); 58 | } 59 | } 60 | 61 | const newDistanceToTarget = Math.abs(newX - targetX); 62 | 63 | // When both velocity and distance to target are under the precision, we 'snap' to the target and stop the spring 64 | // Otherwise - spring would keep moving, slower and slower, forever as it's energy would never fall to 0 65 | if (Math.abs(newVelocity) < precision && newDistanceToTarget < precision) { 66 | return returnReused(targetX, 0); 67 | } 68 | 69 | return returnReused(newX, newVelocity); 70 | } 71 | 72 | export function stepSpring( 73 | deltaMS: number, 74 | currentX: number, 75 | currentV: number, 76 | targetX: number, 77 | stiffness: number, 78 | damping: number, 79 | mass: number, 80 | clamp: boolean, 81 | precision: number, 82 | ): [newX: number, newV: number] { 83 | if (!USE_PRECISE_EMULATION) { 84 | return stepSpringBy(deltaMS, currentX, currentV, targetX, stiffness, damping, mass, clamp, precision); 85 | } 86 | 87 | const upperDeltaMS = Math.ceil(deltaMS); 88 | 89 | if (upperDeltaMS > 10_000) { 90 | throw new Error("Spring emulation is too long, finishing simulation"); 91 | } 92 | 93 | for (let i = 1; i <= upperDeltaMS; i++) { 94 | // Last, sub-1ms step - do precise emulation 95 | if (i > deltaMS) { 96 | [currentX, currentV] = stepSpringBy( 97 | i - deltaMS, 98 | currentX, 99 | currentV, 100 | targetX, 101 | stiffness, 102 | damping, 103 | mass, 104 | clamp, 105 | precision, 106 | ); 107 | } else { 108 | // Emulate in 1ms steps 109 | [currentX, currentV] = stepSpringBy(1, currentX, currentV, targetX, stiffness, damping, mass, clamp, precision); 110 | } 111 | } 112 | 113 | return returnReused(currentX, currentV); 114 | } 115 | -------------------------------------------------------------------------------- /mobxmotion/src/springs.ts: -------------------------------------------------------------------------------- 1 | import { AnimatedSpring } from "./AnimatedSpring"; 2 | import { RefObject } from "react"; 3 | import { SpringConfigInput } from "./springConfig"; 4 | import { getElementOwnerWindow } from "./utils"; 5 | 6 | interface CurrentlyComputingStyleProperty { 7 | springsManager: SpringsManager | null; 8 | property: string; 9 | springIndex: number; 10 | } 11 | 12 | let currentlyComputingStyleProperty: CurrentlyComputingStyleProperty = { 13 | springsManager: null, 14 | property: "", 15 | springIndex: 0, 16 | }; 17 | 18 | type ElementRef = RefObject; 19 | 20 | export function setCurrentlyComputingStyleProperty(springsManager: SpringsManager, property: string) { 21 | currentlyComputingStyleProperty.springsManager = springsManager; 22 | currentlyComputingStyleProperty.property = property; 23 | currentlyComputingStyleProperty.springIndex = 0; 24 | } 25 | 26 | export function clearCurrentlyComputingStyleProperty() { 27 | currentlyComputingStyleProperty.springsManager = null; 28 | } 29 | 30 | export function $spring(value: number, options?: SpringConfigInput): number { 31 | if (!currentlyComputingStyleProperty.springsManager) { 32 | throw new Error("$spring can only be used inside style getters of MobxMotion components"); 33 | } 34 | 35 | return currentlyComputingStyleProperty.springsManager.getSpringValue( 36 | currentlyComputingStyleProperty.property, 37 | currentlyComputingStyleProperty.springIndex++, 38 | value, 39 | options, 40 | ); 41 | } 42 | 43 | const EMPTY_SPRING_CONFIG: SpringConfigInput = {}; 44 | 45 | export class SpringsManager { 46 | private springs: Map = new Map(); 47 | 48 | constructor(private ref: ElementRef) {} 49 | 50 | get element() { 51 | return this.ref.current; 52 | } 53 | 54 | getSpringValue(property: string, springIndex: number, value: number, options?: SpringConfigInput): number { 55 | const element = this.element; 56 | 57 | // It is likely the first render, don't initialize spring yet. Simply return input value. 58 | if (!element) { 59 | return value; 60 | } 61 | 62 | let springs = this.springs.get(property); 63 | 64 | if (!springs) { 65 | springs = []; 66 | this.springs.set(property, springs); 67 | } 68 | 69 | let spring = springs[springIndex]; 70 | 71 | if (!spring) { 72 | spring = new AnimatedSpring(value, options, getElementOwnerWindow(element)); 73 | springs[springIndex] = spring; 74 | } else { 75 | spring.setTargetValue(value); 76 | spring.updateConfig(options ?? EMPTY_SPRING_CONFIG); 77 | } 78 | 79 | return spring.value; 80 | } 81 | 82 | clearUnused(usedProperties: string[]) { 83 | for (let [property, springs] of this.springs.entries()) { 84 | if (usedProperties.includes(property)) continue; 85 | 86 | springs = [...springs]; 87 | 88 | this.springs.delete(property); 89 | 90 | for (const spring of springs) { 91 | spring.stop(); 92 | } 93 | } 94 | } 95 | 96 | clear() { 97 | const springs = [...this.springs.values()].flat(); 98 | 99 | this.springs.clear(); 100 | 101 | for (const spring of springs) { 102 | spring.stop(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /mobxmotion/src/style.ts: -------------------------------------------------------------------------------- 1 | import { SpringsManager, clearCurrentlyComputingStyleProperty, setCurrentlyComputingStyleProperty } from "./springs"; 2 | 3 | import { CSSProperties } from "react"; 4 | import { typedEntries } from "./utils"; 5 | 6 | type HTMLOrSVGElement = HTMLElement | SVGElement; 7 | 8 | type AnyVariableStyle = { 9 | [key in `--${string}`]: string | number; 10 | }; 11 | 12 | type TransformProperties = { 13 | x?: number; 14 | y?: number; 15 | rotateZ?: number; 16 | scale?: number; 17 | }; 18 | 19 | export type StyleWithTransforms = CSSProperties & AnyVariableStyle & TransformProperties; 20 | export type StyleAndVariables = CSSProperties & AnyVariableStyle; 21 | 22 | function resolveStyleInput(style: StyleWithTransforms): StyleAndVariables { 23 | const { x, y, rotateZ, scale, ...cssProperties } = style; 24 | 25 | let finalTransforms: string[] = cssProperties.transform ? [cssProperties.transform] : []; 26 | 27 | if (x !== undefined) { 28 | finalTransforms.push(`translateX(${x}px)`); 29 | } 30 | 31 | if (y !== undefined) { 32 | finalTransforms.push(`translateY(${y}px)`); 33 | } 34 | 35 | if (rotateZ !== undefined) { 36 | finalTransforms.push(`rotateZ(${rotateZ}deg)`); 37 | } 38 | 39 | if (scale !== undefined) { 40 | finalTransforms.push(`scale(${scale})`); 41 | } 42 | 43 | const finalTransform = finalTransforms.length > 0 ? finalTransforms.join(" ") : ""; 44 | 45 | if (finalTransform) { 46 | cssProperties.transform = finalTransform; 47 | } 48 | 49 | return cssProperties; 50 | } 51 | 52 | export function computeStyleAndVariables( 53 | style: StyleWithTransforms, 54 | springsManager: SpringsManager, 55 | ): StyleAndVariables { 56 | const computedStyles: StyleWithTransforms = {}; 57 | 58 | for (const key in style) { 59 | try { 60 | setCurrentlyComputingStyleProperty(springsManager, key); 61 | /** 62 | * style[key] can be a getter 63 | */ 64 | // @ts-ignore 65 | computedStyles[key] = style[key]; 66 | clearCurrentlyComputingStyleProperty(); 67 | } catch (error) { 68 | clearCurrentlyComputingStyleProperty(); 69 | throw error; 70 | } 71 | } 72 | 73 | return resolveStyleInput(computedStyles); 74 | } 75 | 76 | function getIsCSSVariableString(key: string): key is `--${string}` { 77 | return key.startsWith("--"); 78 | } 79 | 80 | export function getChangedProperties( 81 | styleAndVariables: StyleAndVariables, 82 | previousStylesAndVariables: StyleAndVariables, 83 | ): StyleAndVariables { 84 | const changedProperties: StyleAndVariables = {}; 85 | 86 | for (const [key, value] of typedEntries(styleAndVariables)) { 87 | if (previousStylesAndVariables[key] !== value) { 88 | // @ts-ignore 89 | changedProperties[key] = value; 90 | } 91 | } 92 | 93 | return changedProperties; 94 | } 95 | 96 | export function applyStyleAndVariables(element: HTMLOrSVGElement, styleAndVariables: StyleAndVariables) { 97 | const style = element.style; 98 | 99 | for (const [key, value] of typedEntries(styleAndVariables)) { 100 | if (getIsCSSVariableString(key)) { 101 | style.setProperty(key, `${value}`); 102 | } else { 103 | // @ts-ignore 104 | style[key] = value; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /mobxmotion/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { action } from "mobx"; 2 | 3 | export function getElementOwnerWindow(element: Element) { 4 | return element.ownerDocument.defaultView; 5 | } 6 | 7 | export type AnyObject = Record; 8 | 9 | export function typedKeys(input: O): Array { 10 | return Object.keys(input as any) as Array; 11 | } 12 | 13 | export function typedEntries(input: T): Array<[keyof T, T[keyof T]]> { 14 | return Object.entries(input) as Array<[keyof T, T[keyof T]]>; 15 | } 16 | 17 | export function shallowEqual(a: unknown, b: unknown) { 18 | if (a === b) return true; 19 | 20 | if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false; 21 | 22 | const keysA = Object.keys(a); 23 | const keysB = Object.keys(b); 24 | 25 | if (keysA.length !== keysB.length) return false; 26 | 27 | for (const key of keysA) { 28 | // @ts-ignore 29 | if (a[key] !== b[key]) return false; 30 | } 31 | 32 | return true; 33 | } 34 | 35 | const currentRafPromise = new WeakMap>(); 36 | 37 | export function raf(targetWindow: Window = window) { 38 | let promise = currentRafPromise.get(targetWindow); 39 | 40 | if (promise) return promise; 41 | 42 | promise = new Promise((resolve) => { 43 | targetWindow.requestAnimationFrame(action(resolve)); 44 | }).finally(() => { 45 | currentRafPromise.delete(targetWindow); 46 | }); 47 | 48 | currentRafPromise.set(targetWindow, promise); 49 | 50 | return promise; 51 | } 52 | -------------------------------------------------------------------------------- /mobxmotion/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src" 6 | }, 7 | "exclude": ["demo"] 8 | } 9 | -------------------------------------------------------------------------------- /mobxmotion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["ESNext", "DOM"], 7 | "allowJs": true, 8 | "jsx": "react", 9 | "moduleResolution": "Node", 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "esModuleInterop": true, 14 | "noEmit": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "skipLibCheck": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": ["src/*"], 22 | "@": ["src"] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mobxmotion/vite.config.build.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import dts from "vite-plugin-dts"; 3 | import fs from "fs"; 4 | import { resolve } from "path"; 5 | 6 | function collectExternalDependencies() { 7 | const packageJson = fs.readFileSync(resolve(__dirname, "package.json"), "utf8"); 8 | const dependencies = JSON.parse(packageJson).dependencies ?? {}; 9 | const peerDependencies = JSON.parse(packageJson).peerDependencies ?? {}; 10 | return Object.keys(dependencies).concat(Object.keys(peerDependencies)); 11 | } 12 | 13 | export default defineConfig({ 14 | build: { 15 | lib: { 16 | formats: ["es", "cjs"], 17 | entry: resolve(__dirname, "src", "index.ts"), 18 | fileName: (format) => `index.${format}.js`, 19 | }, 20 | outDir: resolve(__dirname, "dist"), 21 | rollupOptions: { 22 | external: collectExternalDependencies(), 23 | }, 24 | minify: false, 25 | emptyOutDir: true, 26 | sourcemap: false, 27 | }, 28 | plugins: [dts({ tsconfigPath: resolve(__dirname, "tsconfig.build.json"), rollupTypes: true })], 29 | }); 30 | -------------------------------------------------------------------------------- /mobxmotion/vite.config.demo.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | 4 | export default defineConfig({ 5 | root: resolve(__dirname, "demo"), 6 | resolve: { 7 | alias: { 8 | "@": resolve(__dirname, "src"), 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /mobxmotion/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import { resolve } from "path"; 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: { 7 | "@": resolve(__dirname, "src"), 8 | }, 9 | }, 10 | test: { 11 | // ... 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobxmotion-root", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "private": true, 6 | "workspaces": { 7 | "packages": [ 8 | "mobxmotion", 9 | "docs" 10 | ] 11 | }, 12 | "scripts": { 13 | "mobxmotion": "yarn workspace mobxmotion", 14 | "docs": "yarn workspace mobxmotion-docs" 15 | }, 16 | "packageManager": "yarn@4.4.1" 17 | } 18 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "version": 2, 4 | "buildCommand": "yarn build", 5 | "outputDirectory": ".next", 6 | "installCommand": "yarn install", 7 | "framework": "nextjs" 8 | } 9 | --------------------------------------------------------------------------------