├── .commitlintrc.cjs ├── .editorconfig ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── eslint.config.mjs ├── images └── tiny-motion-logo.svg ├── package.json ├── packages ├── app │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vite.svg │ ├── src │ │ ├── assets │ │ │ ├── icons │ │ │ │ ├── ease.svg │ │ │ │ ├── easeIn.svg │ │ │ │ ├── easeInBack.svg │ │ │ │ ├── easeInBounce.svg │ │ │ │ ├── easeInCirc.svg │ │ │ │ ├── easeInCubic.svg │ │ │ │ ├── easeInElastic.svg │ │ │ │ ├── easeInEpic.svg │ │ │ │ ├── easeInExpo.svg │ │ │ │ ├── easeInOut.svg │ │ │ │ ├── easeInOutBack.svg │ │ │ │ ├── easeInOutBounce.svg │ │ │ │ ├── easeInOutCirc.svg │ │ │ │ ├── easeInOutCubic.svg │ │ │ │ ├── easeInOutElastic.svg │ │ │ │ ├── easeInOutEpic.svg │ │ │ │ ├── easeInOutExpo.svg │ │ │ │ ├── easeInOutQuad.svg │ │ │ │ ├── easeInOutQuart.svg │ │ │ │ ├── easeInOutQuint.svg │ │ │ │ ├── easeInOutSine.svg │ │ │ │ ├── easeInQuad.svg │ │ │ │ ├── easeInQuart.svg │ │ │ │ ├── easeInQuint.svg │ │ │ │ ├── easeInSine.svg │ │ │ │ ├── easeOut.svg │ │ │ │ ├── easeOutBack.svg │ │ │ │ ├── easeOutBounce.svg │ │ │ │ ├── easeOutCirc.svg │ │ │ │ ├── easeOutCubic.svg │ │ │ │ ├── easeOutElastic.svg │ │ │ │ ├── easeOutEpic.svg │ │ │ │ ├── easeOutExpo.svg │ │ │ │ ├── easeOutInBack.svg │ │ │ │ ├── easeOutInCirc.svg │ │ │ │ ├── easeOutInCubic.svg │ │ │ │ ├── easeOutInEpic.svg │ │ │ │ ├── easeOutInExpo.svg │ │ │ │ ├── easeOutInQuad.svg │ │ │ │ ├── easeOutInQuart.svg │ │ │ │ ├── easeOutInQuint.svg │ │ │ │ ├── easeOutInSine.svg │ │ │ │ ├── easeOutQuad.svg │ │ │ │ ├── easeOutQuart.svg │ │ │ │ ├── easeOutQuint.svg │ │ │ │ ├── easeOutSine.svg │ │ │ │ ├── linear.svg │ │ │ │ └── logo.svg │ │ │ └── images │ │ │ │ └── dog-photo.jpg │ │ ├── components │ │ │ ├── ApiTable.tsx │ │ │ ├── Code.tsx │ │ │ ├── CodeBlock.tsx │ │ │ ├── CodeModal.tsx │ │ │ ├── CopyButton.tsx │ │ │ ├── Header.tsx │ │ │ ├── MotionList.tsx │ │ │ ├── PageNavigate.tsx │ │ │ ├── Playground.tsx │ │ │ ├── PropertyList.tsx │ │ │ ├── SideMenu.tsx │ │ │ ├── SvgIcon.tsx │ │ │ └── ThemeSwitch.tsx │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useMobile.ts │ │ │ └── useTheme.ts │ │ ├── index.css │ │ ├── layout │ │ │ └── index.tsx │ │ ├── lib │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── pages │ │ │ ├── docs │ │ │ │ ├── animate.doc │ │ │ │ │ ├── api.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── common.api.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── overview │ │ │ │ │ └── index.tsx │ │ │ │ ├── use-animate.doc │ │ │ │ │ ├── api.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── use-group.doc │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── codes.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── use-in-view.doc │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── codes.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── use-line-draw.doc │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── codes.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── use-motion.doc │ │ │ │ │ └── index.tsx │ │ │ │ ├── use-multiple.doc │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── codes.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── use-spring.doc │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── codes.ts │ │ │ │ │ └── index.tsx │ │ │ │ └── use-value.doc │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── codes.ts │ │ │ │ │ └── index.tsx │ │ │ ├── home │ │ │ │ └── index.tsx │ │ │ └── presets │ │ │ │ └── index.tsx │ │ ├── routes │ │ │ └── index.tsx │ │ ├── styles │ │ │ ├── animation.css │ │ │ └── theme.css │ │ ├── types │ │ │ └── index.ts │ │ ├── ui │ │ │ ├── Button │ │ │ │ ├── buttonVariants.ts │ │ │ │ └── index.tsx │ │ │ ├── Divider.tsx │ │ │ ├── Heading.tsx │ │ │ ├── HoverCard.tsx │ │ │ ├── Link.tsx │ │ │ ├── Loading.tsx │ │ │ ├── Slider.tsx │ │ │ ├── Table.tsx │ │ │ ├── Tag.tsx │ │ │ └── Tooltip.tsx │ │ ├── utils │ │ │ └── utils.ts │ │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── lib │ ├── LICENSE │ ├── README.md │ ├── hooks │ ├── animate.ts │ ├── controller.ts │ ├── createSpring.ts │ ├── easingAlgorithm.ts │ ├── easingFunction.ts │ ├── index.ts │ ├── presets.ts │ ├── types.ts │ ├── useAnimate.ts │ ├── useGroup.ts │ ├── useInView.ts │ ├── useLineDraw.ts │ ├── useMotion.ts │ ├── useMultiple.ts │ ├── useSpring.ts │ ├── useValue.ts │ └── utils.ts │ ├── package.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── vercel.json /.commitlintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | ['build', 'ci', 'chore', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'] 8 | ] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Package Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: setup pnpm 14 | uses: pnpm/action-setup@v4 15 | with: 16 | version: 8 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: '20.x' 20 | registry-url: 'https://registry.npmjs.org' 21 | - name: install 22 | run: pnpm install 23 | - name: build lib 24 | run: npm run build:lib 25 | - name: publish 26 | run: npm publish 27 | working-directory: packages/lib 28 | env: 29 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | packages/app/tsconfig.tsbuildinfo 7 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "packages/**/*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix", "eslint"], 3 | "packages/**/*.{json,css,md}": ["prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | dist 3 | .git 4 | LICENSE -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": true, 4 | "bracketSpacing": true, 5 | "embeddedLanguageFormatting": "auto", 6 | "htmlWhitespaceSensitivity": "css", 7 | "insertPragma": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 200, 10 | "proseWrap": "never", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": true, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "none", 17 | "useTabs": false, 18 | "vueIndentScriptAndStyle": false, 19 | "singleAttributePerLine": false 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit" 4 | }, 5 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 6 | "editor.formatOnSave": true, 7 | "cSpell.words": ["tailwindcss", "commitlint", "clsx", "tsup", "tabler", "shiki", "WAAPI", "Numberish", "usehooks", "cirolee", "hoverable"], 8 | "typescript.tsdk": "node_modules/typescript/lib", 9 | "tailwindCSS.classFunctions": ["cva", "cn"] 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CiroLee 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 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import pluginReact from 'eslint-plugin-react'; 5 | 6 | export default [ 7 | { files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] }, 8 | { languageOptions: { globals: globals.browser, ecmaVersion: 12, sourceType: 'module' } }, 9 | pluginJs.configs.recommended, 10 | ...tseslint.configs.recommended, 11 | pluginReact.configs.flat.recommended, 12 | { 13 | rules: { 14 | 'react/react-in-jsx-scope': 0, 15 | '@typescript-eslint/no-explicit-any': 0, 16 | 'react/no-unescaped-entities': 0, 17 | '@typescript-eslint/no-unused-expressions': 0 18 | }, 19 | ignores: ['/node_modules', 'dist'], 20 | settings: { 21 | react: { 22 | version: 'detect' 23 | } 24 | } 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cirolee/tiny-motion", 3 | "description": "The high-performance extension of Web Animation API for React Hooks", 4 | "browser": "packages/lib/dist/index.js", 5 | "types": "packages/lib/dist/index.d.ts", 6 | "type": "module", 7 | "scripts": { 8 | "dev:app": "pnpm --filter app run dev", 9 | "dev:lib": "pnpm --filter @cirolee/tiny-motion run dev", 10 | "build:app": "pnpm --filter app run build", 11 | "build:lib": "pnpm --filter @cirolee/tiny-motion run build", 12 | "prepare": "husky" 13 | }, 14 | "workspaces": [ 15 | "packages/*" 16 | ], 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@commitlint/config-conventional": "^19.8.1", 20 | "@eslint/js": "^9.26.0", 21 | "@typescript-eslint/eslint-plugin": "^8.32.1", 22 | "@typescript-eslint/parser": "^8.32.1", 23 | "commitlint": "^19.8.1", 24 | "eslint": "^9.26.0", 25 | "eslint-plugin-react": "^7.37.5", 26 | "globals": "^16.1.0", 27 | "husky": "^9.1.7", 28 | "lint-staged": "^16.0.0", 29 | "prettier": "^3.5.3", 30 | "prettier-plugin-tailwindcss": "^0.6.11", 31 | "typescript": "^5.8.3", 32 | "typescript-eslint": "^8.32.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/app/.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 | -------------------------------------------------------------------------------- /packages/app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": true, 4 | "bracketSpacing": true, 5 | "embeddedLanguageFormatting": "auto", 6 | "htmlWhitespaceSensitivity": "css", 7 | "insertPragma": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 200, 10 | "proseWrap": "never", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": true, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "none", 17 | "useTabs": false, 18 | "vueIndentScriptAndStyle": false, 19 | "singleAttributePerLine": false, 20 | "plugins": ["prettier-plugin-tailwindcss"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/app/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config({ 16 | extends: [ 17 | // Remove ...tseslint.configs.recommended and replace with this 18 | ...tseslint.configs.recommendedTypeChecked, 19 | // Alternatively, use this for stricter rules 20 | ...tseslint.configs.strictTypeChecked, 21 | // Optionally, add this for stylistic rules 22 | ...tseslint.configs.stylisticTypeChecked 23 | ], 24 | languageOptions: { 25 | // other options... 26 | parserOptions: { 27 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 28 | tsconfigRootDir: import.meta.dirname 29 | } 30 | } 31 | }); 32 | ``` 33 | 34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 35 | 36 | ```js 37 | // eslint.config.js 38 | import reactX from 'eslint-plugin-react-x'; 39 | import reactDom from 'eslint-plugin-react-dom'; 40 | 41 | export default tseslint.config({ 42 | plugins: { 43 | // Add the react-x and react-dom plugins 44 | 'react-x': reactX, 45 | 'react-dom': reactDom 46 | }, 47 | rules: { 48 | // other rules... 49 | // Enable its recommended typescript rules 50 | ...reactX.configs['recommended-typescript'].rules, 51 | ...reactDom.configs.recommended.rules 52 | } 53 | }); 54 | ``` 55 | -------------------------------------------------------------------------------- /packages/app/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import reactHooks from 'eslint-plugin-react-hooks'; 4 | import reactRefresh from 'eslint-plugin-react-refresh'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }] 23 | } 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /packages/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | tiny-motion 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@cirolee/tiny-motion": "workspace:*", 13 | "@radix-ui/react-slot": "^1.2.2", 14 | "@tabler/icons-react": "^3.31.0", 15 | "class-variance-authority": "^0.7.1", 16 | "clsx": "^2.1.1", 17 | "radix-ui": "^1.4.1", 18 | "react": "^19.1.0", 19 | "react-dom": "^19.1.0", 20 | "react-router-dom": "^7.6.0", 21 | "shiki": "^3.4.0", 22 | "tailwind-merge": "^3.3.0", 23 | "usehooks-ts": "^3.1.1" 24 | }, 25 | "devDependencies": { 26 | "@tailwindcss/vite": "^4.1.6", 27 | "@types/react": "^19.1.4", 28 | "@types/react-dom": "^19.1.5", 29 | "@vitejs/plugin-react-swc": "^3.9.0", 30 | "prettier": "^3.5.3", 31 | "prettier-plugin-tailwindcss": "^0.6.11", 32 | "tailwindcss": "^4.1.6", 33 | "typescript": "~5.8.3", 34 | "vite": "^6.3.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiroLee/tiny-motion/e71754457f734f267007ba0e922462dbf79b84b7/packages/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/ease.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeIn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInBack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInBounce.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInCirc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInCubic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInEpic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInExpo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutBack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutCirc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutCubic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutElastic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutEpic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutExpo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutQuad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutQuart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutQuint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInOutSine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInQuad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInQuart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInQuint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeInSine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutBack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutCirc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutCubic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutEpic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutExpo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInBack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInCirc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInCubic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInEpic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInExpo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInQuad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInQuart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInQuint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutInSine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutQuad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutQuart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutQuint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/easeOutSine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/icons/linear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/images/dog-photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiroLee/tiny-motion/e71754457f734f267007ba0e922462dbf79b84b7/packages/app/src/assets/images/dog-photo.jpg -------------------------------------------------------------------------------- /packages/app/src/components/ApiTable.tsx: -------------------------------------------------------------------------------- 1 | import { Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow } from '@/ui/Table'; 2 | import Link from '@/ui/Link'; 3 | import Divider from '@/ui/Divider'; 4 | import Tag from '@/ui/Tag'; 5 | 6 | export interface ApiTableRow { 7 | name: React.ReactNode; 8 | desc?: React.ReactNode; 9 | type: React.ReactNode; 10 | required?: boolean | string; 11 | default?: boolean | string | number; 12 | } 13 | export interface ApiTableProps { 14 | rows: ApiTableRow[]; 15 | omitHeads?: string[]; 16 | styles?: { 17 | name?: React.CSSProperties; 18 | description?: React.CSSProperties; 19 | type?: React.CSSProperties; 20 | required?: React.CSSProperties; 21 | default?: React.CSSProperties; 22 | }; 23 | } 24 | const header = ['Name', 'Type', 'Description', 'Required', 'Default']; 25 | function renderType(type: React.ReactNode) { 26 | if (typeof type !== 'string') { 27 | return type; 28 | } 29 | if (type.startsWith('tag')) { 30 | const tags = type.replace(/^tag:/, '').split('|'); 31 | return ( 32 |
33 | {tags.map((item, index) => ( 34 |
35 | {item} 36 | {index !== tags.length - 1 && } 37 |
38 | ))} 39 |
40 | ); 41 | } else if (type.startsWith('link')) { 42 | const pattern = /link: (\w+)\((https?:\/\/\S+)\)/; 43 | const match = type.match(pattern); 44 | if (match) { 45 | return ( 46 | 47 | {match[1]} 48 | 49 | ); 50 | } 51 | } 52 | return type; 53 | } 54 | export default function ApiTable({ rows, omitHeads = [], styles = { name: {}, default: {}, description: {}, required: {} } }: ApiTableProps) { 55 | const _header = header.filter((item) => !omitHeads.includes(item)); 56 | return ( 57 | 58 | 59 | {_header.map((item) => ( 60 | 61 | {item} 62 | 63 | ))} 64 | 65 | 66 | {rows.map((item, index) => ( 67 | 68 | {item.name !== undefined ? {item.name} : null} 69 | {item.type !== undefined ? {renderType(item.type)} : null} 70 | {item.desc !== undefined ? {item.desc} : null} 71 | {item.required !== undefined ? {item.required.toString()} : null} 72 | {item.default !== undefined ? {item.default?.toString()} : null} 73 | 74 | ))} 75 | 76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /packages/app/src/components/Code.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, forwardRef } from 'react'; 2 | import { createHighlighter } from 'shiki'; 3 | 4 | interface CodeProps { 5 | code: string; 6 | lang?: string; 7 | className?: string; 8 | rendered?: () => void; 9 | style?: React.CSSProperties; 10 | highlightLines?: number[]; 11 | highlightRange?: number[][]; 12 | diffAddLines?: number[]; 13 | diffRemoveLines?: number[]; 14 | } 15 | 16 | const Code = forwardRef((props, ref) => { 17 | const { code, lang, diffAddLines = [], diffRemoveLines = [], highlightLines = [], highlightRange = [[]], rendered, ...rest } = props; 18 | const [html, setHtml] = useState(''); 19 | const parseCode = async (code: string, lang = 'typescript') => { 20 | const highlighter = await createHighlighter({ 21 | langs: ['typescript', 'javascript', 'html', 'css', 'bash'], 22 | themes: ['one-dark-pro'] 23 | }); 24 | const data = highlighter.codeToHtml(code, { 25 | lang, 26 | theme: 'one-dark-pro', 27 | transformers: [ 28 | { 29 | line(node, line) { 30 | node.properties['data-line'] = line; 31 | // highlight 32 | if (highlightLines.includes(line)) this.addClassToHast(node, 'highlight-line'); 33 | highlightRange.forEach((arr) => { 34 | if (line >= arr[0] && line <= arr[arr.length - 1]) { 35 | this.addClassToHast(node, 'highlight-line'); 36 | } 37 | }); 38 | // diffs 39 | if (diffAddLines.includes(line)) { 40 | this.addClassToHast(node, 'diff add'); 41 | } 42 | if (diffRemoveLines.includes(line)) { 43 | this.addClassToHast(node, 'diff remove'); 44 | } 45 | } 46 | } 47 | ] 48 | }); 49 | 50 | setHtml(data); 51 | }; 52 | useEffect(() => { 53 | parseCode(code, lang).then(rendered); 54 | }, [code, lang, html]); 55 | 56 | return
; 57 | }); 58 | 59 | Code.displayName = 'Code'; 60 | export default Code; 61 | -------------------------------------------------------------------------------- /packages/app/src/components/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import Code from './Code'; 3 | import CopyButton from './CopyButton'; 4 | import Button from '@/ui/Button'; 5 | import { cn } from '@/lib/utils'; 6 | interface CodeBlockProps { 7 | code: string; 8 | lang?: string; 9 | className?: string; 10 | highlightLines?: number[]; 11 | highlightRange?: number[][]; 12 | diffAddLines?: number[]; 13 | diffRemoveLines?: number[]; 14 | } 15 | export default function CodeBlock(props: CodeBlockProps) { 16 | const { code, lang, highlightLines, highlightRange, diffAddLines, diffRemoveLines, className } = props; 17 | const ref = useRef(null); 18 | const [expanded, setExpanded] = useState(false); 19 | const [showExpandButton, setShowExpandButton] = useState(false); 20 | 21 | const handleOnCodeRendered = () => { 22 | if (ref.current) { 23 | const { height } = ref.current.getBoundingClientRect(); 24 | setShowExpandButton(height > 220); 25 | } 26 | }; 27 | return ( 28 |
29 | 40 | 41 | 42 | {showExpandButton ? ( 43 |
44 | 47 |
48 | ) : null} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/app/src/components/CodeModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { IconXboxXFilled } from '@tabler/icons-react'; 3 | import { type AnimationOptions, useMultiple } from '@cirolee/tiny-motion'; 4 | import { cn } from '@/lib/utils'; 5 | import Code from './Code'; 6 | import CopyButton from './CopyButton'; 7 | 8 | interface CodeDrawerProps { 9 | show?: boolean; 10 | code: string; 11 | lang?: string; 12 | className?: string; 13 | onClose: () => void; 14 | } 15 | const options: AnimationOptions = { 16 | duration: 200, 17 | fill: 'both' 18 | }; 19 | const CodeDrawer = (props: CodeDrawerProps) => { 20 | const { show, code, lang, className, onClose } = props; 21 | 22 | const [visible, setVisible] = useState(false); 23 | const maskRef = useRef(null); 24 | const codeRef = useRef(null); 25 | 26 | const controller = useMultiple( 27 | { 28 | baseOptions: options, 29 | config: [ 30 | { 31 | ref: maskRef, 32 | keyframes: { 33 | opacity: [0, 1] 34 | } 35 | }, 36 | { 37 | ref: codeRef, 38 | keyframes: { 39 | opacity: [0, 1], 40 | transform: ['scale(0.92)', 'scale(1)'] 41 | } 42 | } 43 | ], 44 | onComplete: (trigger) => { 45 | if (trigger === 'reverse') { 46 | setVisible(false); 47 | onClose?.(); 48 | } 49 | } 50 | }, 51 | [visible] 52 | ); 53 | 54 | const handleOnClose = () => { 55 | controller.reverse(); 56 | }; 57 | 58 | useEffect(() => { 59 | setVisible(!!show); 60 | if (visible) { 61 | controller.play(); 62 | } 63 | }, [show, visible]); 64 | return ( 65 | <> 66 | {visible ? ( 67 |
68 |
69 |
70 | 71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 |
81 | ) : null} 82 | 83 | ); 84 | }; 85 | 86 | export default CodeDrawer; 87 | -------------------------------------------------------------------------------- /packages/app/src/components/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { copyToClipboard } from '@/utils/utils'; 3 | import { IconCheck, IconCopy } from '@tabler/icons-react'; 4 | import Button from '@/ui/Button'; 5 | import { cn } from '@/lib/utils'; 6 | 7 | export default function CopyButton({ text, className }: { text: string; className?: string }) { 8 | const [copied, setCopied] = useState(false); 9 | const timer = useRef>(undefined); 10 | const handleCopyCode = (code: string) => { 11 | if (copied) return; 12 | copyToClipboard(code).then(() => { 13 | setCopied(true); 14 | timer.current = setTimeout(() => { 15 | setCopied(false); 16 | }, 1000); 17 | }); 18 | }; 19 | return ( 20 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/app/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import ThemeSwitch from './ThemeSwitch'; 2 | import Button from '@/ui/Button'; 3 | import { Link } from 'react-router-dom'; 4 | import { useMobile } from '@/hooks'; 5 | import SvgIcon from './SvgIcon'; 6 | import { IconExternalLink, IconBackground, IconFile, IconBrandGithub } from '@tabler/icons-react'; 7 | export default function Header() { 8 | const isMobile = useMobile(); 9 | return ( 10 |
11 | 12 | 13 | tiny-motion 14 | 15 |
16 | 22 | 28 | 35 | 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/app/src/components/MotionList.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react'; 2 | import { presetMotionNames, type MotionName } from '@cirolee/tiny-motion'; 3 | import { cn } from '@/lib/utils'; 4 | import { cva } from 'class-variance-authority'; 5 | const prefixes = ['fade', 'slide', 'zoom', 'flip']; 6 | const item = cva('cursor-pointer rounded-sm border border-line px-2 py-3 transition hover:bg-primary/10 hover:border-primary/15', { 7 | variants: { 8 | active: { 9 | true: 'border-primary text-primary' 10 | } 11 | } 12 | }); 13 | export default function MotionList({ onClick }: { onClick: (motionName: MotionName) => void }) { 14 | const [motionName, setMotionName] = useState(presetMotionNames[0]); 15 | const getOtherNames = () => { 16 | const names = presetMotionNames.filter((name) => !prefixes.some((prefix) => name.startsWith(prefix))); 17 | return names; 18 | }; 19 | const groupedNames = useMemo(() => { 20 | const names: { [key: string]: MotionName[] } = {}; 21 | prefixes.forEach((prefix) => { 22 | if (!names[prefix]) { 23 | names[prefix] = []; 24 | } 25 | const t = presetMotionNames.filter((name) => name.startsWith(prefix)); 26 | names[prefix].push(...t); 27 | }); 28 | names['other'] = getOtherNames(); 29 | return names; 30 | }, []); 31 | 32 | const handleOnClick = (motionName: MotionName) => { 33 | setMotionName(motionName); 34 | onClick(motionName); 35 | }; 36 | return ( 37 |
38 |
39 | {Object.entries(groupedNames).map(([key, value]) => ( 40 |
    41 |

    {key}

    42 | {value.map((name) => ( 43 |
  • handleOnClick(name)}> 44 | {name} 45 |
  • 46 | ))} 47 |
48 | ))} 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /packages/app/src/components/PageNavigate.tsx: -------------------------------------------------------------------------------- 1 | import { IconChevronRight } from '@tabler/icons-react'; 2 | import { cn } from '@/lib/utils'; 3 | import { Link } from 'react-router-dom'; 4 | interface PageNavigateProps { 5 | direction: 'prev' | 'next'; 6 | path: string; 7 | children?: React.ReactNode; 8 | className?: string; 9 | style?: React.CSSProperties; 10 | } 11 | export default function PageNavigate(props: PageNavigateProps) { 12 | const { path, className, style, children, direction } = props; 13 | return ( 14 | 15 | {direction === 'next' ? : } 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/app/src/components/Playground.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | interface PlaygroundProps { 3 | className?: string; 4 | children?: React.ReactNode; 5 | ref?: React.Ref; 6 | } 7 | export default function Playground({ children, className, ref }: PlaygroundProps) { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/src/components/SideMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { router, type CustomRouteObject } from '@/routes'; 4 | import { cva } from 'class-variance-authority'; 5 | import { cn } from '@/lib/utils'; 6 | import { IconLayoutSidebarFilled } from '@tabler/icons-react'; 7 | import { useOnClickOutside } from 'usehooks-ts'; 8 | import Tag from '@/ui/Tag'; 9 | 10 | const menuItem = cva('px-4 py-2 transition relative flex items-center hover:text-secondary', { 11 | variants: { 12 | active: { 13 | true: 'text-secondary flex items-center before:absolute before:left-2 before:h-[60%] before:inline-block before:w-[2px] before:bg-secondary' 14 | } 15 | } 16 | }); 17 | 18 | export default function SideMenu() { 19 | const ref = useRef(null); 20 | const [hidden, setHidden] = useState(true); 21 | const children = router.routes.find((item) => item.id === 'root')?.children; 22 | const docsChildren = children?.find((item) => item.id === 'docs')?.children as CustomRouteObject[]; 23 | const hookList = docsChildren.filter((item) => item.meta?.type === 'hook'); 24 | const universalList = docsChildren.filter((item) => item.meta?.type === 'universal'); 25 | 26 | useOnClickOutside(ref as React.RefObject, () => { 27 | setHidden(true); 28 | }); 29 | return ( 30 |
35 |

Hooks

36 | {hookList.map((menu) => ( 37 | cn(menuItem({ active: isActive }))} onClick={() => setHidden(true)}> 38 | {menu.meta?.name} 39 | {menu.meta?.level ? ( 40 | 41 | {menu.meta?.level} 42 | 43 | ) : null} 44 | 45 | ))} 46 |

universal

47 | {universalList.map((menu) => ( 48 | cn(menuItem({ active: isActive }))} onClick={() => setHidden(true)}> 49 | {menu.meta?.name} 50 | {menu.meta?.level ? ( 51 | 52 | {menu.meta?.level} 53 | 54 | ) : null} 55 | 56 | ))} 57 |
setHidden(!hidden)}> 58 | 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /packages/app/src/components/SvgIcon.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | interface SvgIconProps { 4 | name: string; 5 | className?: string; 6 | style?: React.CSSProperties; 7 | } 8 | export default function SvgIcon({ name, ...props }: SvgIconProps) { 9 | const [svg, setSvg] = useState(''); 10 | const getPath = async (name: string) => { 11 | const { default: svg } = await import(`@/assets/icons/${name}.svg?raw`); 12 | setSvg(svg); 13 | }; 14 | 15 | useEffect(() => { 16 | getPath(name); 17 | }, [name]); 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /packages/app/src/components/ThemeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { useIsClient } from 'usehooks-ts'; 2 | import { cva } from 'class-variance-authority'; 3 | import { useTheme } from '@/hooks'; 4 | import { DropdownMenu } from 'radix-ui'; 5 | import Button from '../ui/Button'; 6 | import { IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons-react'; 7 | import type { ThemeMode } from '@/types'; 8 | 9 | const item = cva('flex items-center outline-none gap-1 text-sm rounded p-2 cursor-default transition-colors focus:bg-primary focus:text-white'); 10 | const themeMap = { 11 | light: , 12 | dark: , 13 | system: 14 | }; 15 | export default function ThemeSwitch() { 16 | const isClient = useIsClient(); 17 | const [theme, setTheme] = useTheme(); 18 | 19 | const handleSetTheme = (theme: ThemeMode) => { 20 | if (typeof setTheme === 'function') { 21 | setTheme(theme); 22 | } 23 | }; 24 | 25 | if (!isClient) return null; 26 | return ( 27 | 28 | 29 | 32 | 33 | 34 | 35 |
36 | handleSetTheme('light')}> 37 | 38 | Light 39 | 40 | handleSetTheme('dark')}> 41 | 42 | Dark 43 | 44 | handleSetTheme('system')}> 45 | 46 | System 47 | 48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /packages/app/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTheme'; 2 | export * from './useMobile'; 3 | -------------------------------------------------------------------------------- /packages/app/src/hooks/useMobile.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export const useMobile = (breakPoint: number = 640): boolean => { 4 | const [isMobile, setIsMobile] = useState(() => { 5 | if (typeof window === 'undefined') { 6 | return false; 7 | } 8 | const mediaQuery = window.matchMedia(`(max-width: ${breakPoint}px)`); 9 | return mediaQuery.matches; 10 | }); 11 | 12 | useEffect(() => { 13 | const mediaQuery = window.matchMedia(`(max-width: ${breakPoint}px)`); 14 | 15 | const onMediaQueryChange = () => { 16 | setIsMobile(mediaQuery.matches); 17 | }; 18 | 19 | mediaQuery.addEventListener('change', onMediaQueryChange); 20 | 21 | return () => { 22 | mediaQuery.removeEventListener('change', onMediaQueryChange); 23 | }; 24 | }, [breakPoint]); 25 | return isMobile; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/app/src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeMode } from '@/types'; 2 | import { useEffect } from 'react'; 3 | import { useMediaQuery, useLocalStorage } from 'usehooks-ts'; 4 | 5 | export const useTheme = () => { 6 | const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); 7 | const [theme, setTheme] = useLocalStorage('theme-mode', 'system'); 8 | const themeMode = theme === 'system' ? (prefersDarkMode ? 'dark' : 'light') : theme; 9 | 10 | useEffect(() => { 11 | document.documentElement.setAttribute('data-theme', themeMode); 12 | }, [theme, themeMode]); 13 | 14 | return [theme, setTheme]; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/app/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import './styles/theme.css'; 3 | @import './styles/animation.css'; 4 | 5 | :root { 6 | --header-height: 54px; 7 | } 8 | 9 | code { 10 | counter-reset: line; 11 | } 12 | 13 | code > span.line::before { 14 | counter-increment: line; 15 | content: counter(line); 16 | display: inline-block; 17 | width: 1rem; 18 | margin-right: 1rem; 19 | text-align: right; 20 | @apply text-zinc-500; 21 | } 22 | 23 | .highlight-line { 24 | @apply inline-block w-full bg-[#1d2027]; 25 | } 26 | .diff.add { 27 | display: inline-block; 28 | width: 100%; 29 | background-color: rgb(39 79 57 / 60%); 30 | position: relative; 31 | } 32 | .diff.add::after { 33 | content: '+'; 34 | position: absolute; 35 | top: 0; 36 | left: 1.2em; 37 | width: 1rem; 38 | height: 100%; 39 | color: #3d8769; 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | } 44 | 45 | .diff.remove { 46 | display: inline-block; 47 | width: 100%; 48 | background-color: rgb(46 21 21 / 73%); 49 | position: relative; 50 | } 51 | 52 | .diff.remove::after { 53 | content: '-'; 54 | position: absolute; 55 | top: 0; 56 | left: 1.2em; 57 | width: 1rem; 58 | height: 100%; 59 | color: #934747; 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | top: 0.2em; 64 | } 65 | -------------------------------------------------------------------------------- /packages/app/src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import { Suspense } from 'react'; 3 | import Header from '@/components/Header'; 4 | import Loading from '@/ui/Loading'; 5 | 6 | export default function Layout() { 7 | return ( 8 |
9 |
10 |
11 | }>{} 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { twMerge } from 'tailwind-merge'; 2 | import { type ClassValue, clsx } from 'clsx'; 3 | export function cn(...classnames: ClassValue[]): string { 4 | return twMerge(clsx(classnames)); 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider } from 'react-router-dom'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { router } from './routes'; 4 | import './index.css'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')!); 7 | 8 | root.render(); 9 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/animate.doc/api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiTableRow } from '@/components/ApiTable'; 2 | export const rows: ApiTableRow[] = [ 3 | { 4 | name: 'target', 5 | type: 'tag: DOMElement', 6 | desc: 'target to animate', 7 | required: true, 8 | default: '' 9 | }, 10 | { 11 | name: 'keyframes', 12 | type: 'tag: Keyframe[] | PropertyIndexedKeyframes', 13 | desc: 'keyframes of animation', 14 | required: false, 15 | default: '' 16 | }, 17 | { 18 | name: 'motion', 19 | type: 'tag: MotionName', 20 | desc: 'preset motion name, it has a lower priority than keyframes', 21 | required: false, 22 | default: '' 23 | }, 24 | { 25 | name: 'options', 26 | type: 'tag: AnimationOptions', 27 | desc: 'options of animation', 28 | required: false, 29 | default: '' 30 | } 31 | ]; 32 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/animate.doc/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { animate } from '@cirolee/tiny-motion'; 3 | import Heading from '@/ui/Heading'; 4 | import Playground from '@/components/Playground'; 5 | import Button from '@/ui/Button'; 6 | import CodeBlock from '@/components/CodeBlock'; 7 | import PageNavigate from '@/components/PageNavigate'; 8 | import ApiTable from '@/components/ApiTable'; 9 | import { rows } from './api'; 10 | 11 | const code = `import { animate } from '@cirolee/tiny-motion'; 12 | 13 | export default function App() { 14 | const ref = useRef(null); 15 | 16 | return
17 |
18 | 33 |
34 | }`; 35 | export default function AnimateDoc() { 36 | const ref = useRef(null); 37 | const handleClick = () => { 38 | if (ref.current) { 39 | animate({ 40 | target: ref.current, 41 | motion: 'flipX', 42 | options: { 43 | duration: 500, 44 | fill: 'forwards' 45 | } 46 | }); 47 | } 48 | }; 49 | return ( 50 | <> 51 | 52 | animate 53 | 54 |

animate is universal method for animating elements. It can also use preset motions.

55 | 56 |
57 | 60 |
61 | 62 | 63 | Signature 64 | 65 | 66 | 67 | Props 68 | 69 | 70 |
71 | 72 | useSpring 73 | 74 |
75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/common.api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiTableRow } from '@/components/ApiTable'; 2 | export const animationControllerCode = `// used for useGroup, useMultiple, useLineDraw hooks 3 | interface AnimateController { 4 | play: () => void; 5 | pause: () => void; 6 | cancel: () => void; 7 | reverse: () => void; 8 | resume: () => void; 9 | } 10 | // used for useValue and useSpring hooks 11 | interface ValueController extends Omit { 12 | isPlaying: boolean; 13 | isPaused: boolean; 14 | } 15 | `; 16 | 17 | export const valueControllerCode = ``; 18 | 19 | export const controllerRows: ApiTableRow[] = [ 20 | { 21 | name: 'play', 22 | type: '() => void', 23 | desc: 'play the animation' 24 | }, 25 | { 26 | name: 'pause', 27 | type: '() => void', 28 | desc: 'pause the animation' 29 | }, 30 | { 31 | name: 'cancel', 32 | type: '() => void', 33 | desc: 'cancel the animation' 34 | }, 35 | { 36 | name: 'reverse', 37 | type: '() => void', 38 | desc: 'reverse the animation' 39 | }, 40 | { 41 | name: 'resume', 42 | type: '() => void', 43 | desc: 'resume the animation' 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import SideMenu from '@/components/SideMenu'; 3 | 4 | export default function DocsPage() { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/overview/index.tsx: -------------------------------------------------------------------------------- 1 | import Heading from '@/ui/Heading'; 2 | import CodeBlock from '@/components/CodeBlock'; 3 | import PageNavigate from '@/components/PageNavigate'; 4 | import { animationControllerCode } from '../common.api'; 5 | export default function Overview() { 6 | return ( 7 |
8 | 9 | Overview 10 | 11 |

tiny-motion is a high-performance extension of Web Animation API(WAAPI) for react hooks. It's easy to get started. More controls over animations.

12 | 13 | Install 14 | 15 | 16 | 17 | Types 18 | 19 | 20 |
21 | 22 | useAnimate 23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-animate.doc/api.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@/ui/Link'; 2 | import Tag from '@/ui/Tag'; 3 | import type { ApiTableRow } from '@/components/ApiTable'; 4 | 5 | export const returnRows: ApiTableRow[] = [ 6 | { 7 | name: 'ref', 8 | desc: 'ref to the element to animate', 9 | type: 'tag: React.RefObject' 10 | }, 11 | { 12 | name: 'AnimationController', 13 | desc: ( 14 | <> 15 | the element interface animate() method, it returns the created Animation object instance 16 | 17 | ), 18 | type: ( 19 | 20 | AnimationController 21 | 22 | ) 23 | } 24 | ]; 25 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-animate.doc/index.tsx: -------------------------------------------------------------------------------- 1 | import Heading from '@/ui/Heading'; 2 | import Link from '@/ui/Link'; 3 | import CodeBlock from '@/components/CodeBlock'; 4 | import Button from '@/ui/Button'; 5 | import ApiTable from '@/components/ApiTable'; 6 | import { returnRows } from './api'; 7 | import { useAnimate } from '@cirolee/tiny-motion'; 8 | import PageNavigate from '@/components/PageNavigate'; 9 | import Playground from '@/components/Playground'; 10 | 11 | const useAnimateUsageCode = `import { useAnimate } from '@cirolee/tiny-motion'; 12 | export default function App() { 13 | const [ ref, animate ] = useAnimate(); 14 | return ( 15 |
16 |
17 | 35 |
36 | ) 37 | }`; 38 | export default function UseAnimateDoc() { 39 | const [ref, animate] = useAnimate(); 40 | return ( 41 |
42 | 43 | useAnimate 44 | 45 |

46 | basic wrap of{' '} 47 | 48 | Web Animation API(WAAPI) 49 | 50 |

51 | 52 | Usage 53 | 54 | 55 |
56 | 75 |
76 | 77 | 78 | Signature 79 | 80 | 81 | 82 | ReturnType 83 | 84 | 85 |
86 | 87 | overview 88 | 89 | 90 | useMotion 91 | 92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-group.doc/api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiTableRow } from '@/components/ApiTable'; 2 | 3 | export const propsRows: ApiTableRow[] = [ 4 | { 5 | name: 'refs', 6 | type: 'React.MutableRefObject[]', 7 | desc: 'ref array to the elements to animate', 8 | required: false, 9 | default: '' 10 | }, 11 | { 12 | name: 'selectors', 13 | type: 'string[]', 14 | desc: 'element selectors', 15 | required: false, 16 | default: '' 17 | }, 18 | { 19 | name: 'keyframes', 20 | type: 'tag: Keyframe[] | PropertyIndexedKeyframes', 21 | desc: 'keyframes for the animation', 22 | required: false, 23 | default: '' 24 | }, 25 | { 26 | name: 'motion', 27 | type: 'tag: MotionName', 28 | desc: 'preset motion name', 29 | required: false, 30 | default: '' 31 | }, 32 | { 33 | name: 'options', 34 | type: 'SpecialAnimationOptions', 35 | desc: 'options for the animation', 36 | required: false, 37 | default: '' 38 | }, 39 | { 40 | name: 'onStart', 41 | type: '() => void', 42 | desc: 'callback when the animation starts', 43 | required: false, 44 | default: '' 45 | }, 46 | { 47 | name: 'onPause', 48 | type: '() => void', 49 | desc: 'callback when the animation pauses', 50 | required: false, 51 | default: '' 52 | }, 53 | { 54 | name: 'onCancel', 55 | type: '() => void', 56 | desc: 'callback when the animation cancels', 57 | required: false, 58 | default: '' 59 | }, 60 | { 61 | name: 'onComplete', 62 | type: '(trigger: "play" | "reverse") => void', 63 | desc: 'callback when the animation completes', 64 | required: false, 65 | default: '' 66 | }, 67 | { 68 | name: 'onResume', 69 | type: '() => void', 70 | desc: 'callback when the animation resumes', 71 | required: false, 72 | default: '' 73 | } 74 | ]; 75 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-in-view.doc/api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiTableRow } from '@/components/ApiTable'; 2 | export const rows: ApiTableRow[] = [ 3 | { 4 | name: 'selectors', 5 | type: 'string[]', 6 | desc: 'CSS selectors to match elements', 7 | required: false, 8 | default: '' 9 | }, 10 | { 11 | name: 'refs', 12 | type: 'tag: React.RefObject[]', 13 | desc: 'React refs to match elements', 14 | required: false, 15 | default: '' 16 | }, 17 | { 18 | name: 'enter', 19 | type: '(target: DOMElement) => void', 20 | desc: 'callback when element enters viewport', 21 | required: false, 22 | default: '' 23 | }, 24 | { 25 | name: 'leave', 26 | type: '(target: DOMElement) => void', 27 | desc: 'callback when element leaves viewport', 28 | required: false, 29 | default: '' 30 | }, 31 | { 32 | name: 'options', 33 | type: 'InViewOptions', 34 | desc: 'options for the hook', 35 | required: false, 36 | default: '' 37 | } 38 | ]; 39 | 40 | export const options: ApiTableRow[] = [ 41 | { 42 | name: 'root', 43 | type: 'tag: Element | Document | null', 44 | desc: 'the element element that is used as the viewport for checking visibility of the target. default is browser viewport', 45 | required: false, 46 | default: 'null' 47 | }, 48 | { 49 | name: 'rootMargin', 50 | type: 'string', 51 | desc: 'Margin around the root. Can have values similar to the CSS margin', 52 | required: false, 53 | default: '' 54 | }, 55 | { 56 | name: 'threshold', 57 | type: 'number | number[]', 58 | desc: `Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed`, 59 | required: false, 60 | default: '0' 61 | }, 62 | { 63 | name: 'once', 64 | type: 'boolean', 65 | desc: 'If true, the observer will only trigger once after completion of the first visibility check', 66 | required: false, 67 | default: 'false' 68 | } 69 | ]; 70 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-in-view.doc/codes.ts: -------------------------------------------------------------------------------- 1 | export const code1 = `import { useRef } from 'react'; 2 | import { useInView, animate } from '@cirolee/tiny-motion'; 3 | 4 | export default function App() { 5 | const ref = useRef(null); 6 | 7 | useInView({ 8 | selectors: ['.fruit'], 9 | options: { 10 | root: ref.current, 11 | threshold: 0.5 12 | }, 13 | enter: (target) => { 14 | const textEl = target.querySelector('p'); 15 | textEl && 16 | animate({ 17 | target: textEl, 18 | motion: 'fadeInLeft', 19 | options: { 20 | duration: 1000, 21 | fill: 'forwards', 22 | easing: 'ease' 23 | } 24 | }); 25 | }, 26 | leave: (target) => { 27 | const textEl = target.querySelector('p'); 28 | textEl && 29 | animate({ 30 | target: textEl, 31 | motion: 'fadeOutLeft', 32 | options: { 33 | duration: 1000, 34 | fill: 'forwards', 35 | easing: 'ease' 36 | } 37 | }); 38 | } 39 | }); 40 | 41 | return ( 42 |
43 |
44 |

🍎Apple

45 |
46 |
47 |

🍊orange

48 |
49 |
50 |

🍇Wine

51 |
52 |
53 | ) 54 | } 55 | `; 56 | 57 | export const code2 = `import { useRef } from 'react'; 58 | import { useInView, useGroup, EASING_FUNCTIONS } from '@cirolee/tiny-motion'; 59 | 60 | export default function App() { 61 | const ref = useRef(null); 62 | const targetRef = useRef(null); 63 | const controller = useGroup( 64 | { 65 | selectors: ['.ball'], 66 | keyframes: { 67 | transform: ['translateY(-100%)'] 68 | }, 69 | options: { 70 | fill: 'both', 71 | duration: 500, 72 | easing: EASING_FUNCTIONS.easeInOutBack, 73 | delay: (_, index) => 50 + index * 100 74 | } 75 | }, 76 | [] 77 | ); 78 | useInView({ 79 | refs: [targetRef], 80 | options: { 81 | root: pg2Ref.current 82 | }, 83 | enter: () => { 84 | controller.play(); 85 | } 86 | }); 87 | 88 | return ( 89 |
90 |
control other elements
91 |
92 |
93 |
94 |
95 |
96 |
97 | ) 98 | } 99 | `; 100 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-in-view.doc/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import Heading from '@/ui/Heading'; 3 | import Playground from '@/components/Playground'; 4 | import { useInView, animate, useGroup, EASING_FUNCTIONS } from '@cirolee/tiny-motion'; 5 | import PageNavigate from '@/components/PageNavigate'; 6 | import CodeBlock from '@/components/CodeBlock'; 7 | import { code1, code2 } from './codes'; 8 | import ApiTable from '@/components/ApiTable'; 9 | import { rows, options } from './api'; 10 | import Link from '@/ui/Link'; 11 | 12 | export default function UseInViewDoc() { 13 | const pg1Ref = useRef(null); 14 | const pg2Ref = useRef(null); 15 | const targetRef = useRef(null); 16 | const controller = useGroup( 17 | { 18 | selectors: ['.ball'], 19 | keyframes: { 20 | transform: ['translateY(-100%)'] 21 | }, 22 | options: { 23 | fill: 'both', 24 | duration: 500, 25 | easing: EASING_FUNCTIONS.easeInOutBack, 26 | delay: (_, index) => 50 + index * 100 27 | } 28 | }, 29 | [] 30 | ); 31 | useInView({ 32 | selectors: ['.fruit'], 33 | options: { 34 | root: pg1Ref.current, 35 | threshold: 0.5 36 | }, 37 | enter: (target) => { 38 | const textEl = target.querySelector('p'); 39 | textEl && 40 | animate({ 41 | target: textEl, 42 | motion: 'fadeInLeft', 43 | options: { 44 | duration: 1000, 45 | fill: 'forwards', 46 | easing: 'ease' 47 | } 48 | }); 49 | }, 50 | leave: (target) => { 51 | const textEl = target.querySelector('p'); 52 | textEl && 53 | animate({ 54 | target: textEl, 55 | motion: 'fadeOutLeft', 56 | options: { 57 | duration: 1000, 58 | fill: 'forwards', 59 | easing: 'ease' 60 | } 61 | }); 62 | } 63 | }); 64 | 65 | useInView({ 66 | refs: [targetRef], 67 | options: { 68 | root: pg2Ref.current 69 | }, 70 | enter: () => { 71 | controller.play(); 72 | } 73 | }); 74 | return ( 75 | <> 76 | 77 | useInView 78 | 79 |

useInView is used to animate elements when target enters or leaves the viewport.

80 | 81 |
82 |

🍎Apple

83 |
84 |
85 |

🍊orange

86 |
87 |
88 |

🍇Wine

89 |
90 |
91 | 92 |

you can also use with other hooks to animate multiple elements.

93 | 94 |
control other elements
95 |
96 |
97 |
98 |
99 |
100 |
101 | 102 | 103 | Signature 104 | 105 | 106 | 107 | Props 108 | 109 | 110 |
111 | InViewOptions 112 | 113 | more about IntersectionObserver options 114 | 115 |
116 | 117 |
118 | 119 | useMultiple 120 | 121 | 122 | useLineDraw 123 | 124 |
125 | 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-line-draw.doc/api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiTableRow } from '@/components/ApiTable'; 2 | export const rows: ApiTableRow[] = [ 3 | { 4 | name: 'refs', 5 | type: 'ReactMultipleRef[]', 6 | desc: 'refs of target elements', 7 | required: false, 8 | default: '' 9 | }, 10 | { 11 | name: 'selectors', 12 | type: 'string[]', 13 | desc: 'css selector of target elements', 14 | required: false, 15 | default: '' 16 | }, 17 | { 18 | name: 'keyframes', 19 | type: 'tag: Keyframes[]', 20 | desc: 'keyframes of animation', 21 | required: false, 22 | default: '' 23 | }, 24 | { 25 | name: 'options', 26 | type: 'tag: SpecialAnimationOptions', 27 | desc: 'options of animation', 28 | required: false, 29 | default: '' 30 | }, 31 | { 32 | name: 'drawType', 33 | type: 'tag: appear | disappear', 34 | desc: 'drawing line type', 35 | required: false, 36 | default: 'appear' 37 | }, 38 | { 39 | name: 'onStart', 40 | type: '() => void', 41 | desc: 'callback when the animation starts', 42 | required: false 43 | }, 44 | { 45 | name: 'onPause', 46 | type: '() => void', 47 | desc: 'callback when the animation pauses', 48 | required: false 49 | }, 50 | { 51 | name: 'onCancel', 52 | type: '() => void', 53 | desc: 'callback when the animation cancels', 54 | required: false 55 | }, 56 | { 57 | name: 'onComplete', 58 | type: '(trigger: "play" | "reverse") => void', 59 | desc: 'callback when the animation completes', 60 | required: false 61 | }, 62 | { 63 | name: 'onResume', 64 | type: '() => void', 65 | desc: 'callback when the animation resumes', 66 | required: false 67 | } 68 | ]; 69 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-line-draw.doc/codes.ts: -------------------------------------------------------------------------------- 1 | export const code1 = `import { useRef } from 'react'; 2 | import { useLineDraw } from '@cirolee/tiny-motion'; 3 | 4 | export default function App() { 5 | const path1Ref = useRef(null); 6 | const path2Ref = useRef(null); 7 | const controller = useLineDraw( 8 | { 9 | refs: [path1Ref, path2Ref], 10 | drawType: 'appear', 11 | options: { 12 | duration: 1500, 13 | fill: 'forwards', 14 | easing: 'ease-in-out' 15 | } 16 | }, 17 | [] 18 | ); 19 | return ( 20 |
21 | 31 | 32 | 33 | 34 | 37 |
38 | ) 39 | }`; 40 | export const code2 = `import { useRef } from 'react'; 41 | import { useLineDraw } from '@cirolee/tiny-motion'; 42 | 43 | export default function App() { 44 | const path1Ref = useRef(null); 45 | const path2Ref = useRef(null); 46 | const controller = useLineDraw( 47 | { 48 | selectors: ['#json path'], 49 | drawType: 'appear', 50 | options: { 51 | duration: 1000, 52 | fill: 'forwards', 53 | easing: 'ease-in-out', 54 | delay: (e, index) => { 55 | return index * 1000; 56 | } 57 | } 58 | }, 59 | [] 60 | ); 61 | return ( 62 |
63 | 74 | 75 | 76 | 77 | 78 | 79 | 82 |
83 | ) 84 | }`; 85 | 86 | export const code3 = `import { useRef } from 'react'; 87 | import { useLineDraw } from '@cirolee/tiny-motion'; 88 | 89 | export default function App() { 90 | const path1Ref = useRef(null); 91 | const path2Ref = useRef(null); 92 | const controller = useLineDraw( 93 | { 94 | selectors: ['#smile path', "#smile circle"], 95 | keyframes: { 96 | stroke: ['transparent', '#ffae03'] 97 | }, 98 | options: { 99 | duration: 2000, 100 | fill: 'forwards', 101 | easing: 'ease-in-out' 102 | } 103 | }, 104 | [] 105 | ); 106 | return ( 107 |
108 | 119 | 120 | 121 | 122 | 123 | 124 | 127 |
128 | ) 129 | }`; 130 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-line-draw.doc/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import Playground from '@/components/Playground'; 3 | import { useLineDraw } from '@cirolee/tiny-motion'; 4 | import Button from '@/ui/Button'; 5 | import Heading from '@/ui/Heading'; 6 | import CodeBlock from '@/components/CodeBlock'; 7 | import { code1, code2, code3 } from './codes'; 8 | import Tag from '@/ui/Tag'; 9 | import ApiTable from '@/components/ApiTable'; 10 | import { rows } from './api'; 11 | import { Link } from 'react-router-dom'; 12 | import PageNavigate from '@/components/PageNavigate'; 13 | 14 | export default function UseLineDrawDoc() { 15 | const path1Ref = useRef(null); 16 | const path2Ref = useRef(null); 17 | const controller1 = useLineDraw( 18 | { 19 | refs: [path1Ref, path2Ref], 20 | drawType: 'appear', 21 | options: { 22 | duration: 1500, 23 | fill: 'forwards', 24 | easing: 'ease-in-out' 25 | } 26 | }, 27 | [] 28 | ); 29 | 30 | const controller2 = useLineDraw( 31 | { 32 | selectors: ['#json path'], 33 | drawType: 'appear', 34 | options: { 35 | duration: 1000, 36 | fill: 'forwards', 37 | easing: 'ease-in-out', 38 | delay: (_, index) => { 39 | return index * 1000; 40 | } 41 | } 42 | }, 43 | [] 44 | ); 45 | 46 | const controller3 = useLineDraw( 47 | { 48 | selectors: ['#smile path', '#smile circle'], 49 | keyframes: { 50 | stroke: ['transparent', '#ffae03'] 51 | }, 52 | options: { 53 | duration: 2000, 54 | fill: 'forwards', 55 | easing: 'ease-in-out' 56 | } 57 | }, 58 | [] 59 | ); 60 | return ( 61 | <> 62 | 63 | useLineDraw 64 | 65 |

useLineDraw is used to make svg elements(such as path, circle) to have a line animation effect.

66 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 |

77 | sometimes you need to animate lots of svg elements, then you can use selectors property to select elements. 78 |

79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 89 | 90 | 91 |

you can also add keyframes

92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 105 | Signature 106 | 107 | 108 | 109 | Props 110 | 111 | 112 | 113 | ReturnType 114 | 115 |
116 | 117 | AnimateController 118 | {' '} 119 | see{' '} 120 | 121 | here 122 | 123 |
124 |
125 | 126 | useInView 127 | 128 | 129 | useValue 130 | 131 |
132 | 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-motion.doc/index.tsx: -------------------------------------------------------------------------------- 1 | import Heading from '@/ui/Heading'; 2 | import { Link } from 'react-router-dom'; 3 | import CodeBlock from '@/components/CodeBlock'; 4 | import PageNavigate from '@/components/PageNavigate'; 5 | 6 | const code = `import { useMotion } from '@cirolee/tiny-motion'; 7 | 8 | export default function App() { 9 | /** 10 | * default value of options for the motion is: 11 | * { 12 | * fill: 'forwards', 13 | * duration: 500 14 | * } 15 | */ 16 | 17 | const [ ref, motion ] = useMotion() 18 | 19 | return ( 20 |
21 |
@cirolee/tiny-motion
22 | 23 |
24 | ) 25 | }`; 26 | export default function UseMotionDoc() { 27 | return ( 28 |
29 | 30 | useMotion 31 | 32 |

useMotion is used to perform animation using preset common animation parameters.

33 | 34 | 35 | Signature 36 | 37 | 38 | 39 | Usage 40 | 41 | 42 |
43 | you can view the whole usage{' '} 44 | 45 | here 46 | 47 |
48 |
49 | 50 | useAnimate 51 | 52 | 53 | useGroup 54 | 55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-multiple.doc/api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiTableRow } from '@/components/ApiTable'; 2 | 3 | export const propsRows: ApiTableRow[] = [ 4 | { 5 | name: 'baseOptions', 6 | desc: `the base animate options for all elements's options, it will be combined with element's option. It will be useful when they have some common options, like duration and fill mode`, 7 | type: 'tag: number | KeyframeAnimationOptions', 8 | required: false, 9 | default: '' 10 | }, 11 | { 12 | name: 'baseKeyframes', 13 | desc: `the base keyframes for all elements's keyframes, it will be combined with element's keyframes. It will be useful when they have some common keyframes, like opacity and transform, BUT they must be the same type.`, 14 | type: 'tag: Keyframe[] | PropertyIndexedKeyframes', 15 | required: false, 16 | default: '' 17 | }, 18 | { 19 | name: 'baseMotion', 20 | type: 'tag: MotionName', 21 | desc: 'preset motion name', 22 | required: false, 23 | default: '' 24 | }, 25 | { 26 | name: 'config', 27 | desc: `hook config, it contains each element's animation information`, 28 | type: 'MultipleConfig[]', 29 | required: true, 30 | default: '' 31 | }, 32 | { 33 | name: 'onStart', 34 | type: '() => void', 35 | desc: 'callback when the animation starts', 36 | required: false, 37 | default: '' 38 | }, 39 | { 40 | name: 'onPause', 41 | type: '() => void', 42 | desc: 'callback when the animation pauses', 43 | required: false, 44 | default: '' 45 | }, 46 | { 47 | name: 'onCancel', 48 | type: '() => void', 49 | desc: 'callback when the animation cancels', 50 | required: false, 51 | default: '' 52 | }, 53 | { 54 | name: 'onComplete', 55 | type: '(trigger: "play" | "reverse") => void', 56 | desc: 'callback when the animation completes', 57 | required: false, 58 | default: '' 59 | }, 60 | { 61 | name: 'onResume', 62 | type: '() => void', 63 | desc: 'callback when the animation resumes', 64 | required: false, 65 | default: '' 66 | } 67 | ]; 68 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-multiple.doc/codes.ts: -------------------------------------------------------------------------------- 1 | export const code1 = `import { useRef } from 'react'; 2 | import { useMultiple } from '@cirolee/tiny-motion'; 3 | 4 | export default function App() { 5 | const ballRef1 = useRef(null); 6 | const ballRef2 = useRef(null); 7 | const ballRef3 = useRef(null); 8 | 9 | const controller = useMultiple( 10 | { 11 | baseOptions: { 12 | duration: 1000, 13 | fill: 'forwards' 14 | }, 15 | config: [ 16 | { 17 | ref: ballRef1, 18 | keyframes: [ 19 | { transform: 'translateX(0) scale(1)', borderRadius: '0' }, 20 | { transform: 'translateX(100px) scale(1)', borderRadius: '50%', offset: 0.2 }, 21 | { transform: 'translateX(100px) scale(1)', borderRadius: '50%', offset: 0.6 }, 22 | { 23 | transform: 'translateX(160px) scale(1.6, 1)', 24 | borderRadius: '50%' 25 | }, 26 | { 27 | transform: 'translateX(360px) scale(1, 1)', 28 | borderRadius: '50%' 29 | } 30 | ] 31 | }, 32 | { 33 | ref: ballRef2, 34 | keyframes: { 35 | transform: ['translateX(200px) rotate(2turn)'], 36 | borderRadius: ['4px'] 37 | }, 38 | options: { 39 | duration: 500 40 | } 41 | }, 42 | { 43 | ref: ballRef3, 44 | keyframes: { 45 | transform: ['translateX(0)', 'translateX(300px)'], 46 | easing: 'steps(4)' 47 | } 48 | } 49 | ] 50 | }, 51 | [] 52 | ); 53 | 54 | return ( 55 |
56 |
57 |
58 |
59 | 62 |
63 | ) 64 | }`; 65 | 66 | export const code2 = `import { useRef } from 'react'; 67 | import { useMultiple } from '@cirolee/tiny-motion'; 68 | 69 | export default function App() { 70 | const ballRef1 = useRef(null); 71 | const ballRef2 = useRef(null); 72 | const ballRef3 = useRef(null); 73 | 74 | const controller = useMultiple( 75 | { 76 | baseMotion: 'breath', 77 | baseOptions: { 78 | duration: 1000, 79 | fill: 'forwards' 80 | }, 81 | config: [ 82 | { ref: ballRef4, motion: 'flipX' }, 83 | { ref: ballRef5, motion: 'flipY' }, 84 | { ref: ballRef6 } 85 | ] 86 | }, 87 | [] 88 | ); 89 | 90 | return ( 91 |
92 |
93 |
94 |
95 | 98 |
99 | ) 100 | }`; 101 | 102 | export const types = `interface MultipleConfig { 103 | ref: React.MutableRefObject; 104 | keyframes?: Keyframes; 105 | options?: AnimationOptions; 106 | motion?: MotionName; 107 | }`; 108 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-spring.doc/api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiTableRow } from '@/components/ApiTable'; 2 | export const propsRow: ApiTableRow[] = [ 3 | { 4 | name: 'from', 5 | desc: 'spring start value', 6 | type: 'number', 7 | required: true, 8 | default: '' 9 | }, 10 | { 11 | name: 'to', 12 | desc: 'spring end value', 13 | type: 'number', 14 | required: true, 15 | default: '' 16 | }, 17 | { 18 | name: 'options', 19 | desc: 'spring options', 20 | type: 'tag: SpringOptions', 21 | required: false, 22 | default: '{ mass: 1, stiffness: 100, damping: 10, velocity: 0, autoPlay: true }' 23 | } 24 | ]; 25 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-spring.doc/codes.ts: -------------------------------------------------------------------------------- 1 | export const code1 = `import { useSpring } from '@cirolee/tiny-motion'; 2 | export default function App() { 3 | const [y, controller] = useSpring(0, 240, { autoPlay: false }); 4 | 5 | return ( 6 |
7 |
9 | 12 |
13 | ) 14 | }`; 15 | 16 | export const typeCode = `interface SpringOptions { 17 | // affect the inertia of the spring 18 | mass?: number; 19 | // higher value makes the spring faster and stronger 20 | stiffness?: number; 21 | // higher value cause the spring to stop faster 22 | damping?: number; 23 | // initial speed 24 | velocity?: number; 25 | autoPlay?: boolean; 26 | }`; 27 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-spring.doc/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Playground from '@/components/Playground'; 3 | import Button from '@/ui/Button'; 4 | import Heading from '@/ui/Heading'; 5 | import Slider from '@/ui/Slider'; 6 | import CodeBlock from '@/components/CodeBlock'; 7 | import { code1, typeCode } from './codes'; 8 | import { useSpring } from '@cirolee/tiny-motion'; 9 | import Tag from '@/ui/Tag'; 10 | import { Link } from 'react-router-dom'; 11 | import ApiTable from '@/components/ApiTable'; 12 | import { propsRow } from './api'; 13 | import PageNavigate from '@/components/PageNavigate'; 14 | 15 | export default function UseSpringDoc() { 16 | const [springConf, setSpringConf] = useState({ mass: 1, stiffness: 100, damping: 10, velocity: 0 }); 17 | const [y, controller] = useSpring(0, 240, { ...springConf, autoPlay: false }); 18 | const reset = () => { 19 | controller.cancel(); 20 | setSpringConf({ 21 | mass: 1, 22 | stiffness: 100, 23 | damping: 10, 24 | velocity: 0 25 | }); 26 | }; 27 | return ( 28 |
29 | 30 | useSpring 31 | 32 |

33 | useSpring is used to simulate the real physical spring motion effect. Note: the function doesn't include time parameter, it's calculated by the function itself. 34 |

35 | 36 |
37 |
38 |
39 |
40 |
41 | mass: 42 | {springConf.mass} 43 |
44 | setSpringConf({ ...springConf, mass: value[0] })} /> 45 |
46 |
47 |
48 | stiffness: 49 | {springConf.stiffness} 50 |
51 | setSpringConf({ ...springConf, stiffness: value[0] })} /> 52 |
53 |
54 |
55 | damping: 56 | {springConf.damping} 57 |
58 | setSpringConf({ ...springConf, damping: value[0] })} /> 59 |
60 |
61 |
62 | velocity: 63 | {springConf.velocity} 64 |
65 | setSpringConf({ ...springConf, velocity: value[0] })} /> 66 |
67 |

68 | reset 69 |

70 |
71 | 72 | 75 |
76 |
77 | 78 | 79 | Signature 80 | 81 | 82 | 83 | Types 84 | 85 | 86 | 87 | Props 88 | 89 | 90 | 91 | ReturnType 92 | 93 |
94 | 95 | ValueController 96 | {' '} 97 | see{' '} 98 | 99 | here 100 | 101 |
102 |
103 | 104 | useValue 105 | 106 | 107 | animate 108 | 109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-value.doc/api.ts: -------------------------------------------------------------------------------- 1 | import type { ApiTableRow } from '@/components/ApiTable'; 2 | 3 | export const propsRows: ApiTableRow[] = [ 4 | { 5 | name: 'from', 6 | desc: 'start value', 7 | type: 'number', 8 | required: true 9 | }, 10 | { 11 | name: 'to', 12 | desc: 'end value', 13 | type: 'number', 14 | required: true 15 | }, 16 | { 17 | name: 'options', 18 | desc: 'useValue hook options', 19 | type: 'tag: ValueOptions', 20 | required: false, 21 | default: '{ duration: 1000, precision: 0, autoPlay: true,delay: 0, easing: "easeOutCubic" }' 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-value.doc/codes.ts: -------------------------------------------------------------------------------- 1 | export const code = `import { useValue } from '@cirolee/tiny-motion'; 2 | 3 | export default function App() { 4 | const [value, controller] = useValue(0, 100 { 5 | duration: 5000, 6 | autoPlay: false, 7 | easing: 'easeOutCubic' 8 | }); 9 | 10 | return ( 11 |
12 |
{value}
13 |

isPlaying: {controller.isPlaying.toString()}

14 |
15 | 18 | 21 | 24 | 27 |
28 |
29 | ) 30 | }`; 31 | 32 | export const delayCode = `import { useValue } from '@cirolee/tiny-motion'; 33 | 34 | export default function App() { 35 | const [value, controller] = useValue(0, 100 { 36 | duration: 5000, 37 | autoPlay: false, 38 | delay: 1000, 39 | easing: 'easeOutCubic' 40 | }); 41 | 42 | return ( 43 |
44 |
{value}
45 |
46 | 49 |
50 |
51 | ) 52 | }`; 53 | 54 | export const typesCode = `interface ValueOptions { 55 | duration?: number; 56 | precision?: number; 57 | autoPlay?: boolean; 58 | delay?: number; 59 | easing?: EaseAlgorithmTypes; 60 | } 61 | // support easing algorithm types 62 | type EasingAlgorithmTypes = 63 | | linear 64 | | easeOutCubic 65 | | easeInCubic 66 | | easeInOutCubic 67 | | easeInCirc 68 | | easeOutCirc 69 | | easeInOutCirc 70 | | easeInQuint 71 | | easeOutQuint 72 | | easeInOutQuint 73 | | easeInSine 74 | | easeOutSine 75 | | easeInOutSine 76 | | easeInQuad 77 | | easeOutQuad 78 | | easeInOutQuad 79 | | easeInQuart 80 | | easeOutQuart 81 | | easeInOutQuart 82 | | easeInExpo 83 | | easeOutExpo 84 | | easeInOutExpo 85 | | easeInBack 86 | | easeOutBack 87 | | easeInOutBack 88 | | easeInElastic 89 | | easeOutElastic 90 | | easeInOutElastic 91 | | easeInBounce 92 | | easeOutBounce 93 | | easeInOutBounce 94 | `; 95 | -------------------------------------------------------------------------------- /packages/app/src/pages/docs/use-value.doc/index.tsx: -------------------------------------------------------------------------------- 1 | import PageNavigate from '@/components/PageNavigate'; 2 | import Playground from '@/components/Playground'; 3 | import Heading from '@/ui/Heading'; 4 | import Button from '@/ui/Button'; 5 | import { useValue } from '@cirolee/tiny-motion'; 6 | import CodeBlock from '@/components/CodeBlock'; 7 | import { code, delayCode, typesCode } from './codes'; 8 | import ApiTable from '@/components/ApiTable'; 9 | import { propsRows } from './api'; 10 | import Tag from '@/ui/Tag'; 11 | import { Link } from 'react-router-dom'; 12 | export default function UseValueDoc() { 13 | const [value, controller] = useValue(0, 100, { 14 | duration: 5000, 15 | autoPlay: false, 16 | easing: 'easeOutCubic' 17 | }); 18 | const [value2, controller2] = useValue(0, 100, { 19 | duration: 5000, 20 | autoPlay: false, 21 | easing: 'easeOutCubic', 22 | delay: 1000 23 | }); 24 | return ( 25 |
26 | 27 | useValue 28 | 29 |

useValue is used to animate any number you want.

30 | 31 |
{value}
32 |

isPlaying: {controller.isPlaying.toString()}

33 |
34 | 37 | 40 | 43 | 46 |
47 |
48 | 49 |

50 | use delay to delay the animation. 51 |

52 | 53 |
{value2}
54 | 57 |
58 | 59 | 60 | Signature 61 | 62 | 63 | 64 | Types 65 | 66 | 67 | 68 | Props 69 | 70 | 71 | 72 | ReturnType 73 | 74 |
75 | 76 | ValueController 77 | {' '} 78 | see{' '} 79 | 80 | here 81 | 82 |
83 |
84 | 85 | useLineDraw 86 | 87 | 88 | useSpring 89 | 90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /packages/app/src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import CopyButton from '@/components/CopyButton'; 2 | import Button from '@/ui/Button'; 3 | import Heading from '@/ui/Heading'; 4 | import { useAnimate } from '@cirolee/tiny-motion'; 5 | import { useEffect } from 'react'; 6 | import { Link } from 'react-router-dom'; 7 | export default function Home() { 8 | const [ref, animate] = useAnimate(); 9 | useEffect(() => { 10 | animate( 11 | { 12 | offsetDistance: ['0%', '100%'] 13 | }, 14 | { 15 | duration: 10000, 16 | iterations: Infinity 17 | } 18 | ); 19 | }, []); 20 | return ( 21 |
22 | 23 | tiny-motion 24 | 25 |

The high-performance extension of Web Animation API for React Hooks

26 |
27 |
28 | npm install @cirolee/tiny-motion 29 | 30 |
31 |
32 |
33 |
34 | 37 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /packages/app/src/pages/presets/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef, useMemo } from 'react'; 2 | import { IconCode, IconSquareFilled, IconH1, IconPhotoFilled } from '@tabler/icons-react'; 3 | import { useMotion, presetMotionNames, type EaseFunctionType } from '@cirolee/tiny-motion'; 4 | import Button from '@/ui/Button'; 5 | import MotionList from '@/components/MotionList'; 6 | import PropertyList from '@/components/PropertyList'; 7 | import CodeModal from '@/components/CodeModal'; 8 | import img from '@/assets/images/dog-photo.jpg'; 9 | import { Link } from 'react-router-dom'; 10 | 11 | export default function Presets() { 12 | const [ref, motion] = useMotion(); 13 | 14 | const [target, setTarget] = useState<'text' | 'cube' | 'image'>('text'); 15 | const [showCode, setShowCode] = useState(false); 16 | const [motionName, setMotionName] = useState(presetMotionNames[0]); 17 | const [easingObj, setEasing] = useState<{ name: string; value: EaseFunctionType }>({ 18 | name: 'linear', 19 | value: 'linear' 20 | }); 21 | const [duration, setDuration] = useState(500); 22 | const [delay, setDelay] = useState(0); 23 | const [fill, setFill] = useState('none'); 24 | const [iterations, setIterations] = useState(1); 25 | const [direction, setDirection] = useState('normal'); 26 | const btnBoxRef = useRef(null); 27 | const animation = useRef(null); 28 | 29 | const handlePlay = () => { 30 | animation.current = motion(motionName, { 31 | fill, 32 | duration, 33 | delay, 34 | iterations, 35 | direction, 36 | easing: easingObj.value 37 | }); 38 | }; 39 | 40 | const code = useMemo(() => { 41 | const tpl = `motion('${motionName}', { 42 | fill: '${fill}', 43 | duration: ${duration}, 44 | delay: ${delay}, 45 | iterations: ${iterations}, 46 | direction: '${direction}', 47 | easing: EASING_FUNCTIONS.${easingObj.name} 48 | })`; 49 | return tpl; 50 | }, [fill, duration, delay, iterations, direction, easingObj, motionName]); 51 | 52 | useEffect(() => { 53 | animation.current?.cancel(); 54 | handlePlay(); 55 | }, [motionName, easingObj, iterations, direction, fill, duration, delay]); 56 | 57 | return ( 58 |
59 | 60 |
61 |
62 | {target === 'text' && tiny-motion} 63 | {target === 'cube' &&
} 64 | {target === 'image' && } 65 |
66 |
67 |
68 | 71 | 74 | 77 |
78 |
79 | 82 | 85 |
86 |
87 | setShowCode(false)} /> 88 |
89 | { 93 | if (duration) { 94 | setDuration(duration); 95 | } else { 96 | setDuration(500); 97 | } 98 | }} 99 | onSetEasing={(key, value) => { 100 | setEasing({ name: key, value }); 101 | }} 102 | onSetFill={setFill} 103 | onSetIterations={setIterations} 104 | /> 105 |
106 | preset playground is unavailable under 1024 screen 107 |
108 | you can change screen size or visit{' '} 109 | 110 | Docs 111 | {' '} 112 | page 113 |
114 |
115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /packages/app/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter, Navigate, type RouteObject } from 'react-router-dom'; 2 | import Layout from '@/layout'; 3 | import { lazy } from 'react'; 4 | 5 | export type CustomRouteObject = RouteObject & { 6 | meta?: { 7 | name: string; 8 | visible?: boolean; 9 | level?: string; 10 | order?: number; 11 | type?: string; 12 | }; 13 | }; 14 | 15 | export const router = createBrowserRouter([ 16 | { 17 | path: '/', 18 | id: 'root', 19 | element: , 20 | children: [ 21 | { 22 | path: '/', 23 | id: 'home', 24 | Component: lazy(() => import('@/pages/home')) 25 | }, 26 | { 27 | path: '/docs', 28 | id: 'docs', 29 | Component: lazy(() => import('@/pages/docs')), 30 | children: [ 31 | { 32 | index: true, 33 | element: 34 | }, 35 | { 36 | path: '/docs/overview', 37 | id: 'overview', 38 | Component: lazy(() => import('@/pages/docs/overview')), 39 | meta: { 40 | name: 'overview', 41 | visible: true, 42 | type: 'hook' 43 | } 44 | }, 45 | { 46 | path: 'use-animate', 47 | id: 'use-animate', 48 | Component: lazy(() => import('@/pages/docs/use-animate.doc')), 49 | meta: { 50 | name: 'useAnimate', 51 | type: 'hook' 52 | } 53 | }, 54 | { 55 | path: 'use-motion', 56 | id: 'use-motion', 57 | Component: lazy(() => import('../pages/docs/use-motion.doc')), 58 | meta: { 59 | name: 'useMotion', 60 | type: 'hook' 61 | } 62 | }, 63 | { 64 | path: 'use-group', 65 | id: 'use-group', 66 | Component: lazy(() => import('../pages/docs/use-group.doc')), 67 | meta: { 68 | name: 'useGroup', 69 | type: 'hook' 70 | } 71 | }, 72 | { 73 | path: 'use-multiple', 74 | id: 'use-multiple', 75 | Component: lazy(() => import('../pages/docs/use-multiple.doc')), 76 | meta: { 77 | name: 'useMultiple', 78 | type: 'hook' 79 | } 80 | }, 81 | { 82 | path: 'use-in-view', 83 | id: 'use-in-view', 84 | Component: lazy(() => import('../pages/docs/use-in-view.doc')), 85 | meta: { 86 | name: 'useInView', 87 | type: 'hook' 88 | } 89 | }, 90 | { 91 | path: 'use-line-draw', 92 | id: 'use-line-draw', 93 | Component: lazy(() => import('../pages/docs/use-line-draw.doc')), 94 | meta: { 95 | name: 'useLineDraw', 96 | type: 'hook' 97 | } 98 | }, 99 | { 100 | path: 'use-value', 101 | id: 'use-value', 102 | Component: lazy(() => import('../pages/docs/use-value.doc')), 103 | meta: { 104 | name: 'useValue', 105 | level: 'js', 106 | type: 'hook' 107 | } 108 | }, 109 | { 110 | path: 'use-spring', 111 | id: 'use-spring', 112 | Component: lazy(() => import('../pages/docs/use-spring.doc')), 113 | meta: { 114 | name: 'useSpring', 115 | level: 'js', 116 | type: 'hook' 117 | } 118 | }, 119 | { 120 | path: 'animate', 121 | id: 'animate', 122 | Component: lazy(() => import('../pages/docs/animate.doc')), 123 | meta: { 124 | name: 'animate', 125 | type: 'universal' 126 | } 127 | } 128 | ], 129 | meta: { 130 | name: 'docs', 131 | visible: true, 132 | order: 1 133 | } 134 | }, 135 | { 136 | path: '/presets', 137 | id: 'presets', 138 | Component: lazy(() => import('@/pages/presets')) 139 | } 140 | ] 141 | } 142 | ] as CustomRouteObject[]); 143 | -------------------------------------------------------------------------------- /packages/app/src/styles/animation.css: -------------------------------------------------------------------------------- 1 | @theme { 2 | --animate-fade-in: fade-in 0.15s ease-in; 3 | --animate-fade-out: fade-out 0.15s ease-out; 4 | 5 | --animate-slide-in-from-top: slide-in-from-top 0.2s linear; 6 | --animate-slide-out-to-top: slide-out-to-top 0.2s linear; 7 | --animate-slide-in-from-right: slide-in-from-right 0.2s linear; 8 | --animate-slide-out-to-right: slide-out-to-right 0.2s linear; 9 | --animate-slide-in-from-bottom: slide-in-from-bottom 0.2s linear; 10 | --animate-slide-out-to-bottom: slide-out-to-bottom 0.2s linear; 11 | --animate-slide-in-from-left: slide-in-from-left 0.2s linear; 12 | --animate-slide-out-to-left: slide-out-to-left 0.2s linear; 13 | 14 | --animate-zoom-fade-in: zoom-fade-in 0.15s ease-in; 15 | --animate-zoom-fade-out: zoom-fade-out 0.15s ease-out; 16 | 17 | --animate-accordion-slide-down: accordion-slide-down 0.2s ease-in; 18 | --animate-accordion-slide-up: accordion-slide-up 0.2s ease-out; 19 | --animate-collapsible-slide-down: collapsible-slide-down 0.2s ease-in; 20 | --animate-collapsible-slide-up: collapsible-slide-up 0.2s ease-out; 21 | 22 | --animate-flicker: flicker 2s infinite ease; 23 | --animate-shimmer: shimmer 2s infinite linear; 24 | 25 | /* fade animation */ 26 | @keyframes fade-in { 27 | from { 28 | opacity: 0; 29 | } 30 | to { 31 | opacity: 1; 32 | } 33 | } 34 | @keyframes fade-out { 35 | from { 36 | opacity: 1; 37 | } 38 | to { 39 | opacity: 0; 40 | } 41 | } 42 | /* slide animation */ 43 | @keyframes slide-in-from-top { 44 | from { 45 | transform: translateY(-100%); 46 | } 47 | to { 48 | transform: translateY(0); 49 | } 50 | } 51 | @keyframes slide-out-to-top { 52 | to { 53 | transform: translateY(-100%); 54 | } 55 | } 56 | @keyframes slide-in-from-right { 57 | from { 58 | transform: translateX(100%); 59 | } 60 | to { 61 | transform: translateX(0); 62 | } 63 | } 64 | @keyframes slide-out-to-right { 65 | to { 66 | transform: translateX(100%); 67 | } 68 | } 69 | @keyframes slide-in-from-bottom { 70 | from { 71 | transform: translateY(100%); 72 | } 73 | to { 74 | transform: translateY(0); 75 | } 76 | } 77 | @keyframes slide-out-to-bottom { 78 | to { 79 | transform: translateY(100%); 80 | } 81 | } 82 | @keyframes slide-in-from-left { 83 | from { 84 | transform: translateX(-100%); 85 | } 86 | to { 87 | transform: translateX(0); 88 | } 89 | } 90 | @keyframes slide-out-to-left { 91 | to { 92 | transform: translateX(-100%); 93 | } 94 | } 95 | @keyframes zoom-fade-in { 96 | from { 97 | opacity: 0; 98 | transform: scale(0.94); 99 | } 100 | to { 101 | opacity: 1; 102 | transform: scale(1); 103 | } 104 | } 105 | @keyframes zoom-fade-out { 106 | from { 107 | opacity: 1; 108 | transform: scale(1); 109 | } 110 | to { 111 | opacity: 0; 112 | transform: scale(0.94); 113 | } 114 | } 115 | 116 | /* accordion keyframes */ 117 | @keyframes accordion-slide-down { 118 | from { 119 | height: 0; 120 | opacity: 0; 121 | } 122 | 90% { 123 | opacity: 1; 124 | } 125 | to { 126 | opacity: 1; 127 | height: var(--radix-accordion-content-height); 128 | } 129 | } 130 | 131 | @keyframes accordion-slide-up { 132 | from { 133 | opacity: 1; 134 | height: var(--radix-accordion-content-height); 135 | } 136 | 90% { 137 | opacity: 0; 138 | } 139 | to { 140 | height: 0; 141 | opacity: 0; 142 | } 143 | } 144 | 145 | /* collapsible keyframes */ 146 | @keyframes collapsible-slide-down { 147 | from { 148 | height: 0; 149 | opacity: 0; 150 | } 151 | 90% { 152 | opacity: 1; 153 | } 154 | to { 155 | opacity: 1; 156 | height: var(--radix-collapsible-content-height); 157 | } 158 | } 159 | 160 | @keyframes collapsible-slide-up { 161 | from { 162 | opacity: 1; 163 | height: var(--radix-collapsible-content-height); 164 | } 165 | 90% { 166 | opacity: 0; 167 | } 168 | to { 169 | height: 0; 170 | opacity: 0; 171 | } 172 | } 173 | 174 | @keyframes flicker { 175 | 0% { 176 | background-position: 100% 50%; 177 | } 178 | 100% { 179 | background-position: 0 50%; 180 | } 181 | } 182 | @keyframes shimmer { 183 | 50% { 184 | opacity: 0.5; 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /packages/app/src/styles/theme.css: -------------------------------------------------------------------------------- 1 | @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); 2 | 3 | /* basic semantic colors base on tailwindcss colors */ 4 | :root { 5 | --background: #fff; 6 | --foreground: oklch(0.269 0 0); 7 | --line: oklch(0.922 0 0); 8 | --description: oklch(70.8% 0 0); 9 | /* semantic z-index */ 10 | --loading: 60; 11 | --popup: 50; 12 | --tooltip: 40; 13 | --header: 30; 14 | } 15 | 16 | [data-theme='dark'] { 17 | --background: oklch(0.269 0 0); 18 | --foreground: #fafafa; 19 | --line: oklch(0.371 0 0); 20 | --description: oklch(55.6% 0 0); 21 | } 22 | 23 | @theme { 24 | --color-foreground: var(--foreground); 25 | --color-background: var(--background); 26 | --color-line: var(--line); 27 | --color-description: var(--description); 28 | 29 | --color-primary: var(--color-blue-500); 30 | --color-primary-active: var(--color-blue-600); 31 | --color-danger: var(--color-red-500); 32 | --color-danger-active: var(--color-red-600); 33 | --color-secondary: var(--color-indigo-500); 34 | --color-secondary-active: var(--color-indigo-600); 35 | --color-warning: var(--color-orange-500); 36 | --color-warning-active: var(--color-orange-600); 37 | } 38 | 39 | @utility streamer { 40 | background-image: radial-gradient(50% 50% at 50% 50%, #f6b065 0%, #f1a554 31%, #b97731 63%, #985b1b 100%); 41 | } 42 | 43 | @utility offset-rect { 44 | offset-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%); 45 | } 46 | 47 | @utility polka { 48 | background-color: transparent; 49 | background-image: radial-gradient(#383838 0.7px, transparent 0.7px), radial-gradient(#383838 0.7px, #f9fefe 0.7px); 50 | background-size: 28px 28px; 51 | background-position: 52 | 0 0, 53 | 14px 14px; 54 | 55 | @variant dark { 56 | background-image: radial-gradient(#4e4e4e 0.7px, transparent 0.7px), radial-gradient(#555454 0.7px, #2b2c2c 0.7px); 57 | } 58 | } 59 | 60 | body { 61 | color: var(--foreground); 62 | background-color: var(--background); 63 | } 64 | -------------------------------------------------------------------------------- /packages/app/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type ThemeMode = 'light' | 'dark' | 'system'; 2 | -------------------------------------------------------------------------------- /packages/app/src/ui/Button/buttonVariants.ts: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | primary: 'bg-primary not-disabled:active:bg-primary-active focus-visible:ring-primary/50', 3 | danger: 'bg-danger not-disabled:active:bg-danger-active focus-visible:ring-danger/50', 4 | secondary: 'bg-secondary not-disabled:active:bg-secondary-active focus-visible:ring-secondary/50', 5 | warning: 'bg-warning not-disabled:active:bg-warning-active focus-visible:ring-warning/50', 6 | neutral: `bg-neutral-200 not-disabled:active:bg-neutral-300 focus-visible:ring-neutral-300/40 7 | dark:bg-neutral-700 dark:not-disabled:active:bg-[#343333] dark:focus-visible:ring-neutral-700/60` 8 | }; 9 | 10 | export interface ComputedVariants { 11 | colors: keyof typeof colors; 12 | variant: 'bordered' | 'light'; 13 | className: string; 14 | } 15 | 16 | export const colorsBorderedVariants: ComputedVariants[] = [ 17 | { 18 | colors: 'primary', 19 | variant: 'bordered', 20 | className: 'border-primary text-primary not-disabled:hover:bg-primary/10 not-disabled:active:bg-primary/20' 21 | }, 22 | { 23 | colors: 'secondary', 24 | variant: 'bordered', 25 | className: 'border-secondary text-secondary not-disabled:hover:bg-secondary/10 not-disabled:active:bg-secondary/20' 26 | }, 27 | { 28 | colors: 'warning', 29 | variant: 'bordered', 30 | className: 'border-warning text-warning not-disabled:hover:bg-warning/10 not-disabled:active:bg-warning/20' 31 | }, 32 | { 33 | colors: 'danger', 34 | variant: 'bordered', 35 | className: 'border-danger text-danger not-disabled:hover:bg-danger/10 not-disabled:active:bg-danger/20' 36 | }, 37 | { 38 | colors: 'neutral', 39 | variant: 'bordered', 40 | className: 'border-neutral-300/70 not-disabled:hover:bg-neutral-500/15 dark:not-disabled:active:bg-neutral-700/30 not-disabled:active:bg-neutral-300/30 dark:border-neutral-700' 41 | } 42 | ]; 43 | 44 | export const colorsLightVariants: ComputedVariants[] = [ 45 | { 46 | colors: 'primary', 47 | variant: 'light', 48 | className: 'text-primary not-disabled:hover:bg-primary/15 not-disabled:active:bg-primary/25' 49 | }, 50 | { 51 | colors: 'secondary', 52 | variant: 'light', 53 | className: 'text-secondary not-disabled:hover:bg-secondary/15 not-disabled:active:bg-secondary/25' 54 | }, 55 | { 56 | colors: 'warning', 57 | variant: 'light', 58 | className: 'text-warning not-disabled:hover:bg-warning/15 not-disabled:active:bg-warning/25' 59 | }, 60 | { 61 | colors: 'danger', 62 | variant: 'light', 63 | className: 'text-danger not-disabled:hover:bg-danger/10 not-disabled:active:bg-danger/20' 64 | }, 65 | { 66 | colors: 'neutral', 67 | variant: 'light', 68 | className: 'text-foreground not-disabled:hover:bg-neutral-500/15 not-disabled:active:bg-neutral-400/30 not-disabled:dark:active:bg-neutral-700/20' 69 | } 70 | ]; 71 | -------------------------------------------------------------------------------- /packages/app/src/ui/Button/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cn } from '@/lib/utils'; 4 | import { cva, type VariantProps } from 'class-variance-authority'; 5 | import { colors, colorsBorderedVariants, colorsLightVariants } from './buttonVariants'; 6 | 7 | const button = cva(`inline-flex items-center justify-center transition not-disabled:hover:opacity-80 box-border outline-none focus-visible:ring-3 focus-visible:transition-none`, { 8 | variants: { 9 | colors, 10 | size: { 11 | xs: 'px-2 rounded-sm h-6 text-xs', 12 | sm: 'px-3 rounded h-8 text-sm', 13 | md: 'px-4 rounded-md h-10', 14 | lg: 'px-5 rounded-lg h-12' 15 | }, 16 | 17 | variant: { 18 | solid: 'text-white', 19 | light: 'text-foreground bg-transparent dark:bg-transparent', 20 | bordered: 'border bg-transparent dark:bg-transparent' 21 | }, 22 | icon: { 23 | true: 'aspect-square p-0' 24 | }, 25 | disabled: { 26 | true: 'cursor-not-allowed opacity-50' 27 | }, 28 | loading: { 29 | true: 'cursor-not-default opacity-50' 30 | }, 31 | pill: { 32 | true: 'rounded-full' 33 | } 34 | }, 35 | compoundVariants: [ 36 | ...colorsBorderedVariants, 37 | ...colorsLightVariants, 38 | { 39 | variant: 'solid', 40 | colors: 'neutral', 41 | className: 'text-foreground' 42 | } 43 | ], 44 | defaultVariants: { 45 | size: 'md', 46 | colors: 'primary', 47 | variant: 'solid' 48 | } 49 | }); 50 | 51 | type ButtonVariants = VariantProps; 52 | interface ButtonProps extends React.ButtonHTMLAttributes, Omit { 53 | asChild?: boolean; 54 | ref?: React.Ref; 55 | } 56 | export default function Button(props: ButtonProps) { 57 | const { colors, size, icon, variant, disabled, loading, pill, className, asChild, children, ref, ...rest } = props; 58 | const Component = asChild ? Slot : 'button'; 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /packages/app/src/ui/Divider.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | const divider = cva('relative inline-flex items-center before:bg-line after:bg-line', { 4 | variants: { 5 | orientation: { 6 | horizontal: 'w-full flex-row before:w-full after:w-full before:h-px after:h-px my-4', 7 | vertical: 'flex-col justify-center h-full before:h-full after:h-full before:w-px after:w-px mx-4' 8 | } 9 | }, 10 | defaultVariants: { 11 | orientation: 'horizontal' 12 | } 13 | }); 14 | const content = cva('whitespace-nowrap text-sm', { 15 | variants: { 16 | orientation: { 17 | horizontal: 'px-3', 18 | vertical: 'py-3' 19 | } 20 | }, 21 | defaultVariants: { 22 | orientation: 'horizontal' 23 | } 24 | }); 25 | 26 | type DividerVariants = VariantProps; 27 | interface DividerProps extends React.HTMLAttributes, DividerVariants {} 28 | export default function Divider({ className, orientation, children, ...props }: DividerProps) { 29 | return ( 30 |
31 | {children ? {children} : null} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/app/src/ui/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { cva } from 'class-variance-authority'; 2 | import { cn } from '@/lib/utils'; 3 | 4 | const heading = cva('font-normal', { 5 | variants: { 6 | as: { 7 | h1: 'text-[2.5rem]/[1.3] tracking-[0.5px] font-bold', 8 | h2: 'text-[2rem]/[1.4] tracking-[0.3px] font-semibold', 9 | h3: 'text-[1.75rem]/[1.5] tracking-[0.2px] font-semibold', 10 | h4: 'text-[1.5rem]/[1,5] tracking-[0.1px] font-medium', 11 | h5: 'text-[1,25rem]/[1.6] tracking-0 font-medium', 12 | h6: 'text-[1rem]/[1.5] -tracking-[0.2px]' 13 | } 14 | } 15 | }); 16 | 17 | interface HeadingProps { 18 | as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 19 | className?: string; 20 | style?: React.CSSProperties; 21 | children?: React.ReactNode; 22 | ref?: React.Ref; 23 | } 24 | export default function Heading({ as: Tag, className, style, children, ref }: HeadingProps) { 25 | const getLevel = () => { 26 | const match = Tag.match(/d+/g); 27 | return match ? Number(match[0]) : 1; 28 | }; 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/app/src/ui/HoverCard.tsx: -------------------------------------------------------------------------------- 1 | import { HoverCard as HoverCardPrimitive } from 'radix-ui'; 2 | import { cva } from 'class-variance-authority'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | const hoverCardContent = cva( 6 | `rounded-md bg-background drop-shadow-[0_-1px_0,1px_0_0,0_1px_0,-1px_0_0] drop-shadow-line p-2 z-(--popup) transition-all origin-[--radix-hover-card-content-transform-origin] 7 | animate-zoom-fade-in data-[state=closed]:animate-zoom-fade-out outline-none` 8 | ); 9 | 10 | interface HoverCardProps extends React.ComponentPropsWithRef, React.ComponentPropsWithRef { 11 | trigger: React.ReactNode; 12 | hiddenArrow?: boolean; 13 | } 14 | export default function HoverCard({ trigger, hiddenArrow, defaultOpen, open, sideOffset = 5, alignOffset = 1, onOpenChange, openDelay, closeDelay, className, children, ...props }: HoverCardProps) { 15 | return ( 16 | 17 | {trigger} 18 | 19 | 20 | {children} 21 | {hiddenArrow ? null : } 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/app/src/ui/Link.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { cn } from '@/lib/utils'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | const link = cva( 6 | `inline-flex items-center gap-0.5 text-secondary transition-colors outline-none leading-[1em] 7 | not-data-disabled:focus-visible:ring-secondary/50 not-data-disabled:focus-visible:ring-3 not-data-disabled:focus-visible:rounded-xs`, 8 | { 9 | variants: { 10 | underline: { 11 | true: 'shadow-[0_1px_0_0] shadow-secondary' 12 | }, 13 | disabled: { 14 | true: 'cursor-not-allowed opacity-50', 15 | false: 'hover:opacity-80' 16 | } 17 | }, 18 | defaultVariants: { 19 | underline: false, 20 | disabled: false 21 | } 22 | } 23 | ); 24 | 25 | type LinkVariants = VariantProps; 26 | interface LinkProps extends React.ComponentPropsWithRef<'a'>, LinkVariants {} 27 | export default function Link({ className, underline, disabled, target, onClick, ...props }: LinkProps) { 28 | return ( 29 | { 38 | if (disabled) { 39 | e.preventDefault(); 40 | e.stopPropagation(); 41 | return; 42 | } 43 | onClick?.(e); 44 | }} 45 | {...props} 46 | /> 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/app/src/ui/Loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEffect, useRef, useState } from 'react'; 3 | import { IconLoader2 } from '@tabler/icons-react'; 4 | import { cn } from '@/lib/utils'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | 7 | const mask = cva('absolute inset-0', { 8 | variants: { 9 | backdrop: { 10 | opaque: 'bg-black/45 dark:bg-black/55', 11 | blur: 'bg-black/45 dark:bg-black/55 backdrop-blur-sm', 12 | transparent: 'bg-transparent' 13 | } 14 | }, 15 | defaultVariants: { 16 | backdrop: 'opaque' 17 | } 18 | }); 19 | interface LoadingProps extends VariantProps { 20 | className?: string; 21 | style?: React.CSSProperties; 22 | open?: boolean; 23 | children?: React.ReactNode; 24 | ref?: React.RefObject; 25 | isFullscreen?: boolean; 26 | indicator?: React.ReactNode; 27 | } 28 | export default function Loading({ className, open, backdrop, indicator, isFullscreen, children, ...props }: LoadingProps) { 29 | const [visible, setVisible] = useState(false); 30 | const maskRef = useRef(null); 31 | const animation = useRef(null); 32 | useEffect(() => { 33 | if (open) { 34 | setVisible(true); 35 | document.body.setAttribute('style', 'overflow: hidden'); 36 | if (maskRef.current) { 37 | animation.current = maskRef.current.animate( 38 | { opacity: [0, 1] }, 39 | { 40 | duration: 200, 41 | fill: 'both', 42 | easing: 'linear' 43 | } 44 | ); 45 | } 46 | } else if (maskRef.current && animation.current) { 47 | animation.current.reverse(); 48 | animation.current.onfinish = () => { 49 | setVisible(false); 50 | document.body.removeAttribute('style'); 51 | }; 52 | } 53 | }, [open]); 54 | return ( 55 |
56 | {children} 57 | {visible || open ? ( 58 |
59 |
60 |
61 | {indicator ? <>{indicator} : } 62 |
63 |
64 | ) : null} 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /packages/app/src/ui/Slider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Slider as SliderPrimitive } from 'radix-ui'; 3 | import { cn } from '@/lib/utils'; 4 | import { cva, type VariantProps } from 'class-variance-authority'; 5 | 6 | const sliderRoot = cva( 7 | `relative flex touch-none select-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 8 | data-[orientation=horizontal]:items-center data-[orientation=vertical]:w-fit data-[orientation=vertical]:items-start 9 | data-[orientation=vertical]:justify-center` 10 | ); 11 | const sliderTrack = cva('rounded-full relative grow bg-neutral-200 dark:bg-neutral-700 data-[orientation=vertical]:h-full', { 12 | variants: { 13 | size: { 14 | sm: 'data-[orientation=horizontal]:h-1 data-[orientation=vertical]:w-1', 15 | md: 'data-[orientation=horizontal]:h-2 data-[orientation=vertical]:w-2', 16 | lg: 'data-[orientation=horizontal]:h-3 data-[orientation=vertical]:w-3' 17 | } 18 | }, 19 | defaultVariants: { 20 | size: 'md' 21 | } 22 | }); 23 | const sliderRange = cva( 24 | `absolute data-[orientation=vertical]:w-full data-[orientation=horizontal]:h-full 25 | data-[orientation=vertical]:rounded-b-full data-[orientation=horizontal]:rounded-l-full`, 26 | { 27 | variants: { 28 | colors: { 29 | primary: 'bg-primary', 30 | secondary: 'bg-secondary', 31 | warning: 'bg-warning', 32 | danger: 'bg-danger', 33 | neutral: 'bg-neutral-400 dark:bg-neutral-300' 34 | } 35 | }, 36 | defaultVariants: { 37 | colors: 'primary' 38 | } 39 | } 40 | ); 41 | 42 | const sliderThumb = cva('block rounded-full bg-background border-2 outline-none focus-visible:ring-2', { 43 | variants: { 44 | size: { 45 | sm: 'size-4', 46 | md: 'size-5', 47 | lg: 'size-6' 48 | }, 49 | colors: { 50 | primary: 'border-primary not-[data-disabled]:focus-visible:ring-primary/50', 51 | secondary: 'border-secondary focus-visible:ring-secondary/50', 52 | warning: 'border-warning not-[data-disabled]:focus-visible:ring-warning/50', 53 | danger: 'border-danger not-[data-disabled]:focus-visible:ring-danger/50', 54 | neutral: 'border-neutral-400 dark:border-neutral-300 not-[data-disabled]:focus-visible:ring-neutral-500/50' 55 | } 56 | }, 57 | defaultVariants: { 58 | size: 'md', 59 | colors: 'primary' 60 | } 61 | }); 62 | 63 | type SliderThumbVariants = VariantProps; 64 | interface SliderProps extends React.ComponentPropsWithRef, SliderThumbVariants {} 65 | export default function Slider({ size, defaultValue, value, colors, className, ...props }: SliderProps) { 66 | const currentValue = value ?? defaultValue ?? []; 67 | return ( 68 | 69 | 70 | 71 | 72 | {currentValue.map((_, index) => ( 73 | 74 | ))} 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /packages/app/src/ui/Table.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | interface TableProps extends React.ComponentPropsWithRef<'table'> { 4 | fixedHeader?: boolean; 5 | } 6 | export function Table({ fixedHeader, className, ...props }: TableProps) { 7 | return ( 8 |
16 | 17 | 18 | ); 19 | } 20 | 21 | export function TableHeader({ className, ...props }: React.ComponentPropsWithRef<'thead'>) { 22 | return ; 23 | } 24 | export function TableHeaderCell({ className, ...props }: React.ComponentPropsWithRef<'th'>) { 25 | return ( 26 | ; 38 | } 39 | 40 | export function TableCell({ className, ...props }: React.ComponentPropsWithRef<'td'>) { 41 | return ; 49 | } 50 | -------------------------------------------------------------------------------- /packages/app/src/ui/Tag.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { cn } from '@/lib/utils'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | const tag = cva('min-h-6 inline-flex items-center px-2 text-sm rounded-sm outline-none focus-visible:ring-3', { 5 | variants: { 6 | colors: { 7 | primary: 'text-primary border-primary bg-blue-50 dark:border-blue-800 dark:text-foreground dark:bg-blue-950 ring-primary/20', 8 | secondary: 'text-secondary border-secondary bg-indigo-50 dark:border-indigo-800 dark:text-foreground dark:bg-indigo-950 ring-secondary/20', 9 | warning: 'text-warning border-warning bg-orange-50 dark:border-yellow-800 dark:text-foreground dark:bg-yellow-950 ring-warning/20', 10 | danger: 'text-danger border-danger bg-red-50 dark:border-red-800 dark:text-foreground dark:bg-red-950 ring-danger/20', 11 | neutral: 'text-foreground border-neutral-300 bg-neutral-100 dark:border-neutral-600 dark:bg-neutral-700 ring-neutral-300/40 dark:ring-neutral-700/60' 12 | }, 13 | pill: { 14 | true: 'rounded-full' 15 | }, 16 | bordered: { 17 | true: 'border' 18 | } 19 | }, 20 | defaultVariants: { 21 | colors: 'primary' 22 | } 23 | }); 24 | 25 | type TagVariants = VariantProps; 26 | interface TagProps extends React.HTMLAttributes, TagVariants { 27 | className?: string; 28 | style?: React.CSSProperties; 29 | children?: React.ReactNode; 30 | ref?: React.Ref; 31 | } 32 | export default function Tag({ className, colors, bordered, pill, children, ref, ...props }: TagProps) { 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /packages/app/src/ui/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Tooltip as TooltipPrimitive } from 'radix-ui'; 3 | import { cn } from '@/lib/utils'; 4 | import { cva } from 'class-variance-authority'; 5 | 6 | const tooltipContent = cva( 7 | `bg-foreground text-sm text-background rounded-md px-2 py-1 z-(--tooltip) transition-all origin-[--radix-tooltip-content-transform-origin] 8 | animate-zoom-fade-in data-[state=closed]:animate-zoom-fade-out` 9 | ); 10 | type TooltipPrimitiveProvider = Omit, 'children'>; 11 | type TooltipPrimitiveRoot = React.ComponentPropsWithRef; 12 | 13 | interface TooltipProps extends TooltipPrimitiveProvider, TooltipPrimitiveRoot { 14 | trigger: React.ReactNode; 15 | className?: string; 16 | side?: 'top' | 'right' | 'bottom' | 'left'; 17 | align?: 'start' | 'center' | 'end'; 18 | sideOffset?: number; 19 | alignOffset?: number; 20 | hiddenArrow?: boolean; 21 | style?: React.CSSProperties; 22 | } 23 | 24 | export default function ToolTip({ 25 | trigger, 26 | side = 'top', 27 | align = 'center', 28 | sideOffset = 5, 29 | alignOffset = 1, 30 | hiddenArrow, 31 | delayDuration, 32 | skipDelayDuration, 33 | disableHoverableContent, 34 | children, 35 | className, 36 | ...props 37 | }: TooltipProps) { 38 | return ( 39 | 40 | 41 | {trigger} 42 | 43 | 44 | {children} 45 | {!hiddenArrow ? : null} 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/app/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function copyToClipboard(text: string) { 2 | return navigator.clipboard.writeText(text); 3 | } 4 | -------------------------------------------------------------------------------- /packages/app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "baseUrl": "./", 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "erasableSyntaxOnly": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "noUncheckedSideEffectImports": true 29 | }, 30 | "include": ["src"] 31 | } 32 | -------------------------------------------------------------------------------- /packages/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /packages/app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | import tailwindcss from '@tailwindcss/vite'; 4 | import path from 'path'; 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), tailwindcss()], 8 | resolve: { 9 | alias: { 10 | '@': path.resolve(__dirname, './src') 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /packages/lib/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CiroLee 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 | -------------------------------------------------------------------------------- /packages/lib/hooks/animate.ts: -------------------------------------------------------------------------------- 1 | import { AnimationOptions, DOMElement, Keyframes } from './types'; 2 | import type { MotionName } from './useMotion'; 3 | import { checkDuration, combineKeyframeByMotion } from './utils'; 4 | 5 | interface AnimationProps { 6 | target: DOMElement; 7 | keyframes?: Keyframes; 8 | motion?: MotionName; 9 | options?: AnimationOptions; 10 | } 11 | /** 12 | * @description universal animation function, target is DOM element, and it also has access to motion presets 13 | * @param {AnimationProps} props 14 | * @returns {Animation} 15 | */ 16 | export function animate(props: AnimationProps): Animation { 17 | const { target, keyframes, motion, options } = props; 18 | checkDuration(options); 19 | const _keyframes = combineKeyframeByMotion(keyframes, motion); 20 | return target.animate(_keyframes, options); 21 | } 22 | -------------------------------------------------------------------------------- /packages/lib/hooks/controller.ts: -------------------------------------------------------------------------------- 1 | import type { Animations } from './types'; 2 | export function play(animations: Animations, cb?: (...args: any[]) => void) { 3 | if (animations.every(Boolean)) { 4 | animations.forEach((animation) => { 5 | animation!.playbackRate = 1; 6 | animation!.play(); 7 | }); 8 | allFinish(animations, 'play', cb); 9 | } 10 | } 11 | 12 | export function allFinish(animations: Animations, trigger: 'play' | 'reverse', cb?: (...args: any[]) => void) { 13 | if (animations.every(Boolean)) { 14 | Promise.all(animations.map((animation) => animation!.finished)) 15 | .then(() => { 16 | cb?.(trigger); 17 | }) 18 | .catch((error) => { 19 | // TODO maybe there are some other kinds of errors 20 | console.warn('[animations maybe aborted]', error); 21 | }); 22 | } 23 | } 24 | 25 | export function pause(animations: Animations) { 26 | animations.forEach((animation) => { 27 | if (animation) { 28 | animation.pause(); 29 | } 30 | }); 31 | } 32 | 33 | export function cancel(animations: Animations) { 34 | animations.forEach((animation) => animation?.cancel()); 35 | } 36 | 37 | export function reverse(animations: Animations, cb?: (...args: any[]) => void) { 38 | if (animations.every(Boolean)) { 39 | animations.forEach((animation) => { 40 | animation!.playbackRate = -1; 41 | animation!.play(); 42 | }); 43 | allFinish(animations, 'reverse', cb); 44 | } 45 | } 46 | 47 | export const resume = (animations: Animations) => { 48 | animations.forEach((animation) => { 49 | if (animation) { 50 | animation.play(); 51 | } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/lib/hooks/createSpring.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-condition */ 2 | interface CreateSpringParams { 3 | mass: number; 4 | stiffness: number; 5 | damping: number; 6 | velocity: number; 7 | } 8 | type CreateSpringReturn = [number, (t: number) => number]; 9 | 10 | // this algorithm refers to: https://linear-easing-generator.netlify.app/ 11 | export function createSpring({ mass, stiffness, damping, velocity }: CreateSpringParams): CreateSpringReturn { 12 | const w0 = Math.sqrt(stiffness / mass); 13 | const zeta = damping / (2 * Math.sqrt(stiffness * mass)); 14 | const wd = zeta < 1 ? w0 * Math.sqrt(1 - zeta * zeta) : 0; 15 | const b = zeta < 1 ? (zeta * w0 + -velocity) / wd : -velocity + w0; 16 | 17 | function solver(t: number): number { 18 | if (zeta < 1) { 19 | t = Math.exp(-t * zeta * w0) * (1 * Math.cos(wd * t) + b * Math.sin(wd * t)); 20 | } else { 21 | t = (1 + b * t) * Math.exp(-t * w0); 22 | } 23 | return 1 - t; 24 | } 25 | 26 | const duration = (() => { 27 | const step = 1 / 6; 28 | let time = 0; 29 | while (true) { 30 | if (Math.abs(1 - solver(time)) < 0.001) { 31 | const restStart = time; 32 | let restSteps = 1; 33 | while (true) { 34 | time += step; 35 | if (Math.abs(1 - solver(time)) >= 0.001) break; 36 | restSteps++; 37 | if (restSteps === 16) return restStart; 38 | } 39 | } 40 | time += step; 41 | } 42 | })(); 43 | 44 | return [duration * 1000, solver]; 45 | } 46 | -------------------------------------------------------------------------------- /packages/lib/hooks/easingAlgorithm.ts: -------------------------------------------------------------------------------- 1 | function linear(x: number): number { 2 | return x; 3 | } 4 | // cubic 5 | function easeInCubic(x: number): number { 6 | return x * x * x; 7 | } 8 | function easeOutCubic(x: number): number { 9 | return 1 - Math.pow(1 - x, 3); 10 | } 11 | function easeInOutCubic(x: number): number { 12 | return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; 13 | } 14 | // circ 15 | function easeInCirc(x: number): number { 16 | return 1 - Math.sqrt(1 - Math.pow(x, 2)); 17 | } 18 | function easeOutCirc(x: number): number { 19 | return Math.sqrt(1 - Math.pow(x - 1, 2)); 20 | } 21 | function easeInOutCirc(x: number): number { 22 | return x < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2; 23 | } 24 | // quint 25 | function easeInQuint(x: number): number { 26 | return x * x * x * x * x; 27 | } 28 | function easeOutQuint(x: number): number { 29 | return 1 - Math.pow(1 - x, 5); 30 | } 31 | function easeInOutQuint(x: number): number { 32 | return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2; 33 | } 34 | // sine 35 | function easeInSine(x: number): number { 36 | return 1 - Math.cos((x * Math.PI) / 2); 37 | } 38 | function easeOutSine(x: number): number { 39 | return Math.sin((x * Math.PI) / 2); 40 | } 41 | function easeInOutSine(x: number): number { 42 | return -(Math.cos(Math.PI * x) - 1) / 2; 43 | } 44 | // quad 45 | function easeInQuad(x: number): number { 46 | return x * x; 47 | } 48 | function easeOutQuad(x: number): number { 49 | return 1 - (1 - x) * (1 - x); 50 | } 51 | function easeInOutQuad(x: number): number { 52 | return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; 53 | } 54 | // quart 55 | function easeInQuart(x: number): number { 56 | return x * x * x * x; 57 | } 58 | function easeOutQuart(x: number): number { 59 | return 1 - Math.pow(1 - x, 4); 60 | } 61 | function easeInOutQuart(x: number): number { 62 | return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2; 63 | } 64 | // expo 65 | function easeInExpo(x: number): number { 66 | return x === 0 ? 0 : Math.pow(2, 10 * x - 10); 67 | } 68 | function easeOutExpo(x: number): number { 69 | return x === 1 ? 1 : 1 - Math.pow(2, -10 * x); 70 | } 71 | function easeInOutExpo(x: number): number { 72 | return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? Math.pow(2, 20 * x - 10) / 2 : (2 - Math.pow(2, -20 * x + 10)) / 2; 73 | } 74 | // back 75 | function easeInBack(x: number): number { 76 | const c1 = 1.70158; 77 | const c3 = c1 + 1; 78 | 79 | return c3 * x * x * x - c1 * x * x; 80 | } 81 | function easeOutBack(x: number): number { 82 | const c1 = 1.70158; 83 | const c3 = c1 + 1; 84 | 85 | return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); 86 | } 87 | function easeInOutBack(x: number): number { 88 | const c1 = 1.70158; 89 | const c2 = c1 * 1.525; 90 | 91 | return x < 0.5 92 | ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2 93 | : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; 94 | } 95 | // elastic 96 | function easeInElastic(x: number): number { 97 | const c4 = (2 * Math.PI) / 3; 98 | 99 | return x === 0 ? 0 : x === 1 ? 1 : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4); 100 | } 101 | function easeOutElastic(x: number): number { 102 | const c4 = (2 * Math.PI) / 3; 103 | 104 | return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; 105 | } 106 | function easeInOutElastic(x: number): number { 107 | const c5 = (2 * Math.PI) / 4.5; 108 | 109 | return x === 0 110 | ? 0 111 | : x === 1 112 | ? 1 113 | : x < 0.5 114 | ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 115 | : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1; 116 | } 117 | // bounce 118 | function easeInBounce(x: number): number { 119 | return 1 - easeOutBounce(1 - x); 120 | } 121 | function easeOutBounce(x: number): number { 122 | const n1 = 7.5625; 123 | const d1 = 2.75; 124 | 125 | if (x < 1 / d1) { 126 | return n1 * x * x; 127 | } else if (x < 2 / d1) { 128 | return n1 * (x -= 1.5 / d1) * x + 0.75; 129 | } else if (x < 2.5 / d1) { 130 | return n1 * (x -= 2.25 / d1) * x + 0.9375; 131 | } else { 132 | return n1 * (x -= 2.625 / d1) * x + 0.984375; 133 | } 134 | } 135 | function easeInOutBounce(x: number): number { 136 | return x < 0.5 ? (1 - easeOutBounce(1 - 2 * x)) / 2 : (1 + easeOutBounce(2 * x - 1)) / 2; 137 | } 138 | export { 139 | linear, 140 | easeOutCubic, 141 | easeInCubic, 142 | easeInOutCubic, 143 | easeInCirc, 144 | easeOutCirc, 145 | easeInOutCirc, 146 | easeInQuint, 147 | easeOutQuint, 148 | easeInOutQuint, 149 | easeInSine, 150 | easeOutSine, 151 | easeInOutSine, 152 | easeInQuad, 153 | easeOutQuad, 154 | easeInOutQuad, 155 | easeInQuart, 156 | easeOutQuart, 157 | easeInOutQuart, 158 | easeInExpo, 159 | easeOutExpo, 160 | easeInOutExpo, 161 | easeInBack, 162 | easeOutBack, 163 | easeInOutBack, 164 | easeInElastic, 165 | easeOutElastic, 166 | easeInOutElastic, 167 | easeInBounce, 168 | easeOutBounce, 169 | easeInOutBounce 170 | }; 171 | -------------------------------------------------------------------------------- /packages/lib/hooks/easingFunction.ts: -------------------------------------------------------------------------------- 1 | export const EASING_FUNCTIONS = { 2 | linear: 'linear', 3 | ease: 'ease', 4 | easeIn: 'ease-in', 5 | easeOut: 'ease-out', 6 | easeInOut: 'ease-in-out', 7 | easeInSine: 'cubic-bezier(0.47, 0.0, 0.745, 0.715)', 8 | easeOutSine: 'cubic-bezier(0.39, 0.575, 0.565, 1.0)', 9 | easeInOutSine: 'cubic-bezier(0.445, 0.05, 0.55, 0.95)', 10 | easeOutInSine: 'cubic-bezier(0.05, 0.445, 0.95, 0.55)', 11 | easeInQuad: 'cubic-bezier(0.55, 0.085, 0.68, 0.53)', 12 | easeOutQuad: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 13 | easeInOutQuad: 'cubic-bezier(0.455, 0.03, 0.515, 0.955)', 14 | easeOutInQuad: 'cubic-bezier(0.03, 0.455, 0.955, 0.515)', 15 | easeInCubic: 'cubic-bezier(0.55, 0.055, 0.675, 0.19)', 16 | easeOutCubic: 'cubic-bezier(0.215, 0.61, 0.355, 1.0)', 17 | easeInOutCubic: 'cubic-bezier(0.645, 0.045, 0.355, 1.0)', 18 | easeOutInCubic: 'cubic-bezier(0.045, 0.645, 1.0, 0.355)', 19 | easeInQuart: 'cubic-bezier(0.895, 0.03, 0.685, 0.22)', 20 | easeOutQuart: 'cubic-bezier(0.165, 0.84, 0.44, 1.0)', 21 | easeInOutQuart: 'cubic-bezier(0.77, 0.0, 0.175, 1.0)', 22 | easeOutInQuart: 'cubic-bezier(0.0, 0.77, 1.0, 0.175)', 23 | easeInQuint: 'cubic-bezier(0.755, 0.05, 0.855, 0.06)', 24 | easeOutQuint: 'cubic-bezier(0.23, 1.0, 0.32, 1.0)', 25 | easeInOutQuint: 'cubic-bezier(0.86, 0.0, 0.07, 1.0)', 26 | easeOutInQuint: 'cubic-bezier(0.0, 0.86, 1.0, 0.07)', 27 | easeInExpo: 'cubic-bezier(0.95, 0.05, 0.795, 0.035)', 28 | easeOutExpo: 'cubic-bezier(0.19, 1.0, 0.22, 1.0)', 29 | easeInOutExpo: 'cubic-bezier(1.0, 0.0, 0.0, 1.0)', 30 | easeOutInExpo: 'cubic-bezier(0.0, 1.0, 1.0, 0.0)', 31 | easeInCirc: 'cubic-bezier(0.6, 0.04, 0.98, 0.335)', 32 | easeOutCirc: 'cubic-bezier(0.075, 0.82, 0.165, 1.0)', 33 | easeInOutCirc: 'cubic-bezier(0.785, 0.135, 0.15, 0.86)', 34 | easeOutInCirc: 'cubic-bezier(0.135, 0.785, 0.86, 0.15)', 35 | easeInBack: 'cubic-bezier(0.6, -0.28, 0.735, 0.045)', 36 | easeOutBack: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', 37 | easeInOutBack: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', 38 | easeOutInBack: 'cubic-bezier(0.25, 1.25, 0.75, -0.25)', 39 | easeInEpic: 'cubic-bezier(0.25, 0.0, 0.75, 0.25)', 40 | easeOutEpic: 'cubic-bezier(0.25, 0.75, 0.75, 1.0)', 41 | easeInOutEpic: 'cubic-bezier(0.75, 0.25, 0.25, 0.75)', 42 | easeOutInEpic: 'cubic-bezier(0.25, 0.75, 0.75, 0.25)', 43 | easeInElastic: `linear( 44 | 0, 0.002 13.3%, -0.006 27.8%, -0.001 31.9%, 0.015 39.3%, 0.016 42.5%, 0.012, 45 | 0.004 46.7%, -0.042 54.3%, -0.046 55.9% 57.3%, -0.036, -0.012 61.7%, 46 | 0.019 63.6%, 0.093 67.6%, 0.118 69.3%, 0.13, 0.131 72.2%, 0.123, 0.109 74.1%, 47 | 0.059 76%, -0.032 78.2%, -0.315 83.8%, -0.364 85.5%, -0.373, -0.37 87.1%, 48 | -0.346, -0.296 89.3%, -0.114 91.5%, 0.138 93.5%, 0.782 97.9%, 1 49 | )`, 50 | easeOutElastic: `linear( 51 | 0, 0.218 2.1%, 0.862 6.5%, 1.114, 1.296 10.7%, 1.346, 1.37 12.9%, 1.373, 52 | 1.364 14.5%, 1.315 16.2%, 1.032 21.8%, 0.941 24%, 0.891 25.9%, 0.877, 53 | 0.869 27.8%, 0.87, 0.882 30.7%, 0.907 32.4%, 0.981 36.4%, 1.012 38.3%, 1.036, 54 | 1.046 42.7% 44.1%, 1.042 45.7%, 0.996 53.3%, 0.988, 0.984 57.5%, 0.985 60.7%, 55 | 1.001 68.1%, 1.006 72.2%, 0.998 86.7%, 1 56 | )`, 57 | easeInOutElastic: `linear( 58 | 0, 0.001 8.5%, -0.005 19.2%, 0.002 22.4%, 0.023 27.8%, 0.024 30.2%, 0.014, 59 | -0.006 33.5%, -0.109 39.1%, -0.118, -0.115, -0.099, -0.07 43%, 0.03 44.8%, 60 | 0.93 54.7%, 1.045 56.4%, 1.082, 1.106 58.2%, 1.114, 1.118, 1.117, 1.112 60.7%, 61 | 1.092 62%, 1.005 66.5%, 0.986, 0.976 69.9%, 0.977 72.2%, 0.998 77.6%, 62 | 1.005 80.8%, 0.999 91.5%, 1 63 | )`, 64 | easeInBounce: `linear( 65 | 0, 0.012, 0.016, 0.012, 0, 0.027, 0.047, 0.059, 0.063, 0.059, 0.047, 0.027, 66 | 0 27.3%, 0.109 31.8%, 0.152, 0.188, 0.215, 0.234, 0.246, 0.25, 0.246, 0.234, 67 | 0.215, 0.188, 0.152, 0.109 59.1%, 0, 0.234, 0.438, 0.609, 0.75, 0.859 86.4%, 68 | 0.902, 0.938, 0.965, 0.984, 0.996, 1 69 | )`, 70 | easeOutBounce: `linear( 71 | 0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765, 72 | 1, 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785, 73 | 0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953, 74 | 0.973, 1, 0.988, 0.984, 0.988, 1 75 | )`, 76 | easeInOutBounce: `linear( 77 | 0, 0.006 1.1%, 0.007, 0.008, 0.007, 0.006 3.4%, 0, 0.014, 0.023, 0.029 8%, 78 | 0.031 8.5% 9.7%, 0.029 10.2%, 0.023, 0.014, 0, 0.03, 0.055, 0.076, 0.094, 79 | 0.107, 0.117, 0.123 21.6%, 0.124, 0.125, 0.125, 0.123 23.9%, 0.117, 0.108, 80 | 0.094, 0.076, 0.055, 0.03, 0 31.8%, 0.074, 0.137, 0.196, 0.25, 0.299, 0.342, 81 | 0.38, 0.413 42.4%, 0.436, 0.456, 0.472, 0.485 46.8%, 0.492, 0.497 48.7%, 82 | 0.499 49.4%, 0.501 50.8%, 0.505, 0.514, 0.527, 0.544, 0.567, 0.593, 0.625, 83 | 0.658, 0.695, 0.736, 0.781 63.6%, 0.882, 1 68.2%, 0.969, 0.945, 0.924, 0.906, 84 | 0.893, 0.883, 0.877 76.1%, 0.875, 0.875, 0.876, 0.877 78.4%, 0.883, 0.893, 85 | 0.906, 0.924, 0.945, 0.97, 1, 0.986, 0.977, 0.971 89.8%, 0.969 90.3% 91.5%, 86 | 0.971 92%, 0.977, 0.986, 1, 0.994 96.6%, 0.993, 0.992, 0.993, 0.994 98.9%, 1 87 | )` 88 | }; 89 | 90 | export type EaseFunctionType = keyof typeof EASING_FUNCTIONS; 91 | -------------------------------------------------------------------------------- /packages/lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './animate'; 2 | export * from './useAnimate'; 3 | export * from './useMotion'; 4 | export * from './useGroup'; 5 | export * from './useMultiple'; 6 | export * from './useInView'; 7 | export * from './easingFunction'; 8 | export * from './useValue'; 9 | export * from './useLineDraw'; 10 | export * from './useSpring'; 11 | export * from './types'; 12 | -------------------------------------------------------------------------------- /packages/lib/hooks/presets.ts: -------------------------------------------------------------------------------- 1 | // fade 2 | export const fadeIn: PropertyIndexedKeyframes = { 3 | opacity: [0, 1] 4 | }; 5 | 6 | export const fadeOut: PropertyIndexedKeyframes = { 7 | opacity: [1, 0] 8 | }; 9 | 10 | export const fadeInDown: Keyframe[] = [ 11 | { opacity: 0, transform: 'translateY(-100%)' }, 12 | { opacity: 1, transform: 'translateY(0)' } 13 | ]; 14 | 15 | export const fadeInUp: Keyframe[] = [ 16 | { opacity: 0, transform: 'translateY(100%)' }, 17 | { opacity: 1, transform: 'translateY(0)' } 18 | ]; 19 | 20 | export const fadeInLeft: Keyframe[] = [ 21 | { opacity: 0, transform: 'translateX(-100%)' }, 22 | { opacity: 1, transform: 'translateX(0)' } 23 | ]; 24 | 25 | export const fadeInRight: Keyframe[] = [ 26 | { opacity: 0, transform: 'translateX(100%)' }, 27 | { opacity: 1, transform: 'translateX(0)' } 28 | ]; 29 | 30 | export const fadeOutDown: Keyframe[] = [ 31 | { opacity: 1, transform: 'translateY(0)' }, 32 | { opacity: 0, transform: 'translateY(100%)' } 33 | ]; 34 | export const fadeOutUp: Keyframe[] = [ 35 | { opacity: 1, transform: 'translateY(0)' }, 36 | { opacity: 0, transform: 'translateY(-100%)' } 37 | ]; 38 | 39 | export const fadeOutLeft: Keyframe[] = [ 40 | { opacity: 1, transform: 'translateX(0)' }, 41 | { opacity: 0, transform: 'translateX(-100%)' } 42 | ]; 43 | 44 | export const fadeOutRight: Keyframe[] = [ 45 | { opacity: 1, transform: 'translateX(0)' }, 46 | { opacity: 0, transform: 'translateX(100%)' } 47 | ]; 48 | 49 | // slides 50 | export const slideInLeft: PropertyIndexedKeyframes = { transform: ['translateX(-100%)', 'translateX(0)'] }; 51 | export const slideInRight: PropertyIndexedKeyframes = { transform: ['translateX(100%)', 'translateX(0)'] }; 52 | export const slideInUp: PropertyIndexedKeyframes = { transform: ['translateY(100%)', 'translateY(0)'] }; 53 | export const slideInDown: PropertyIndexedKeyframes = { transform: ['translateY(-100%)', 'translateY(0)'] }; 54 | export const slideOutLeft: PropertyIndexedKeyframes = { transform: ['translateX(0)', 'translateX(-100%)'] }; 55 | export const slideOutRight: PropertyIndexedKeyframes = { transform: ['translateX(0)', 'translateX(100%)'] }; 56 | export const slideOutUp: PropertyIndexedKeyframes = { transform: ['translateY(0)', 'translateY(-100%)'] }; 57 | export const slideOutDown: PropertyIndexedKeyframes = { transform: ['translateY(0)', 'translateY(100%)'] }; 58 | 59 | // zoom 60 | export const zoomIn: PropertyIndexedKeyframes = { transform: ['scale(0)', 'scale(1)'] }; 61 | export const zoomOut: PropertyIndexedKeyframes = { transform: ['scale(1)', 'scale(0)'] }; 62 | export const zoomFadeIn: PropertyIndexedKeyframes = { 63 | transform: ['scale(0)', 'scale(1)'], 64 | opacity: [0, 1] 65 | }; 66 | export const zoomFadeOut: PropertyIndexedKeyframes = { 67 | transform: ['scale(1)', 'scale(0)'], 68 | opacity: [1, 0] 69 | }; 70 | export const zoomOverIn: PropertyIndexedKeyframes = { 71 | transform: ['scale(0.94)', 'scale(1)'], 72 | opacity: [0, 1] 73 | }; 74 | export const zoomOverOut: PropertyIndexedKeyframes = { 75 | transform: ['scale(1)', 'scale(0.94)'], 76 | opacity: [1, 0] 77 | }; 78 | 79 | // flip 80 | export const flipX: PropertyIndexedKeyframes = { transform: ['rotateX(0deg)', 'rotateX(180deg)'] }; 81 | export const flipY: PropertyIndexedKeyframes = { transform: ['rotateY(0deg)', 'rotateY(180deg)'] }; 82 | export const flipXTop: PropertyIndexedKeyframes = { 83 | transform: ['rotateX(0deg)', 'rotateX(180deg)'], 84 | transformOrigin: 'top' 85 | }; 86 | export const flipXBottom: PropertyIndexedKeyframes = { 87 | transform: ['rotateX(0deg)', 'rotateX(180deg)'], 88 | transformOrigin: 'bottom' 89 | }; 90 | export const flipYLeft: PropertyIndexedKeyframes = { 91 | transform: ['rotateY(0deg)', 'rotateY(180deg)'], 92 | transformOrigin: 'left' 93 | }; 94 | export const flipYRight: PropertyIndexedKeyframes = { 95 | transform: ['rotateY(0deg)', 'rotateY(180deg)'], 96 | transformOrigin: 'right' 97 | }; 98 | 99 | export const flash: PropertyIndexedKeyframes = { 100 | opacity: [1, 0, 1, 0, 1], 101 | offset: [0, 0.25, 0.5, 0.75, 1] 102 | }; 103 | export const pulse: PropertyIndexedKeyframes = { transform: ['scale(1)', 'scale(1.1)', 'scale(1)'] }; 104 | export const heartBeat: PropertyIndexedKeyframes = { 105 | transform: ['scale(1)', 'scale(1.2)', 'scale(1)', 'scale(1.2)', 'scale(1)'], 106 | offset: [0, 0.14, 0.28, 0.48, 0.8] 107 | }; 108 | export const breath: PropertyIndexedKeyframes = { opacity: [1, 0.3, 1] }; 109 | export const swing: PropertyIndexedKeyframes = { 110 | transform: ['rotate(0)', 'rotate(12deg)', 'rotate(-8deg)', 'rotate(5deg)', 'rotate(-5deg)', 'rotate(0)'], 111 | offset: [0, 0.2, 0.4, 0.6, 0.8] 112 | }; 113 | export const shakeX: PropertyIndexedKeyframes = { 114 | transform: [ 115 | 'translateX(0)', 116 | 'translateX(-10px)', 117 | 'translateX(10px)', 118 | 'translateX(-10px)', 119 | 'translateX(10px)', 120 | 'translateX(-10px)', 121 | 'translateX(10px)', 122 | 'translateX(-10px)', 123 | 'translateX(10px)', 124 | 'translateX(-10px)', 125 | 'translateX(0)' 126 | ], 127 | offset: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] 128 | }; 129 | export const shakeY: PropertyIndexedKeyframes = { 130 | transform: [ 131 | 'translateY(0)', 132 | 'translateY(-8px)', 133 | 'translateY(8px)', 134 | 'translateY(-8px)', 135 | 'translateY(8px)', 136 | 'translateY(-8px)', 137 | 'translateY(8px)', 138 | 'translateY(-8px)', 139 | 'translateY(8px)', 140 | 'translateY(-8px)', 141 | 'translateY(0)' 142 | ], 143 | offset: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] 144 | }; 145 | -------------------------------------------------------------------------------- /packages/lib/hooks/types.ts: -------------------------------------------------------------------------------- 1 | export type DOMElement = HTMLElement | SVGElement | MathMLElement; 2 | export type AnimationOptions = number | _KeyframeAnimationOptions; 3 | export type SpecialAnimationOptions = number | SpecialKeyframeAnimationOptions; 4 | export type Keyframes = Keyframe[] | PropertyIndexedKeyframes; 5 | export type Animations = (Animation | undefined)[]; 6 | export type AnimationController = (keyframes: Keyframes, options?: AnimationOptions) => Animation; 7 | // duration can not be string 8 | export type _KeyframeAnimationOptions = Omit & { 9 | duration?: number | CSSNumericValue; 10 | }; 11 | export interface ValueController extends Omit { 12 | isPlaying: boolean; 13 | isPaused: boolean; 14 | } 15 | export interface AnimateController { 16 | play: () => void; 17 | pause: () => void; 18 | cancel: () => void; 19 | reverse: () => void; 20 | resume: () => void; 21 | } 22 | type SpecialKeyframeAnimationOptions = Omit<_KeyframeAnimationOptions, 'delay' | 'endDelay'> & { 23 | delay?: number | DelayFunction; 24 | endDelay?: number | DelayFunction; 25 | }; 26 | type DelayFunction = (el: T, index: number, length: number) => number; 27 | -------------------------------------------------------------------------------- /packages/lib/hooks/useAnimate.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import type { AnimationOptions, AnimationController, DOMElement, Keyframes } from './types'; 3 | import { checkDuration, checkRef } from './utils'; 4 | export function useAnimate(): [React.RefObject, AnimationController] { 5 | const ref = useRef(null); 6 | 7 | function animate(keyframes: Keyframes, options?: AnimationOptions): Animation { 8 | checkRef(ref as React.RefObject); 9 | checkDuration(options); 10 | return ref.current!.animate(keyframes, options); 11 | } 12 | 13 | return [ref as React.RefObject, animate]; 14 | } 15 | -------------------------------------------------------------------------------- /packages/lib/hooks/useGroup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description advanced use. manage a group of elements using the same animation parameters 3 | */ 4 | import { useRef, useEffect, useCallback } from 'react'; 5 | import * as controller from './controller'; 6 | import { combineKeyframeByMotion, combineOptions } from './utils'; 7 | import type { AnimateController, DOMElement, Keyframes, SpecialAnimationOptions } from './types'; 8 | import type { MotionName } from './useMotion'; 9 | 10 | interface useGroupProps { 11 | selectors?: string[]; 12 | refs?: React.MutableRefObject[]; 13 | keyframes?: Keyframes; 14 | motion?: MotionName; 15 | options?: SpecialAnimationOptions; 16 | onComplete?: (trigger?: 'play' | 'reverse') => void; 17 | onStart?: () => void; 18 | onPause?: () => void; 19 | onCancel?: () => void; 20 | onResume?: () => void; 21 | } 22 | 23 | export function useGroup(props: useGroupProps, deps: any[]): AnimateController { 24 | const { 25 | selectors = [], 26 | refs, 27 | keyframes, 28 | motion, 29 | options = 0, 30 | onComplete, 31 | onStart, 32 | onPause, 33 | onCancel, 34 | onResume 35 | } = props; 36 | const animations = useRef<(Animation | undefined)[]>([]); 37 | const targets = useRef(null); 38 | 39 | const getTargets = useCallback(() => { 40 | if (refs) { 41 | return refs.map((ref) => ref.current as T); 42 | } 43 | if (Array.isArray(selectors)) { 44 | const list: T[] = []; 45 | selectors.forEach((selector) => { 46 | list.push(...document.querySelectorAll(selector)); 47 | }); 48 | return list; 49 | } 50 | }, [refs, selectors]); 51 | 52 | useEffect(() => { 53 | const newTargets = getTargets(); 54 | if (!newTargets) { 55 | throw new Error('useGroup: selectors or refs must resolve to at least one DOM element'); 56 | } 57 | targets.current = newTargets; 58 | const _keyframes = combineKeyframeByMotion(keyframes, motion); 59 | animations.current = targets.current!.map((el, index, arr) => { 60 | const animation = el.animate(_keyframes, combineOptions(options, el, index, arr.length)); 61 | animation.cancel(); 62 | return animation; 63 | }); 64 | 65 | return () => clear(); 66 | }, deps); 67 | 68 | const clear = () => { 69 | targets.current?.forEach((el) => { 70 | el?.getAnimations().forEach((animation) => animation.cancel()); 71 | }); 72 | }; 73 | 74 | const play = useCallback(() => { 75 | clear(); 76 | onStart?.(); 77 | controller.play(animations.current, onComplete); 78 | }, [onStart]); 79 | 80 | const pause = useCallback(() => { 81 | onPause?.(); 82 | controller.pause(animations.current); 83 | }, []); 84 | 85 | const cancel = useCallback(() => { 86 | onCancel?.(); 87 | controller.cancel(animations.current); 88 | }, [onCancel]); 89 | 90 | const reverse = useCallback(() => { 91 | controller.reverse(animations.current, onComplete); 92 | }, []); 93 | 94 | const resume = useCallback(() => { 95 | onResume?.(); 96 | controller.resume(animations.current); 97 | }, [onResume]); 98 | 99 | return { play, pause, cancel, reverse, resume }; 100 | } 101 | -------------------------------------------------------------------------------- /packages/lib/hooks/useInView.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import { DOMElement } from './types'; 3 | type MotionCallback = (target: DOMElement) => void; 4 | interface InViewOptions extends IntersectionObserverInit { 5 | once?: boolean; 6 | } 7 | interface UseInViewProps { 8 | selectors?: string[]; 9 | refs?: React.RefObject[]; 10 | enter?: MotionCallback; 11 | leave?: MotionCallback; 12 | options?: InViewOptions; 13 | } 14 | interface CreateObserverOptions extends InViewOptions { 15 | enter?: MotionCallback; 16 | leave?: MotionCallback; 17 | } 18 | 19 | function createObserver(target: DOMElement, options: CreateObserverOptions) { 20 | const { once = false, enter, leave, ...opt } = options || {}; 21 | let hasTriggered = false; 22 | 23 | const observer = new IntersectionObserver(([entry]) => { 24 | if (entry.isIntersecting) { 25 | if (!hasTriggered) { 26 | enter?.(target); 27 | if (once) { 28 | hasTriggered = true; 29 | observer.unobserve(target); 30 | } 31 | } 32 | } else { 33 | if (!once || !hasTriggered) { 34 | leave?.(target); 35 | } 36 | } 37 | }, opt); 38 | 39 | if (target) { 40 | observer.observe(target); 41 | } 42 | 43 | return observer; 44 | } 45 | 46 | export function useInView(props: UseInViewProps) { 47 | const { refs, selectors, enter, leave, options } = props; 48 | const targets = useRef(null); 49 | const observers = useRef([]); 50 | const getTargets = useCallback(() => { 51 | if (refs) { 52 | return refs.map((ref) => ref.current as T); 53 | } 54 | if (Array.isArray(selectors)) { 55 | const list: T[] = []; 56 | selectors.forEach((selector) => { 57 | list.push(...document.querySelectorAll(selector)); 58 | }); 59 | return list; 60 | } 61 | }, [refs, selectors]); 62 | useEffect(() => { 63 | const newTargets = getTargets(); 64 | if (!newTargets) { 65 | throw new Error('useInView: selectors or refs must resolve to at least one DOM element'); 66 | } 67 | targets.current = newTargets; 68 | 69 | observers.current = targets.current.map((target) => 70 | createObserver(target, { 71 | ...options, 72 | enter, 73 | leave 74 | }) 75 | ); 76 | 77 | return () => { 78 | observers.current.forEach((observer) => observer.disconnect()); 79 | }; 80 | }, [refs, selectors, enter, leave, options]); 81 | } 82 | -------------------------------------------------------------------------------- /packages/lib/hooks/useLineDraw.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import * as controller from './controller'; 3 | import { Keyframes, SpecialAnimationOptions, AnimateController } from './types'; 4 | import { combineOptions, getType } from './utils'; 5 | type DrawType = 'appear' | 'disappear'; 6 | interface UseLineDrawProps { 7 | selectors?: string[]; 8 | refs?: React.MutableRefObject[]; 9 | keyframes?: Keyframes; 10 | drawType?: DrawType; 11 | options?: SpecialAnimationOptions; 12 | onComplete?: (trigger?: 'play' | 'reverse') => void; 13 | onStart?: () => void; 14 | onPause?: () => void; 15 | onCancel?: () => void; 16 | onResume?: () => void; 17 | } 18 | 19 | function combineKeyframes(drawType: DrawType, length: number, keyframes?: Keyframes) { 20 | if (getType(keyframes) === 'object') { 21 | return { 22 | ...keyframes, 23 | strokeDashoffset: drawType === 'appear' ? [length, 0] : [0, length] 24 | }; 25 | } 26 | if (Array.isArray(keyframes)) { 27 | return [ 28 | ...keyframes, 29 | { strokeDashoffset: drawType === 'appear' ? length : 0 }, 30 | { strokeDashoffset: drawType === 'appear' ? 0 : length } 31 | ]; 32 | } 33 | return { strokeDashoffset: drawType === 'appear' ? [length, 0] : [0, length] }; 34 | } 35 | export function useLineDraw(props: UseLineDrawProps, deps: any[]): AnimateController { 36 | const { 37 | selectors = [], 38 | refs, 39 | drawType = 'appear', 40 | keyframes, 41 | options = 0, 42 | onComplete, 43 | onStart, 44 | onPause, 45 | onCancel, 46 | onResume 47 | } = props; 48 | const animations = useRef<(Animation | undefined)[]>([]); 49 | const targets = useRef(null); 50 | 51 | const getTargets = useCallback(() => { 52 | if (refs) { 53 | return refs.map((ref) => ref.current as T); 54 | } 55 | if (Array.isArray(selectors)) { 56 | const list: T[] = []; 57 | selectors.forEach((selector) => { 58 | list.push(...document.querySelectorAll(selector)); 59 | }); 60 | return list; 61 | } 62 | }, [refs, selectors]); 63 | 64 | useEffect(() => { 65 | const newTargets = getTargets(); 66 | if (!newTargets) { 67 | throw new Error('useLineDraw: selectors or refs must resolve to at least one DOM element'); 68 | } 69 | targets.current = newTargets; 70 | animations.current = targets.current!.map((el, index, arr) => { 71 | const length = el.getTotalLength(); 72 | el.setAttribute('stroke-dasharray', length + ' ' + length); 73 | const animation = el.animate( 74 | combineKeyframes(drawType, length, keyframes), 75 | combineOptions(options, el, index, arr.length) 76 | ); 77 | animation.cancel(); 78 | return animation; 79 | }); 80 | 81 | return () => clear(); 82 | }, deps); 83 | 84 | const clear = () => { 85 | targets.current?.forEach((el) => { 86 | el?.getAnimations().forEach((animation) => animation.cancel()); 87 | }); 88 | }; 89 | 90 | const play = useCallback(() => { 91 | clear(); 92 | onStart?.(); 93 | controller.play(animations.current, onComplete); 94 | }, [onStart]); 95 | 96 | const pause = useCallback(() => { 97 | onPause?.(); 98 | controller.pause(animations.current); 99 | }, []); 100 | 101 | const cancel = useCallback(() => { 102 | onCancel?.(); 103 | controller.cancel(animations.current); 104 | }, [onCancel]); 105 | 106 | const reverse = useCallback(() => { 107 | controller.reverse(animations.current, onComplete); 108 | }, []); 109 | 110 | const resume = useCallback(() => { 111 | onResume?.(); 112 | controller.resume(animations.current); 113 | }, [onResume]); 114 | 115 | return { play, pause, cancel, reverse, resume }; 116 | } 117 | -------------------------------------------------------------------------------- /packages/lib/hooks/useMultiple.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description advanced use. Manage multiple elements and use independent animations or the same animation parameters 3 | */ 4 | import { useRef, useEffect, useCallback } from 'react'; 5 | import * as controller from './controller'; 6 | import { getType, combine, checkDuration } from './utils'; 7 | import * as presets from './presets'; 8 | import type { AnimationOptions, AnimateController, DOMElement, Keyframes, _KeyframeAnimationOptions } from './types'; 9 | import type { MotionName } from './useMotion'; 10 | interface MultipleConfig { 11 | ref: React.MutableRefObject; 12 | keyframes?: Keyframes; 13 | options?: AnimationOptions; 14 | motion?: MotionName; 15 | } 16 | 17 | interface useMultipleProps { 18 | baseOptions?: AnimationOptions; 19 | baseKeyframes?: Keyframes; 20 | baseMotion?: MotionName; 21 | config: MultipleConfig[]; 22 | onComplete?: (trigger?: 'play' | 'reverse') => void; 23 | onStart?: () => void; 24 | onPause?: () => void; 25 | onCancel?: () => void; 26 | onResume?: () => void; 27 | } 28 | 29 | function combineOptions(baseOptions?: AnimationOptions, options?: AnimationOptions) { 30 | if (typeof baseOptions === 'number' && typeof options === 'number') { 31 | return options; 32 | } 33 | if (typeof baseOptions === 'number' && getType(options) === 'object') { 34 | return { 35 | duration: baseOptions, 36 | ...(options as _KeyframeAnimationOptions) 37 | }; 38 | } 39 | if (getType(baseOptions) === 'object' && typeof options === 'number') { 40 | return { 41 | ...(baseOptions as _KeyframeAnimationOptions), 42 | duration: options 43 | }; 44 | } 45 | if (getType(baseOptions) === 'object' && getType(options) === 'object') { 46 | return combine(baseOptions, options); 47 | } 48 | 49 | return baseOptions || options || {}; 50 | } 51 | 52 | function combineMotion(baseMotion?: MotionName, motion?: MotionName) { 53 | if (motion) { 54 | return presets[motion]; 55 | } 56 | return baseMotion && presets[baseMotion] ? presets[baseMotion] : undefined; 57 | } 58 | 59 | function combineKeyframes({ 60 | baseKeyframes, 61 | keyframes, 62 | baseMotion, 63 | motion 64 | }: { 65 | baseKeyframes?: Keyframes; 66 | keyframes?: Keyframes; 67 | baseMotion?: MotionName; 68 | motion?: MotionName; 69 | }) { 70 | if (!baseKeyframes && !keyframes) { 71 | return combineMotion(baseMotion, motion); 72 | } 73 | if (getType(baseKeyframes) === getType(keyframes)) { 74 | return combine(baseKeyframes, keyframes); 75 | } 76 | if (baseKeyframes && !keyframes) { 77 | return baseKeyframes; 78 | } 79 | return keyframes; 80 | } 81 | export function useMultiple(props: useMultipleProps, deps: any[]): AnimateController { 82 | const { baseKeyframes, baseOptions, baseMotion, config, onStart, onCancel, onComplete, onPause, onResume } = props; 83 | const animations = useRef<(Animation | undefined)[]>([]); 84 | 85 | useEffect(() => { 86 | animations.current = config.map(({ ref, keyframes, options, motion }) => { 87 | const _keyframes = combineKeyframes({ baseKeyframes, keyframes, baseMotion, motion }); 88 | const _options = combineOptions(baseOptions, options); 89 | checkDuration(_options); 90 | const animation = ref.current?.animate(_keyframes || [], _options); 91 | animation?.cancel(); 92 | return animation; 93 | }); 94 | 95 | return () => clear(); 96 | }, deps); 97 | 98 | const clear = () => { 99 | config.forEach(({ ref }) => { 100 | ref.current?.getAnimations().forEach((animation) => animation.cancel()); 101 | }); 102 | }; 103 | 104 | const play = useCallback(() => { 105 | clear(); 106 | onStart?.(); 107 | controller.play(animations.current, onComplete); 108 | }, [onStart]); 109 | 110 | const pause = useCallback(() => { 111 | onPause?.(); 112 | controller.pause(animations.current); 113 | }, []); 114 | 115 | const cancel = useCallback(() => { 116 | onCancel?.(); 117 | controller.cancel(animations.current); 118 | }, [onCancel]); 119 | 120 | const reverse = useCallback(() => { 121 | controller.reverse(animations.current, onComplete); 122 | }, []); 123 | 124 | const resume = useCallback(() => { 125 | onResume?.(); 126 | controller.resume(animations.current); 127 | }, [onResume]); 128 | 129 | return { play, pause, cancel, reverse, resume }; 130 | } 131 | -------------------------------------------------------------------------------- /packages/lib/hooks/useSpring.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef, useCallback } from 'react'; 2 | import { createSpring } from './createSpring'; 3 | import type { ValueController } from './types'; 4 | interface SpringOptions { 5 | mass?: number; // affect the inertia of the spring 6 | stiffness?: number; // higher value makes the spring motion faster and stronger 7 | damping?: number; // higher values cause the spring to stop faster 8 | velocity?: number; // initial speed 9 | autoPlay?: boolean; 10 | } 11 | export function useSpring(from: number, to: number, options: SpringOptions = {}): [number, ValueController] { 12 | const { mass = 1, stiffness = 100, damping = 10, velocity = 0, autoPlay = true } = options; 13 | 14 | const [value, setValue] = useState(from); 15 | const [isPlaying, setIsPlaying] = useState(autoPlay); 16 | const [isPaused, setIsPaused] = useState(false); 17 | 18 | const animationRef = useRef(null); 19 | const startTimeRef = useRef(null); 20 | const pausedTimeRef = useRef(null); 21 | 22 | const animate = useCallback( 23 | (currentTime: number) => { 24 | if (!startTimeRef.current) startTimeRef.current = currentTime; 25 | const [duration, solver] = createSpring({ 26 | mass, 27 | stiffness, 28 | damping, 29 | velocity 30 | }); 31 | 32 | let elapsedTime = currentTime - startTimeRef.current; 33 | if (pausedTimeRef.current) { 34 | elapsedTime -= pausedTimeRef.current; 35 | } 36 | 37 | if (elapsedTime < duration) { 38 | const progress = solver(elapsedTime / duration); 39 | setValue(from + (to - from) * progress); 40 | animationRef.current = requestAnimationFrame(animate); 41 | } else { 42 | setValue(to); 43 | setIsPlaying(false); 44 | } 45 | }, 46 | [from, to, mass, stiffness, damping, velocity] 47 | ); 48 | 49 | const play = useCallback(() => { 50 | setIsPlaying(true); 51 | setIsPaused(false); 52 | startTimeRef.current = null; 53 | pausedTimeRef.current = null; 54 | animationRef.current = requestAnimationFrame(animate); 55 | }, [animate]); 56 | 57 | const pause = useCallback(() => { 58 | if (animationRef.current !== null) { 59 | cancelAnimationFrame(animationRef.current); 60 | setIsPaused(true); 61 | setIsPlaying(false); 62 | pausedTimeRef.current = performance.now() - (startTimeRef.current || 0); 63 | } 64 | }, []); 65 | 66 | const resume = useCallback(() => { 67 | if (isPaused) { 68 | setIsPlaying(true); 69 | setIsPaused(false); 70 | animationRef.current = requestAnimationFrame(animate); 71 | } 72 | }, [isPaused, animate]); 73 | 74 | const cancel = useCallback(() => { 75 | if (animationRef.current !== null) { 76 | cancelAnimationFrame(animationRef.current); 77 | setValue(from); 78 | setIsPlaying(false); 79 | setIsPaused(false); 80 | startTimeRef.current = null; 81 | pausedTimeRef.current = null; 82 | } 83 | }, [from]); 84 | 85 | useEffect(() => { 86 | if (autoPlay) { 87 | play(); 88 | } 89 | 90 | return () => { 91 | if (animationRef.current !== null) { 92 | cancelAnimationFrame(animationRef.current); 93 | } 94 | }; 95 | }, [autoPlay, play]); 96 | 97 | const controller: ValueController = { 98 | play, 99 | pause, 100 | resume, 101 | cancel, 102 | isPlaying, 103 | isPaused 104 | }; 105 | 106 | return [value, controller]; 107 | } 108 | -------------------------------------------------------------------------------- /packages/lib/hooks/useValue.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback, useRef } from 'react'; 2 | import * as easeAlgorithm from './easingAlgorithm'; 3 | import type { ValueController } from './types'; 4 | 5 | type EaseAlgorithmTypes = keyof typeof easeAlgorithm; 6 | 7 | interface ValueOptions { 8 | duration?: number; 9 | precision?: number; 10 | autoPlay?: boolean; 11 | easing?: EaseAlgorithmTypes; 12 | delay?: number; 13 | } 14 | 15 | export function useValue(from: number, to: number, options: ValueOptions = {}): [number, ValueController] { 16 | const { duration = 1000, precision = 0, autoPlay = true, easing = 'easeOutCubic', delay = 0 } = options; 17 | 18 | if (duration <= 0) { 19 | throw new Error('useValue: duration must be greater than 0'); 20 | } 21 | 22 | const [value, setValue] = useState(from); 23 | const [isPlaying, setIsPlaying] = useState(autoPlay); 24 | const [isPaused, setIsPaused] = useState(false); 25 | 26 | const startTimeRef = useRef(null); 27 | const pausedTimeRef = useRef(0); 28 | const animationFrameId = useRef(null); 29 | const timeoutId = useRef(null); 30 | 31 | const roundToPrecision = useCallback( 32 | (num: number) => { 33 | const multiplier = Math.pow(10, precision); 34 | return Math.round(num * multiplier) / multiplier; 35 | }, 36 | [precision] 37 | ); 38 | 39 | const animate = useCallback( 40 | (currentTime: number) => { 41 | if (startTimeRef.current === null) { 42 | startTimeRef.current = currentTime - pausedTimeRef.current; 43 | } 44 | const easingFunc = easeAlgorithm[easing]; 45 | const elapsedTime = currentTime - startTimeRef.current; 46 | const progress = Math.min(elapsedTime / duration, 1); 47 | const currentValue = from + (to - from) * easingFunc(progress); 48 | 49 | setValue(roundToPrecision(currentValue)); 50 | 51 | if (progress < 1 && isPlaying && !isPaused) { 52 | animationFrameId.current = requestAnimationFrame(animate); 53 | } else if (progress >= 1) { 54 | setIsPlaying(false); 55 | } 56 | }, 57 | [from, to, duration, easing, roundToPrecision, isPlaying, isPaused] 58 | ); 59 | 60 | useEffect(() => { 61 | if (isPlaying && !isPaused) { 62 | if (delay > 0) { 63 | timeoutId.current = window.setTimeout(() => { 64 | animationFrameId.current = requestAnimationFrame(animate); 65 | }, delay); 66 | } else { 67 | animationFrameId.current = requestAnimationFrame(animate); 68 | } 69 | } 70 | 71 | return () => { 72 | if (animationFrameId.current !== null) { 73 | cancelAnimationFrame(animationFrameId.current); 74 | } 75 | if (timeoutId.current !== null) { 76 | clearTimeout(timeoutId.current); 77 | } 78 | }; 79 | }, [animate, isPlaying, isPaused, delay]); 80 | 81 | const control: ValueController = { 82 | play: () => { 83 | setIsPlaying(true); 84 | setIsPaused(false); 85 | startTimeRef.current = null; 86 | pausedTimeRef.current = 0; 87 | }, 88 | cancel: () => { 89 | setIsPlaying(false); 90 | setIsPaused(false); 91 | startTimeRef.current = null; 92 | pausedTimeRef.current = 0; 93 | if (timeoutId.current !== null) { 94 | clearTimeout(timeoutId.current); 95 | } 96 | setValue(from); 97 | }, 98 | pause: () => { 99 | if (isPlaying && !isPaused) { 100 | setIsPaused(true); 101 | pausedTimeRef.current = performance.now() - (startTimeRef.current || 0); 102 | if (animationFrameId.current !== null) { 103 | cancelAnimationFrame(animationFrameId.current); 104 | } 105 | if (timeoutId.current !== null) { 106 | clearTimeout(timeoutId.current); 107 | } 108 | } 109 | }, 110 | resume: () => { 111 | if (isPlaying && isPaused) { 112 | setIsPaused(false); 113 | startTimeRef.current = null; 114 | } 115 | }, 116 | isPlaying, 117 | isPaused 118 | }; 119 | 120 | return [value, control]; 121 | } 122 | -------------------------------------------------------------------------------- /packages/lib/hooks/utils.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import * as presets from './presets'; 3 | import { AnimationOptions, DOMElement, Keyframes, SpecialAnimationOptions } from './types'; 4 | import { MotionName } from './useMotion'; 5 | 6 | export function checkRef(ref: RefObject) { 7 | if (!ref.current) { 8 | throw new Error('ref is not valid'); 9 | } 10 | } 11 | function isCSSNumericValueLessThanZero(value: CSSNumericValue) { 12 | const zero = CSS.number(0); 13 | const result = value.sub(zero) as CSSUnitValue; 14 | 15 | return result.value <= 0; 16 | } 17 | /** 18 | * @explain if duration is set to zero or undefined, if will not trigger animation 19 | * it may cause some unexpected behavior,so this function will check duration and 20 | * throw error if duration is less than or equals to zero 21 | */ 22 | export function checkDuration(options?: AnimationOptions | SpecialAnimationOptions) { 23 | if (typeof options === 'number' && options <= 0) { 24 | throw new Error('options is a number and must be greater than 0'); 25 | } else if (typeof options === 'object') { 26 | if ([typeof options.duration === 'number' && options.duration <= 0, options.duration instanceof CSSNumericValue && isCSSNumericValueLessThanZero(options.duration)].some(Boolean)) { 27 | throw new Error('options.duration must be greater than 0'); 28 | } 29 | } 30 | } 31 | export function getType(v?: any): string { 32 | return Object.prototype.toString.call(v).slice(8, -1).toLowerCase(); 33 | } 34 | 35 | type Combine = T extends Array ? U[] : T; 36 | 37 | export function combine(param1: T[], param2: U[]): Combine[]; 38 | export function combine(param1: T, param2: T): Combine; 39 | 40 | export function combine(...args: any[]): any { 41 | if (args.length !== 2) { 42 | throw new Error('Combine function requires exactly two arguments.'); 43 | } 44 | const [param1, param2] = args; 45 | if (getType(param1) !== getType(param2)) { 46 | throw new Error('Both arguments must be of the same type.'); 47 | } 48 | if (!Array.isArray(param1) && typeof param1 !== 'object') { 49 | throw new Error('Arguments must be either arrays or objects.'); 50 | } 51 | if (Array.isArray(param1)) { 52 | return [...param1, ...param2]; 53 | } 54 | if (typeof param1 === 'object') { 55 | return { ...param1, ...param2 }; 56 | } 57 | } 58 | 59 | export function combineOptions(options: SpecialAnimationOptions, el: T, index: number, length: number) { 60 | checkDuration(options); 61 | if (typeof options === 'number') { 62 | return options; 63 | } 64 | if (getType(options) === 'object') { 65 | return { 66 | ...options, 67 | delay: typeof options.delay === 'number' ? options.delay : options.delay?.(el, index, length), 68 | endDelay: typeof options.endDelay === 'number' ? options.endDelay : options.endDelay?.(el, index, length) 69 | }; 70 | } 71 | } 72 | 73 | export function combineKeyframeByMotion(keyframes?: Keyframes, motion?: MotionName) { 74 | if (keyframes) return keyframes; 75 | return motion ? presets[motion] : []; 76 | } 77 | -------------------------------------------------------------------------------- /packages/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cirolee/tiny-motion", 3 | "description": "The high-performance extension of Web Animation API for React Hooks", 4 | "version": "0.2.7", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.js" 13 | } 14 | }, 15 | "scripts": { 16 | "dev": "tsup --watch", 17 | "build": "tsup" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "keywords": [ 23 | "animation", 24 | "hooks", 25 | "Web Animation API", 26 | "motion", 27 | "@cirolee/tiny-motion" 28 | ], 29 | "author": "CiroLee", 30 | "homepage": "https://tiny-motion.vercel.app", 31 | "bugs": { 32 | "url": "https://github.com/CiroLee/tiny-motion/issues" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/CiroLee/tiny-motion.git" 37 | }, 38 | "license": "MIT", 39 | "peerDependencies": { 40 | "react": "^18 || ^19", 41 | "react-dom": "^18 || ^19" 42 | }, 43 | "devDependencies": { 44 | "@types/react": "^19.1.2", 45 | "@types/react-dom": "^19.1.3", 46 | "tsup": "^8.4.0", 47 | "typescript": "^5.8.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/lib/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, Options } from 'tsup'; 2 | 3 | export default defineConfig((options: Options) => { 4 | return { 5 | entry: ['hooks/index.ts'], 6 | outDir: 'dist', 7 | format: ['esm', 'cjs'], 8 | dts: true, 9 | minify: !options.watch, 10 | watch: options.watch, 11 | clean: true, 12 | platform: 'browser' 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------
33 | ); 34 | } 35 | 36 | export function TableRow({ className, ...props }: React.ComponentPropsWithRef<'tr'>) { 37 | return
; 42 | } 43 | 44 | interface TableBodyProps extends React.ComponentPropsWithRef<'tbody'> { 45 | striped?: boolean; 46 | } 47 | export function TableBody({ striped, className, ...props }: TableBodyProps) { 48 | return