├── .env.example ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CNAME ├── LICENSE ├── README.md ├── assets ├── all.gif ├── brand │ ├── usal_icon.svg │ ├── usal_icon_144.gif │ ├── usal_icon_144.png │ ├── usal_icon_144.webp │ ├── usal_icon_16.gif │ ├── usal_icon_16.png │ ├── usal_icon_16.webp │ ├── usal_icon_192.gif │ ├── usal_icon_192.png │ ├── usal_icon_192.webp │ ├── usal_icon_32.gif │ ├── usal_icon_32.png │ ├── usal_icon_32.webp │ ├── usal_icon_48.gif │ ├── usal_icon_48.png │ ├── usal_icon_48.webp │ ├── usal_icon_512.gif │ ├── usal_icon_512.png │ ├── usal_icon_512.webp │ ├── usal_icon_72.gif │ ├── usal_icon_72.png │ ├── usal_icon_72.webp │ ├── usal_icon_96.gif │ ├── usal_icon_96.png │ ├── usal_icon_96.webp │ ├── usal_logo.svg │ └── usal_logo_dark.svg ├── count.gif ├── logo.png ├── split.gif └── text.gif ├── dev ├── debug.css ├── debug.html └── debug.js ├── docs ├── API.md ├── CNAME ├── favicon.png ├── index.html ├── llms.txt ├── playground.css ├── playground.html ├── playground.js └── style.css ├── eslint.config.js ├── package-lock.json ├── package.json ├── scripts ├── build.js ├── colorize.js ├── lint-texts.js ├── postbuild.js ├── publish.js └── release-gh.js ├── src ├── integrations │ ├── angular.ts │ ├── lit.ts │ ├── react.tsx │ ├── solid.ts │ ├── svelte.ts │ └── vue.ts ├── plugins │ ├── nuxt-plugin.js │ └── nuxt.js ├── types │ ├── lit.d.ts │ ├── react.d.ts │ ├── solid.d.ts │ ├── svelte.d.ts │ └── vue.d.ts ├── usal.d.ts └── usal.js ├── test ├── angular │ ├── .gitignore │ ├── angular.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app │ │ │ └── app.ts │ │ ├── index.html │ │ ├── main.server.ts │ │ ├── main.ts │ │ └── server.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── lit │ ├── .gitignore │ ├── .prettierrc.json │ ├── LICENSE │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── pnpm-lock.yaml │ ├── res │ │ └── drawable │ │ │ ├── favicon.svg │ │ │ ├── logo-192x192.png │ │ │ ├── logo-48x48.png │ │ │ ├── logo-512x512.png │ │ │ └── og │ │ │ └── default.png │ ├── src │ │ ├── base │ │ │ ├── app-404.ts │ │ │ ├── app-localstorage.ts │ │ │ ├── app-router.ts │ │ │ └── styles │ │ │ │ ├── default-theme.ts │ │ │ │ └── lit-shared-styles.ts │ │ ├── home │ │ │ └── activities │ │ │ │ └── home-app.ts │ │ ├── lit-app.ts │ │ ├── login │ │ │ └── activities │ │ │ │ └── app-login.ts │ │ └── xconfig │ │ │ ├── strings │ │ │ ├── index.ts │ │ │ └── lang │ │ │ │ └── es.ts │ │ │ └── typings.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── nuxt │ ├── .gitignore │ ├── app │ │ └── app.vue │ ├── nuxt.config.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── react │ ├── .gitignore │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── next.config.ts │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ └── tsconfig.json ├── solid │ ├── .gitignore │ ├── app.config.ts │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.css │ │ ├── app.tsx │ │ ├── components │ │ │ ├── Counter.css │ │ │ └── Counter.tsx │ │ ├── entry-client.tsx │ │ ├── entry-server.tsx │ │ ├── global.d.ts │ │ └── routes │ │ │ ├── [...404].tsx │ │ │ ├── about.tsx │ │ │ └── index.tsx │ └── tsconfig.json ├── svelte │ ├── .gitignore │ ├── .npmrc │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── lib │ │ │ ├── assets │ │ │ │ └── favicon.svg │ │ │ └── index.ts │ │ └── routes │ │ │ ├── +layout.svelte │ │ │ └── +page.svelte │ ├── static │ │ └── robots.txt │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts └── vue │ ├── .gitignore │ ├── index.html │ ├── jsconfig.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── App.vue │ ├── assets │ │ ├── base.css │ │ ├── logo.svg │ │ └── main.css │ ├── components │ │ ├── HelloWorld.vue │ │ ├── TheWelcome.vue │ │ ├── WelcomeItem.vue │ │ └── icons │ │ │ ├── IconCommunity.vue │ │ │ ├── IconDocumentation.vue │ │ │ ├── IconEcosystem.vue │ │ │ ├── IconSupport.vue │ │ │ └── IconTooling.vue │ └── main.js │ └── vite.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=example.com 2 | NEXT_PUBLIC_ALLOWED_HOST=example.com -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: italoalmeida0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | 9 | # Build outputs 10 | dist/ 11 | packages/ 12 | *.tgz 13 | 14 | # Environment variables 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | # IDE/Editor files 22 | .vscode/ 23 | .idea/ 24 | *.swp 25 | *.swo 26 | *~ 27 | *.sublime-project 28 | *.sublime-workspace 29 | 30 | # OS generated files 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | desktop.ini 39 | 40 | # Logs 41 | logs/ 42 | *.log 43 | lerna-debug.log* 44 | .pnpm-debug.log* 45 | 46 | # Runtime data 47 | pids 48 | *.pid 49 | *.seed 50 | *.pid.lock 51 | 52 | # Testing 53 | coverage/ 54 | *.lcov 55 | .nyc_output 56 | 57 | # Caches 58 | .npm 59 | .eslintcache 60 | .stylelintcache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | .cache 66 | .parcel-cache 67 | .turbo 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Yarn 73 | .yarn-integrity 74 | .yarn/* 75 | !.yarn/patches 76 | !.yarn/plugins 77 | !.yarn/releases 78 | !.yarn/sdks 79 | !.yarn/versions 80 | 81 | # pnpm 82 | .pnp.* 83 | .pnpm-store/ 84 | 85 | # Build tools 86 | .grunt 87 | .sass-cache 88 | 89 | # Temporary folders 90 | tmp/ 91 | temp/ 92 | .tmp/ 93 | 94 | # TypeScript 95 | *.tsbuildinfo 96 | .tsc-cache/ 97 | 98 | # Next.js 99 | .next/ 100 | out/ 101 | 102 | # Nuxt.js 103 | .nuxt/ 104 | 105 | # Gatsby 106 | .cache/ 107 | public/ 108 | 109 | # Storybook 110 | .out 111 | .storybook-out 112 | storybook-static/ 113 | 114 | # Rollup 115 | .rollup.cache 116 | 117 | # Vite 118 | vite.config.js.timestamp-* 119 | vite.config.ts.timestamp-* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "always", 11 | "endOfLine": "lf" 12 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to USAL.js will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.3.1] - 2025-10-06 9 | 10 | ### Fixed 11 | 12 | - **Blazor component reuse compatibility**: Fixed edge case where Blazor's DOM element reuse caused animations to trigger on elements without `data-usal` attributes 13 | - USAL now ignores elements with empty or whitespace-only `data-usal` values 14 | - Prevents animations from triggering when Blazor strips attributes during navigation 15 | - Special thanks to [@mdmontesinos](https://github.com/mdmontesinos) for the detailed investigation and testing [TypeError: Cannot read properties of null (reading 'replaceChild') #5](https://github.com/usaljs/usal/issues/5) 16 | 17 | - **Tab visibility desynchronization**: Fixed stagger timing issues when browser tabs become inactive 18 | - Implemented virtual time system capped at 16.67ms per frame 19 | - Stagger delays now remain synchronized after tab switches 20 | - Eliminates animation "bunching" when returning to inactive tabs 21 | - Applies to both split animations and count animations 22 | 23 | - **Console error handling**: Improved error handling for `replaceChild` operations during DOM manipulation 24 | - Elements causing errors are now properly ignored and cleaned up 25 | - No visual impact on animations 26 | - Reduces console spam in edge cases 27 | 28 | - **Split animation nesting**: Fixed excessive DOM nesting in split letter/word animations 29 | - Proper instance detection prevents duplicate wrapper generation 30 | - Cleaner DOM structure with minimal nesting 31 | 32 | ## [1.3.0] - 2025-09-20 33 | 34 | ### Added 35 | 36 | - **Brand identity refresh**: New logos and icon set with dark mode support 37 | - SVG logos with automatic dark/light theme switching 38 | - Complete icon set in multiple sizes (16px to 512px) and formats (PNG, WebP, GIF) 39 | - Updated favicon and social media assets 40 | 41 | - **Text animation effects via timeline**: Shimmer and fluid effects now use timeline syntax 42 | - `text-shimmer`: Converted to `line-[o+50g+100|50o+100g+130|o+50g+100]` 43 | - `text-fluid`: Converted to `line-[w+100|50w+900|w+100]` 44 | - Effects can now be customized and combined with other timeline properties 45 | 46 | - **Timeline property extensions**: New animation properties for advanced effects 47 | - `g±value`: Glow/brightness control (0-100+) 48 | - `w±value`: Font weight morphing (100-900) 49 | 50 | - **Comprehensive test suites**: Added integration tests for all major frameworks 51 | - Angular, React/Next.js, Vue/Nuxt, Svelte/SvelteKit, Solid, Lit 52 | - Framework-specific examples and edge case testing 53 | 54 | ### Fixed 55 | 56 | - **SSR compatibility**: Complete overhaul for server-side rendering safety 57 | - HTML structure preservation during split/count animations 58 | - No element cloning or reconstruction 59 | - Only text nodes modified for text/count animations 60 | - Eliminated hydration mismatches in SSR frameworks 61 | 62 | - **Animation synchronization**: Fixed 5-95% progress bug causing visual artifacts 63 | - Animations now use full 0-100% range with proper edge handling 64 | - Eliminated "snap" effect at animation boundaries 65 | - Improved timing precision and synchronization 66 | 67 | - **Angular integration**: Resolved directive compatibility issues 68 | - Migrated to standalone directive architecture 69 | - Fixed attribute binding with proper `usal="value"` syntax 70 | - Added platform browser checks for SSR safety 71 | 72 | - **Default configuration**: Improved configuration inheritance system 73 | - `config.defaults` now properly cascades through all animation types 74 | - Consistent application of default values across split animations 75 | - Better respect for user-defined defaults 76 | 77 | ### Changed 78 | 79 | - **Documentation improvements**: Reorganized for better discoverability 80 | - Framework usage examples moved directly below installation 81 | - Added Vue.js plugin setup alongside Nuxt configuration 82 | - Updated API documentation with new timeline properties 83 | 84 | - **Animation engine robustness**: Enhanced safety and error handling 85 | - Added processing locks to prevent race conditions 86 | - Improved cleanup with proper promise chains 87 | - Better memory management with element state tracking 88 | - Enhanced edge case handling for complex DOM mutations 89 | 90 | ### Performance 91 | 92 | - **DOM manipulation optimization**: Reduced layout thrashing 93 | - Text node replacement instead of innerHTML manipulation 94 | - RequestAnimationFrame batching for DOM updates 95 | - Eliminated unnecessary style recalculations 96 | 97 | ## [1.2.3] - 2025-09-16 98 | 99 | ### Added 100 | 101 | - **Slide animation**: New `slide` animation type for pure movement without opacity changes 102 | - Supports all directional variants (slide-u, slide-d, slide-l, slide-r, etc.) 103 | - Maintains original element opacity throughout animation 104 | - Useful for elements that need to stay fully visible during entrance 105 | 106 | ### Fixed 107 | 108 | - **Split text HTML preservation**: Split animations now correctly preserve HTML structure 109 | - Bold, italic, and other inline elements are maintained during split 110 | - Nested HTML elements remain properly formatted 111 | - Fixed issue where `textContent` was destroying HTML tags 112 | - **Count animation HTML preservation**: Count animations now work with formatted text 113 | - Preserves surrounding HTML elements when replacing numbers 114 | - Works correctly with nested HTML structures 115 | 116 | - **Animation type persistence**: Fixed parser bug that reset animation types 117 | - Animation configuration no longer lost when processing subsequent tokens 118 | - Fixed issue where all animations were defaulting to fade 119 | 120 | - **Split animation tuning**: Fixed issue where split animations lost tuning values 121 | - Tuning parameters (e.g., fade-u-200) now correctly preserved with split text 122 | - Empty configuration arrays no longer override valid tuning values 123 | 124 | - **Letter animation display**: Fixed inline-block application for split letter animations 125 | - Letters now animate correctly with proper display properties 126 | - Fixed "snap" effect when animations complete 127 | - Proper cleanup maintains inline-block for split elements 128 | 129 | - **Build script compatibility**: Fixed Node.js version compatibility in build script 130 | - Added fallback for `file.path` in recursive directory reading 131 | - Works across Node.js versions 18-24 132 | 133 | ### Changed 134 | 135 | - **Internal refactoring**: Improved code organization 136 | - Extracted `genEmptyConfig()` function for configuration generation 137 | - Renamed `splitByNotItem` to `isSplitText` for clarity 138 | - Better separation of concerns in split and count setup functions 139 | 140 | ### Performance 141 | 142 | - **Animation cleanup**: Improved cleanup of cancelled animations 143 | - Better garbage collection hints with effect/timeline nullification 144 | - More efficient memory management for long-running applications 145 | 146 | ## [1.2.2] - 2025-09-10 147 | 148 | ### Added 149 | 150 | - **Advanced loop types**: New `loop-mirror` and `loop-jump` modifiers for different loop behaviors 151 | - `loop-mirror`: Back and forth animation (default behavior) 152 | - `loop-jump`: Restart animation from beginning 153 | - **Enhanced split delay stagger patterns**: New stagger types for split animations 154 | - `split-delay-{value}-linear`: Linear distance-based stagger 155 | - `split-delay-{value}-center`: Center-outward stagger on X/Y axes 156 | - `split-delay-{value}-edges`: Edges-inward stagger on X/Y axes 157 | - `split-delay-{value}-random`: Random stagger pattern 158 | - **Improved blur precision**: Support for decimal blur values (e.g., `blur-0.5`, `blur-1.5`) 159 | - **Enhanced easing options**: Added `ease-in-out`, `step-start`, and `step-end` easing functions 160 | 161 | ### Changed 162 | 163 | - **Loop configuration**: Default loop type can now be configured via `config.defaults.loop` (default: 'mirror') 164 | - **Playground improvements**: Enhanced controls and preset examples including new stagger effects 165 | - **Animation engine**: Refactored animation controller with better state management and performance 166 | - **Split animation syntax**: Improved parsing for split animations with tuning parameters 167 | 168 | ## [1.2.1] - 2025-09-09 169 | 170 | ### Fixed 171 | 172 | - **Animation tuning**: Fixed parsing of tuning values when no direction is specified (e.g., `fade-50`, `flip-90` now work correctly) 173 | 174 | ## [1.2.0] - 2025-09-09 175 | 176 | ### Added 177 | 178 | - **Loop animations**: New `loop` modifier for continuous animation cycles 179 | - **Custom timeline animations**: Advanced `line-[{timeline}]` syntax for precise keyframe control 180 | - Support for opacity, scale, translate, rotate, blur, and perspective properties 181 | - Multi-keyframe animations with percentage-based timing 182 | - 3D transformations with proper transform order handling 183 | - **Animation tuning**: Numeric parameters for fine-tuning standard animations 184 | - Fade movement distance control (e.g., `fade-40` for 40% movement) 185 | - Zoom intensity and movement customization (e.g., `zoomin-40-60`) 186 | - Flip angle and perspective adjustment (e.g., `flip-120-50`) 187 | - **Enhanced blur effects**: Custom blur intensity with `blur-{value}` syntax 188 | - **Forwards modifier**: `forwards` option to maintain final animation state 189 | - **Comprehensive debug panel**: Extended testing suite for new features 190 | 191 | ### Changed 192 | 193 | - **Bundle size**: Updated from ~5KB to ~8KB gzipped due to new advanced features 194 | - **API documentation**: Moved detailed API docs to separate file and wiki 195 | - **Framework icons**: Added emoji icons to framework setup sections 196 | - **Performance comparison**: Updated competitor bundle sizes and feature matrix 197 | 198 | ### Enhanced 199 | 200 | - **Configuration system**: Extended config array to support new animation types 201 | - **Split animations**: Improved handling with new animation tuning system 202 | - **Transform composition**: Better transform order handling for complex animations 203 | - **Style management**: Enhanced keyframe generation for custom timelines 204 | 205 | ### Fixed 206 | 207 | - **Element cleanup**: Improved cleanup process for disconnected elements 208 | - **Animation state**: Better handling of loop and forwards states 209 | - **Split text**: Enhanced inline-block display for split animations 210 | 211 | ## [1.1.1] - 2025-09-08 212 | 213 | ### Changed 214 | 215 | - Complete API overhaul with promise-based chain system using Rust-like enum states 216 | - Significantly improved edge case handling for extreme usage scenarios 217 | - Better orchestrated state machine for animation lifecycle 218 | - Enhanced public API usability and consistency 219 | - Improved error handling and recovery mechanisms 220 | 221 | ### Added 222 | 223 | - Comprehensive debug panel in source code for development testing (debug.html, debug.css, debug.js) 224 | 225 | ### Fixed 226 | 227 | - Text animations losing characteristics in flex containers without wrapper 228 | - Smooth scroll issue on mobile when switching tabs 229 | 230 | ## [1.1.0] - 2025-09-07 231 | 232 | ### Changed 233 | 234 | - **BREAKING**: Complete migration to Web Animations API (WAAPI) for all animations 235 | - Only count animations remain using RAF for precise number formatting 236 | - Zero direct DOM manipulation - no inline styles or attributes 237 | - Eliminated SSR hydration mismatches 238 | - No longer manipulates HTML node attributes directly 239 | 240 | ### Added 241 | 242 | - Perfect compatibility with React, Vue, Svelte, Solid, and other SSR frameworks 243 | - Better browser optimization and hardware acceleration 244 | 245 | ### Performance 246 | 247 | - Significantly improved performance through WAAPI 248 | - Reduced JavaScript overhead 249 | - Better memory management 250 | 251 | ## [1.0.0] - 2025-09-03 252 | 253 | ### Added 254 | 255 | - Initial release with 40+ scroll-triggered animations 256 | - Intersection Observer based triggers 257 | - Zero dependencies, ~5KB gzipped 258 | - Full Shadow DOM support 259 | - Framework packages for React, Vue, Svelte, Solid, Lit, Angular 260 | - Automatic initialization 261 | - Split text animations (word/letter) 262 | - Count animations with smart number formatting 263 | - Custom easing support 264 | - Blur effects 265 | - Threshold controls 266 | - Duration and delay modifiers 267 | 268 | [1.3.1]: https://github.com/usaljs/usal/compare/v1.3.0...v1.3.1 269 | [1.3.0]: https://github.com/usaljs/usal/compare/v1.2.3...v1.3.0 270 | [1.2.3]: https://github.com/usaljs/usal/compare/v1.2.2...v1.2.3 271 | [1.2.2]: https://github.com/usaljs/usal/compare/v1.2.1...v1.2.2 272 | [1.2.1]: https://github.com/usaljs/usal/compare/v1.2.0...v1.2.1 273 | [1.2.0]: https://github.com/usaljs/usal/compare/v1.1.1...v1.2.0 274 | [1.1.1]: https://github.com/usaljs/usal/compare/v1.1.0...v1.1.1 275 | [1.1.0]: https://github.com/usaljs/usal/compare/v1.0.0...v1.1.0 276 | [1.0.0]: https://github.com/usaljs/usal/releases/tag/v1.0.0 277 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | usal.dev -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Italo Almeida 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 6 | USAL.js 7 | 8 | 9 | ⚡ 10 | 11 | 12 | **Ultimate Scroll Animation Library - Lightweight, powerful, wonderfully simple ✨** 13 | 14 | **Works with React, Solid, Svelte, Vue, Lit, Angular, Vanilla JS and more** 15 | 16 | **[> usal.dev/](https://usal.dev/)** 17 | 18 | [![Available On NPM](https://badgers.space/badge/AVAILABLE%20ON/NPM/red?icon=cssgg-npm&theme=tailwind)](https://npmjs.com/package/usal) [![Join The Community](https://badgers.space/badge/JOIN%20THE%20COMMUNITY/DISCORD/black?icon=feather-message-circle&theme=tailwind)](https://discord.usal.dev/) 19 | 20 | [![Powered by Cloudflare](https://badgers.space/badge/POWERED%20BY/CLOUDFLARE/orange?icon=feather-cloud-lightning&theme=tailwind)](https://www.cloudflare.com/) [![Delivered By JsDelivr](https://badgers.space/badge/DELIVERED%20BY/JSDELIVR/blue?icon=feather-download-cloud&theme=tailwind)](https://jsdelivr.com) [![Sponsor](https://badgers.space/badge/BECOMING%20A/SPONSOR/pink?icon=feather-heart&theme=tailwind)](https://github.com/sponsors/italoalmeida0) 21 | 22 |
23 | 24 | ## ✨ Features 25 | 26 | - 🎯 **40+ animations** (fade, zoom, flip with all directions) 27 | - 📝 **Text animations** (split by word/letter) 28 | - 🔢 **Number counters** 29 | - 🎨 **Text effects** (shimmer, fluid) 30 | - 📦 **Only 8KB Gzipped** 31 | - 🚀 **Zero dependencies** 32 | - ♾ **60fps performance** 33 | - 🪤 **Web components supported** 34 | - 🔧 **Framework agnostic** 35 | - ⚡ **CDN powered** by jsDelivr & Cloudflare 36 | 37 | ## 📦 Installation 38 | 39 | ### CDN (Quickstart) 40 | 41 | ```html 42 | 43 | ``` 44 | 45 | ### NPM 46 | 47 | ```bash 48 | npm install usal 49 | 50 | # Framework-specific packages 51 | npm install @usal/react # For React/Next.js 52 | npm install @usal/solid # For Solid/SolidStart 53 | npm install @usal/svelte # For Svelte/SvelteKit 54 | npm install @usal/vue # For Vue/Nuxt 55 | npm install @usal/lit # For Lit 56 | npm install @usal/angular # For Angular 57 | ``` 58 | 59 | ## 🚀 Framework Setup 60 | 61 | ### ⬛ React (Next.js) 62 | 63 | ```jsx 64 | import { USALProvider } from '@usal/react'; 65 | {children}; 66 | ``` 67 | 68 | ### 🟦 Solid (SolidStart) 69 | 70 | ```jsx 71 | import { USALProvider } from '@usal/solid'; 72 | {props.children}; 73 | ``` 74 | 75 | ### 🟧 Svelte (SvelteKit) 76 | 77 | ```js 78 | import { usal } from '@usal/svelte'; 79 | // USAL auto-initializes globally 80 | ``` 81 | 82 | ### 🟩 Vue (Nuxt) 83 | 84 | ```js 85 | import { USALPlugin } from '@usal/vue'; 86 | createApp(App).use(USALPlugin).mount('#app'); 87 | //for Nuxt 88 | export default defineNuxtConfig({ 89 | modules: ['@usal/vue/nuxt'] 90 | //... 91 | ``` 92 | 93 | ### 🟪 Lit 94 | 95 | ```js 96 | import { usal } from '@usal/lit'; 97 | // USAL auto-initializes globally 98 | ``` 99 | 100 | ### 🟥 Angular 101 | 102 | ```js 103 | import { USALModule } from '@usal/angular'; 104 | @Component({imports: [USALModule]}) 105 | export class AppComponent 106 | ``` 107 | 108 | ## 📐 Basic Usage 109 | 110 | ```html 111 | 112 |
Fade from bottom
113 | 114 | 115 |
Zoom in
116 | 117 | 118 |
Flip from right
119 | ``` 120 | 121 | ## [📖 Complete API Documentation](https://github.com/usaljs/usal/wiki/API-Documentation) or https://usal.dev/#api 122 | 123 | ## 🎲 Demos 124 | 125 | ![Animations](https://github.com/usaljs/usal/raw/main/assets/all.gif) 126 | 127 | ![Text Animations](https://github.com/usaljs/usal/raw/main/assets/text.gif) 128 | 129 | ![Count Animations](https://github.com/usaljs/usal/raw/main/assets/count.gif) 130 | 131 | ![Split Animations](https://github.com/usaljs/usal/raw/main/assets/split.gif) 132 | 133 | ## 📊 Packages Overview 134 | 135 | | Package | Version | 136 | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | 137 | | `usal` | ![npm](https://badge.usal.dev/?nn&ps=%40usal%2Freact%2C%40usal%2Fsolid%2C%40usal%2Fsvelte%2C%40usal%2Fvue%2C%40usal%2Flit%2C%40usal%2Fangular) | 138 | | `@usal/react` | ![npm](https://badge.usal.dev/?nn&p=%40usal%2Freact&color=grey) | 139 | | `@usal/solid` | ![npm](https://badge.usal.dev/?nn&p=%40usal%2Fsolid&color=blue) | 140 | | `@usal/svelte` | ![npm](https://badge.usal.dev/?nn&p=%40usal%2Fsvelte&color=orange) | 141 | | `@usal/vue` | ![npm](https://badge.usal.dev/?nn&p=%40usal%2Fvue&color=green) | 142 | | `@usal/lit` | ![npm](https://badge.usal.dev/?nn&p=%40usal%2Flit&color=cyan) | 143 | | `@usal/angular` | ![npm](https://badge.usal.dev/?nn&p=%40usal%2Fangular&color=red) | 144 | 145 | ## 📈 JavaScript Animation Frameworks Comparison (2025) 146 | 147 | ### Performance & Size Comparison 148 | 149 | | Framework | Bundle Size (gzip) | React | Vue | Angular | Svelte | Solid | Lit | Vanilla | 150 | | -------------- | ------------------ | --------- | --------- | --------- | --------- | --------- | --------- | ------- | 151 | | **🚀 USAL.js** | **~8KB** | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ | 152 | | Motion One | Variable (~small) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | 153 | | GSAP | ~28KB | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 154 | | Anime.js v4 | ~27KB | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 155 | | Lottie | ~60KB | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ✅ | 156 | | AOS | ~8KB | ⚠️ | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ✅ | 157 | | SAL.js | ~2.7KB | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | 158 | 159 | ### Feature Comparison 160 | 161 | | Framework | Split (Letters/Words/Items) | Counters | Scroll Trigger | Timeline | SVG | Learning | 162 | | ----------- | --------------------------- | --------- | -------------- | ----------- | --------- | ------------- | 163 | | **USAL.js** | ✅ Core | ✅ Core | ✅ Core | ✅ Core | ❌ | **Very Easy** | 164 | | Motion | ❌ | ❌ | ✅ Core | ⚠️ Variants | ✅ Core | Medium | 165 | | GSAP | ⚠️ Plugin | ⚠️ Plugin | ✅ Plugin | ✅ Advanced | ⚠️ Plugin | Complex | 166 | | Anime.js v4 | ✅ Core | ✅ Core | ✅ Core | ✅ Core | ✅ Core | Medium | 167 | | Lottie | ⚠️ via AE | ⚠️ via AE | ❌ | ✅ Core | ✅ Core | Complex | 168 | | AOS | ❌ | ❌ | ✅ Core | ❌ | ❌ | Very Easy | 169 | | SAL.js | ❌ | ❌ | ✅ Core | ❌ | ❌ | Very Easy | 170 | 171 | **Legend:** 172 | 173 | - ✅ Native/Core support 174 | - ⚠️ Plugin/Wrapper required 175 | - ❌ Not supported 176 | 177 | ## 🙏 Acknowledgments 178 | 179 | USAL.js was inspired by: 180 | 181 | - **[AOS.js](https://github.com/michalsnik/aos)** - Pioneering attribute-based animations 182 | - **[SAL.js](https://github.com/mciastek/sal)** - Lightweight performance optimization 183 | - **[Tailwind CSS](https://tailwindcss.com)** - Utility-first naming conventions 184 | 185 | --- 186 | 187 | ## 📄 License 188 | 189 | MIT License © 2025 Italo Almeida ([@italoalmeida0](https://github.com/italoalmeida0)) 190 | -------------------------------------------------------------------------------- /assets/all.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/all.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon.svg: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | -------------------------------------------------------------------------------- /assets/brand/usal_icon_144.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_144.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_144.png -------------------------------------------------------------------------------- /assets/brand/usal_icon_144.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_144.webp -------------------------------------------------------------------------------- /assets/brand/usal_icon_16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_16.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_16.png -------------------------------------------------------------------------------- /assets/brand/usal_icon_16.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_16.webp -------------------------------------------------------------------------------- /assets/brand/usal_icon_192.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_192.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_192.png -------------------------------------------------------------------------------- /assets/brand/usal_icon_192.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_192.webp -------------------------------------------------------------------------------- /assets/brand/usal_icon_32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_32.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_32.png -------------------------------------------------------------------------------- /assets/brand/usal_icon_32.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_32.webp -------------------------------------------------------------------------------- /assets/brand/usal_icon_48.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_48.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_48.png -------------------------------------------------------------------------------- /assets/brand/usal_icon_48.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_48.webp -------------------------------------------------------------------------------- /assets/brand/usal_icon_512.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_512.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_512.png -------------------------------------------------------------------------------- /assets/brand/usal_icon_512.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_512.webp -------------------------------------------------------------------------------- /assets/brand/usal_icon_72.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_72.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon_72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_72.png -------------------------------------------------------------------------------- /assets/brand/usal_icon_72.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_72.webp -------------------------------------------------------------------------------- /assets/brand/usal_icon_96.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_96.gif -------------------------------------------------------------------------------- /assets/brand/usal_icon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_96.png -------------------------------------------------------------------------------- /assets/brand/usal_icon_96.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/brand/usal_icon_96.webp -------------------------------------------------------------------------------- /assets/brand/usal_logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/brand/usal_logo_dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/count.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/count.gif -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/logo.png -------------------------------------------------------------------------------- /assets/split.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/split.gif -------------------------------------------------------------------------------- /assets/text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/assets/text.gif -------------------------------------------------------------------------------- /dev/debug.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: 'Consolas', 'Monaco', 'Courier New', monospace; 9 | background: #0a0a0a; 10 | color: #888; 11 | padding: 20px; 12 | padding-right: 440px; 13 | line-height: 1.6; 14 | } 15 | 16 | h1, 17 | h2 { 18 | color: #fff; 19 | margin-bottom: 20px; 20 | } 21 | 22 | h2 { 23 | color: #666; 24 | font-size: 1.2rem; 25 | border-bottom: 1px solid #222; 26 | padding-bottom: 10px; 27 | } 28 | 29 | /* Control Panel */ 30 | .controls { 31 | position: fixed; 32 | top: 0; 33 | right: 0; 34 | background: #0a0a0a; 35 | border-left: 1px solid #333; 36 | padding: 20px; 37 | width: 420px; 38 | height: 100vh; 39 | overflow-y: auto; 40 | box-shadow: -5px 0 20px rgba(0, 0, 0, 0.5); 41 | z-index: 1000; 42 | } 43 | 44 | .controls h3 { 45 | color: #fff; 46 | margin: 15px 0 10px; 47 | font-size: 0.9rem; 48 | border-top: 1px solid #333; 49 | padding-top: 10px; 50 | } 51 | 52 | .controls h3:first-child { 53 | border-top: none; 54 | padding-top: 0; 55 | margin-top: 0; 56 | } 57 | 58 | .controls button { 59 | display: inline-block; 60 | margin: 3px; 61 | padding: 6px 10px; 62 | background: #1a1a1a; 63 | color: #888; 64 | border: 1px solid #333; 65 | cursor: pointer; 66 | font-family: monospace; 67 | font-size: 11px; 68 | transition: all 0.2s; 69 | } 70 | 71 | .controls button:hover { 72 | background: #222; 73 | color: #fff; 74 | border-color: #444; 75 | } 76 | 77 | .controls button:active { 78 | transform: scale(0.98); 79 | } 80 | 81 | .controls button.active { 82 | background: #333; 83 | color: #4ade80; 84 | border-color: #4ade80; 85 | } 86 | 87 | /* Sections */ 88 | .section { 89 | margin: 40px 0; 90 | padding: 20px; 91 | border: 1px solid #222; 92 | background: #0f0f0f; 93 | } 94 | 95 | .grid { 96 | display: grid; 97 | grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); 98 | gap: 10px; 99 | margin: 20px 0; 100 | } 101 | 102 | /* Test Boxes */ 103 | .test-box { 104 | background: #111; 105 | border: 1px solid #222; 106 | padding: 15px; 107 | text-align: center; 108 | min-height: 80px; 109 | display: flex; 110 | flex-direction: column; 111 | justify-content: center; 112 | align-items: center; 113 | position: relative; 114 | color: #666; 115 | font-size: 12px; 116 | transition: all 0.3s; 117 | } 118 | 119 | .test-box:hover { 120 | border-color: #444; 121 | } 122 | 123 | .test-box::before { 124 | content: attr(data-usal); 125 | position: absolute; 126 | top: 2px; 127 | left: 2px; 128 | right: 2px; 129 | font-size: 8px; 130 | color: #333; 131 | text-align: left; 132 | white-space: nowrap; 133 | overflow: hidden; 134 | text-overflow: ellipsis; 135 | } 136 | 137 | .test-box.has-animation { 138 | border-color: #4ade80; 139 | box-shadow: 0 0 10px rgba(74, 222, 128, 0.2); 140 | } 141 | 142 | .test-box.has-fragment { 143 | background: #1a1a0a; 144 | } 145 | 146 | /* Monitor displays */ 147 | .monitor { 148 | background: #0a0a0a; 149 | border: 1px solid #222; 150 | padding: 8px; 151 | margin: 10px 0; 152 | font-size: 10px; 153 | font-family: monospace; 154 | max-height: 200px; 155 | overflow-y: auto; 156 | } 157 | 158 | .monitor-item { 159 | padding: 3px 5px; 160 | margin: 2px 0; 161 | border-left: 2px solid #333; 162 | padding-left: 8px; 163 | } 164 | 165 | .monitor-item.active { 166 | border-left-color: #4ade80; 167 | color: #4ade80; 168 | } 169 | 170 | .monitor-item.pending { 171 | border-left-color: #fbbf24; 172 | color: #fbbf24; 173 | } 174 | 175 | .monitor-item.idle { 176 | border-left-color: #666; 177 | color: #666; 178 | } 179 | 180 | /* Status Display */ 181 | #status { 182 | position: fixed; 183 | bottom: 20px; 184 | right: 440px; 185 | background: #111; 186 | border: 1px solid #333; 187 | padding: 10px; 188 | font-size: 11px; 189 | color: #666; 190 | font-family: monospace; 191 | z-index: 100; 192 | } 193 | 194 | .status-item { 195 | display: flex; 196 | justify-content: space-between; 197 | margin: 2px 0; 198 | min-width: 150px; 199 | } 200 | 201 | .status-value { 202 | color: #fff; 203 | margin-left: 10px; 204 | } 205 | 206 | .status-value.active { 207 | color: #4ade80; 208 | } 209 | 210 | .status-value.inactive { 211 | color: #ef4444; 212 | } 213 | 214 | /* Shadow DOM Styles */ 215 | .shadow-host { 216 | background: #0a0a0a; 217 | border: 2px dashed #333; 218 | padding: 20px; 219 | margin: 20px 0; 220 | position: relative; 221 | } 222 | 223 | .shadow-host::before { 224 | content: 'Shadow DOM'; 225 | position: absolute; 226 | top: -10px; 227 | left: 10px; 228 | background: #0a0a0a; 229 | padding: 0 5px; 230 | color: #444; 231 | font-size: 10px; 232 | } 233 | 234 | /* Stress Test Grid */ 235 | .stress-grid { 236 | display: grid; 237 | grid-template-columns: repeat(20, 1fr); 238 | gap: 2px; 239 | margin: 20px 0; 240 | } 241 | 242 | .stress-item { 243 | width: 100%; 244 | aspect-ratio: 1; 245 | background: #111; 246 | border: 1px solid #222; 247 | transition: border-color 0.3s; 248 | } 249 | 250 | .stress-item:hover { 251 | border-color: #444; 252 | } 253 | 254 | /* Memory Info */ 255 | .memory-info { 256 | background: #0a0a0a; 257 | border: 1px solid #222; 258 | padding: 8px; 259 | margin: 10px 0; 260 | font-size: 10px; 261 | display: grid; 262 | grid-template-columns: 1fr 1fr; 263 | gap: 10px; 264 | } 265 | 266 | .memory-stat { 267 | display: flex; 268 | justify-content: space-between; 269 | } 270 | 271 | .memory-value { 272 | color: #fbbf24; 273 | } 274 | 275 | /* Config Display */ 276 | .config-display { 277 | background: #0a0a0a; 278 | border: 1px solid #222; 279 | padding: 8px; 280 | margin: 10px 0; 281 | font-size: 10px; 282 | white-space: pre; 283 | overflow-x: auto; 284 | color: #666; 285 | font-family: monospace; 286 | } 287 | 288 | /* Form Elements */ 289 | input, 290 | select { 291 | background: #1a1a1a; 292 | color: #888; 293 | border: 1px solid #333; 294 | padding: 4px 8px; 295 | margin: 3px; 296 | font-family: monospace; 297 | font-size: 11px; 298 | transition: all 0.2s; 299 | } 300 | 301 | input:focus, 302 | select:focus { 303 | outline: none; 304 | border-color: #444; 305 | color: #fff; 306 | background: #222; 307 | } 308 | 309 | input[type='number'] { 310 | width: 80px; 311 | } 312 | 313 | /* Test Runner */ 314 | .test-runner { 315 | background: #0f0f0f; 316 | border: 1px solid #222; 317 | padding: 10px; 318 | margin: 10px 0; 319 | max-height: 300px; 320 | overflow-y: auto; 321 | } 322 | 323 | .test-result { 324 | padding: 5px; 325 | margin: 3px 0; 326 | font-size: 11px; 327 | border-left: 2px solid transparent; 328 | padding-left: 8px; 329 | } 330 | 331 | .test-pass { 332 | color: #4ade80; 333 | border-left-color: #4ade80; 334 | } 335 | 336 | .test-fail { 337 | color: #ef4444; 338 | border-left-color: #ef4444; 339 | } 340 | 341 | .test-warn { 342 | color: #fbbf24; 343 | border-left-color: #fbbf24; 344 | } 345 | 346 | /* Performance Metrics */ 347 | .perf-display { 348 | display: grid; 349 | grid-template-columns: repeat(3, 1fr); 350 | gap: 10px; 351 | margin: 10px 0; 352 | } 353 | 354 | .perf-metric { 355 | background: #0a0a0a; 356 | border: 1px solid #222; 357 | padding: 8px; 358 | text-align: center; 359 | transition: all 0.3s; 360 | } 361 | 362 | .perf-metric:hover { 363 | border-color: #333; 364 | } 365 | 366 | .perf-value { 367 | font-size: 18px; 368 | color: #4ade80; 369 | font-weight: bold; 370 | } 371 | 372 | .perf-label { 373 | font-size: 10px; 374 | color: #666; 375 | margin-top: 5px; 376 | text-transform: uppercase; 377 | letter-spacing: 1px; 378 | } 379 | 380 | /* Scrollbar Styling */ 381 | ::-webkit-scrollbar { 382 | width: 8px; 383 | height: 8px; 384 | } 385 | 386 | ::-webkit-scrollbar-track { 387 | background: #0a0a0a; 388 | } 389 | 390 | ::-webkit-scrollbar-thumb { 391 | background: #333; 392 | border-radius: 4px; 393 | } 394 | 395 | ::-webkit-scrollbar-thumb:hover { 396 | background: #444; 397 | } 398 | 399 | /* Responsive adjustments */ 400 | @media (max-width: 1200px) { 401 | body { 402 | padding-right: 20px; 403 | } 404 | 405 | .controls { 406 | width: 350px; 407 | } 408 | 409 | #status { 410 | right: 360px; 411 | } 412 | } 413 | 414 | @media (max-width: 768px) { 415 | .controls { 416 | position: static; 417 | width: 100%; 418 | height: auto; 419 | border: 1px solid #333; 420 | margin-bottom: 20px; 421 | } 422 | 423 | #status { 424 | right: 20px; 425 | } 426 | 427 | .grid { 428 | grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | usal.dev -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italoalmeida0/usal/ea9b62ba1cf004630eff9b792a426a2bf64f222e/docs/favicon.png -------------------------------------------------------------------------------- /docs/llms.txt: -------------------------------------------------------------------------------- 1 | # USAL.js 2 | 3 | > USAL.js is a lightweight, powerful JavaScript library that brings together scroll animations, text effects, number counters, and custom timelines into a single 8KB package. Framework-agnostic with native support for React, Solid, Svelte, Vue, Lit, Angular, and Vanilla JS. Uses Tailwind-inspired utility-first syntax for creating 60fps scroll-triggered animations. 4 | 5 | Things to remember when using USAL.js: 6 | 7 | - USAL.js uses data attributes for configuration, inspired by Tailwind CSS utility-first approach - no complex JavaScript initialization required 8 | - The library is framework-agnostic but provides native integrations for all major frameworks, not just compatibility wrappers 9 | - Split animations (word/letter/item) and number counters are built-in, unlike most animation libraries that require plugins 10 | - Custom timeline animations provide advanced control with `line-[{timeline}]` syntax for complex keyframe sequences 11 | - All animations are hardware-accelerated using CSS transforms and run at 60fps with automatic cleanup 12 | - Use `threshold-{value}` for large elements (>100vh height) to ensure proper trigger timing 13 | 14 | ## Docs 15 | 16 | - [Complete API Documentation](https://github.com/usaljs/usal/wiki/API-Documentation): Full reference with examples and advanced usage patterns 17 | - [API Summary](https://github.com/usaljs/usal/wiki/API-Summary): Quick reference of all animations, modifiers, and syntax 18 | - [Interactive Playground](https://playground.usal.dev/): Live code editor to test animations and generate code 19 | - [Main Website](https://usal.dev/): Interactive demos with source code for every animation type 20 | 21 | ## Installation & Setup 22 | 23 | ### CDN (Quickstart) 24 | ```html 25 | 26 | ``` 27 | 28 | ### NPM 29 | ```bash 30 | npm install usal # Core library 31 | npm install @usal/react # React/Next.js 32 | npm install @usal/solid # Solid/SolidStart 33 | npm install @usal/svelte # Svelte/SvelteKit 34 | npm install @usal/vue # Vue/Nuxt 35 | npm install @usal/lit # Lit 36 | npm install @usal/angular # Angular 37 | ``` 38 | 39 | ## Minimal API Reference 40 | 41 | ### Base Animations 42 | - **Fade:** `fade` | `fade-{direction}` | `fade-{distance}` | `fade-{distance}-{distance}` | `fade-{direction}-{distance}` | `fade-{direction}-{distance}-{distance}` 43 | - **Slide:** `slide` | `slide-{direction}` | `slide-{distance}` | `slide-{distance}-{distance}` | `slide-{direction}-{distance}` | `slide-{direction}-{distance}-{distance}` 44 | - **Zoom In:** `zoomin` | `zoomin-{direction}` | `zoomin-{intensity}` | `zoomin-{distance}-{intensity}` | `zoomin-{distance}-{distance}-{intensity}` | `zoomin-{direction}-{intensity}` | `zoomin-{direction}-{distance}-{intensity}` | `zoomin-{direction}-{distance}-{distance}-{intensity}` 45 | - **Zoom Out:** `zoomout` | `zoomout-{direction}` | `zoomout-{intensity}` | `zoomout-{distance}-{intensity}` | `zoomout-{distance}-{distance}-{intensity}` | `zoomout-{direction}-{intensity}` | `zoomout-{direction}-{distance}-{intensity}` | `zoomout-{direction}-{distance}-{distance}-{intensity}` 46 | - **Flip:** `flip` | `flip-{direction}` | `flip-{angle}` | `flip-{angle}-{perspective}` | `flip-{direction}-{angle}` | `flip-{direction}-{angle}-{perspective}` 47 | 48 | 49 | **Directions:** u, d, l, r, ul, ur, dl, dr 50 | 51 | ### Split Animations 52 | - **Types:** `split-word` | `split-letter` | `split-item` 53 | - **Combined:** `split-{animation}-{direction}` | `split-{animation}-{direction}-{values}` 54 | - **Delays:** `split-delay-{milliseconds}` | `split-delay-{milliseconds}-{stagger}` 55 | - **Stagger types:** linear, center, edges, random 56 | 57 | ### Counters & Effects 58 | - **Count:** `count-[{target}]` (target: numbers, spaces, dots, commas only) 59 | - **Text Effects:** `text-shimmer` | `text-fluid` (requires split-letter) 60 | 61 | ### Custom Timeline 62 | - **Syntax:** `line-[{timeline}]` 63 | - **Properties:** `o±{value}` (opacity 0-100) | `s±{value}` (scale) | `sx/sy/sz±{value}` (scale axis) | `t±{value}` (translateX %) | `tx/ty/tz±{value}` (translate axis %) | `r±{value}` (rotateZ °) | `rx/ry/rz±{value}` (rotate axis °) | `b±{value}` (blur rem) | `g±{value}` (glow/brightness %) | `w±{value}` (font weight 100-900) | `p±{value}` (perspective rem) 64 | - **Keyframes:** `|` (separator) | `|{percentage}` (% position) 65 | 66 | ### Modifiers 67 | - **Timing:** `duration-{ms}` | `delay-{ms}` 68 | - **Easing:** `linear` | `ease` | `ease-in` | `ease-out` | `ease-in-out` | `step-start` | `step-end` | `easing-[{css-function}]` 69 | - **Trigger:** `threshold-{percentage}` 70 | - **Effects:** `blur` | `blur-{rem}` | `once` | `loop` | `loop-{type}` | `forwards` 71 | - **Loop types:** mirror, jump 72 | 73 | ## Framework Usage Examples 74 | 75 | ```html 76 | 77 |
Content
78 | 79 | 80 |
Content
81 | 82 | 83 |
Content
84 | 85 | 86 |
Content
87 | 88 | 89 |
Content
90 | 91 | 92 |
Content
93 | 94 | 95 |
Content
96 | ``` 97 | 98 | ## JavaScript API 99 | 100 | ```javascript 101 | // Configuration 102 | window.USAL.config({ 103 | defaults: { 104 | animation: 'fade', 105 | direction: 'u', 106 | duration: 1000, 107 | delay: 0, 108 | threshold: 10, 109 | splitDelay: 30, 110 | easing: 'ease-out', 111 | blur: false, 112 | loop: 'mirror' 113 | }, 114 | observersDelay: 50, 115 | once: false 116 | }); 117 | 118 | // Control methods 119 | window.USAL.initialized() // Check if running (auto initialized) 120 | window.USAL.restart() // Restart USAL, use only extreme cases, the system is reactive even in shadowRoot 121 | window.USAL.destroy() // Shut down, use only extreme cases, the system is reactive even in shadowRoot 122 | window.USAL.version // Get version 123 | ``` 124 | 125 | ## Performance Comparison (2025) 126 | 127 | | Framework | Bundle Size | Framework Support | Split Animations | Number Counters | Learning Curve | 128 | |-----------|-------------|-------------------|------------------|-----------------|----------------| 129 | | **USAL.js** | **~8KB** | **All major + native** | **✅ Core** | **✅ Core** | **Very Easy** | 130 | | GSAP | ~28KB | All major | ⚠️ Plugin | ⚠️ Plugin | Complex | 131 | | Anime.js v4 | ~27KB | All major | ✅ Core | ✅ Core | Medium | 132 | | Motion One | Variable | Limited | ❌ | ❌ | Medium | 133 | | Lottie | ~60KB | Limited | ⚠️ via AE | ⚠️ via AE | Complex | 134 | | AOS | ~8KB | ⚠️ Limited | ❌ | ❌ | Very Easy | 135 | 136 | ## Key Features 137 | 138 | - **40+ animations** with directional variations (fade, zoom, flip) 139 | - **Text animations** (split by word/letter/item with stagger options) 140 | - **Number counters** with formatting preservation 141 | - **Text effects** (shimmer, fluid morphing) 142 | - **Custom timeline** system for complex keyframe animations 143 | - **Animation tuning** with numeric parameters 144 | - **Framework agnostic** with native integrations 145 | - **8KB gzipped** with zero dependencies 146 | - **60fps performance** with hardware acceleration 147 | - **CDN powered** by jsDelivr & Cloudflare 148 | 149 | ## Use Cases 150 | 151 | - Landing pages and marketing sites with scroll-triggered reveals 152 | - Product showcases and portfolios with staggered animations 153 | - SaaS applications with smooth onboarding flows 154 | - E-commerce sites with product reveal animations 155 | - Blog and content sites with reading experience enhancements 156 | - Mobile-responsive applications with touch-optimized animations 157 | 158 | ## Technical Details 159 | 160 | - Built with modern JavaScript (ES6+) and TypeScript support 161 | - Uses Intersection Observer API for optimal performance 162 | - CSS transform-based animations for hardware acceleration 163 | - Automatic cleanup and memory management 164 | - No jQuery or external dependencies 165 | - Graceful degradation for older browsers 166 | - Works with Web Components and Shadow DOM 167 | 168 | ## Community & Support 169 | 170 | - **GitHub Repository:** https://github.com/usaljs/usal 171 | - **Issues & Bug Reports:** https://github.com/usaljs/usal/issues 172 | - **NPM Package:** https://npmjs.com/package/usal 173 | - **CDN:** https://cdn.usal.dev/latest 174 | - **Creator:** [@italoalmeida0](https://github.com/italoalmeida0) 175 | 176 | ## Inspiration 177 | 178 | USAL.js was inspired by: 179 | - **AOS.js** - Pioneering attribute-based animations 180 | - **SAL.js** - Lightweight performance optimization 181 | - **Tailwind CSS** - Utility-first naming conventions 182 | 183 | The goal was to combine the best aspects of these libraries while adding modern framework support, advanced features like split animations and custom timelines, and maintaining minimal bundle size. 184 | 185 | ## License 186 | 187 | MIT License © 2025 Italo Almeida 188 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import typescriptPlugin from '@typescript-eslint/eslint-plugin'; 3 | import typescriptParser from '@typescript-eslint/parser'; 4 | import prettierConfig from 'eslint-config-prettier'; 5 | import compatPlugin from 'eslint-plugin-compat'; 6 | import importPlugin from 'eslint-plugin-import'; 7 | import sonarjs from 'eslint-plugin-sonarjs'; 8 | import globals from 'globals'; 9 | 10 | export default [ 11 | js.configs.recommended, 12 | sonarjs.configs.recommended, 13 | prettierConfig, 14 | compatPlugin.configs['flat/recommended'], 15 | { 16 | files: ['**/*.{js,mjs,cjs,ts}'], 17 | languageOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | globals: { 21 | ...globals.browser, 22 | ...globals.node, 23 | }, 24 | }, 25 | plugins: { 26 | import: importPlugin, 27 | }, 28 | rules: { 29 | 'no-console': ['warn', { allow: ['warn', 'error', 'log'] }], 30 | 'no-unused-vars': [ 31 | 'warn', 32 | { 33 | argsIgnorePattern: '^_', 34 | varsIgnorePattern: '^_', 35 | }, 36 | ], 37 | 'no-debugger': 'warn', 38 | 'prefer-const': 'error', 39 | 'no-var': 'error', 40 | eqeqeq: ['error', 'always', { null: 'ignore' }], 41 | 'arrow-body-style': ['error', 'as-needed'], 42 | 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], 43 | 'no-duplicate-imports': 'error', 44 | 'no-useless-return': 'error', 45 | 'no-else-return': 'error', 46 | 47 | 'no-constant-condition': 'error', 48 | 'no-unreachable': 'error', 49 | 'no-unneeded-ternary': 'error', 50 | 'no-useless-computed-key': 'error', 51 | 'no-useless-constructor': 'error', 52 | 'no-useless-escape': 'error', 53 | 'no-useless-rename': 'error', 54 | 55 | 'sonarjs/no-identical-conditions': 'error', 56 | 'sonarjs/no-duplicated-branches': 'error', 57 | 'sonarjs/no-redundant-boolean': 'error', 58 | 'sonarjs/prefer-single-boolean-return': 'error', 59 | 'sonarjs/no-identical-expressions': 'error', 60 | 'sonarjs/no-useless-catch': 'error', 61 | 'sonarjs/prefer-immediate-return': 'error', 62 | 'sonarjs/no-redundant-jump': 'error', 63 | 'sonarjs/no-same-line-conditional': 'error', 64 | 'sonarjs/no-collapsible-if': 'error', 65 | 'sonarjs/no-inverted-boolean-check': 'error', 66 | 67 | 'sonarjs/cognitive-complexity': ['warn', 15], 68 | 'sonarjs/max-switch-cases': ['warn', 30], 69 | 'sonarjs/no-nested-switch': 'off', 70 | 'sonarjs/no-nested-template-literals': 'warn', 71 | 'sonarjs/no-nested-conditional': 'off', 72 | 'sonarjs/no-nested-functions': 'off', 73 | 'sonarjs/pseudo-random': 'off', 74 | 75 | 'import/order': [ 76 | 'error', 77 | { 78 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 79 | 'newlines-between': 'always', 80 | alphabetize: { 81 | order: 'asc', 82 | caseInsensitive: true, 83 | }, 84 | }, 85 | ], 86 | }, 87 | }, 88 | { 89 | files: ['**/*.ts'], 90 | languageOptions: { 91 | parser: typescriptParser, 92 | parserOptions: { 93 | project: false, 94 | }, 95 | }, 96 | plugins: { 97 | '@typescript-eslint': typescriptPlugin, 98 | }, 99 | rules: { 100 | ...typescriptPlugin.configs.recommended.rules, 101 | 'no-unused-vars': 'off', 102 | '@typescript-eslint/no-unused-vars': [ 103 | 'warn', 104 | { 105 | argsIgnorePattern: '^_', 106 | varsIgnorePattern: '^_', 107 | }, 108 | ], 109 | '@typescript-eslint/no-explicit-any': 'warn', 110 | '@typescript-eslint/no-require-imports': 'off', 111 | }, 112 | }, 113 | { 114 | files: ['src/usal.js', 'src/usal.ts'], 115 | rules: { 116 | 'no-console': 'off', 117 | complexity: 'off', 118 | 'max-depth': 'off', 119 | 'max-lines': 'off', 120 | 'sonarjs/cognitive-complexity': 'off', 121 | }, 122 | }, 123 | { 124 | files: ['src/integrations/react/**/*.{js,ts}'], 125 | languageOptions: { 126 | parserOptions: { 127 | ecmaFeatures: { 128 | jsx: true, 129 | }, 130 | }, 131 | }, 132 | rules: { 133 | 'no-unused-vars': [ 134 | 'warn', 135 | { 136 | argsIgnorePattern: '^_', 137 | varsIgnorePattern: '^_|React', 138 | }, 139 | ], 140 | }, 141 | }, 142 | { 143 | files: ['src/integrations/vue/**/*.{js,ts}'], 144 | rules: { 145 | 'no-unused-vars': [ 146 | 'warn', 147 | { 148 | argsIgnorePattern: '^_|h', 149 | varsIgnorePattern: '^_|h', 150 | }, 151 | ], 152 | }, 153 | }, 154 | { 155 | files: ['src/integrations/lit/**/*.{js,ts}'], 156 | rules: { 157 | 'no-unused-vars': [ 158 | 'warn', 159 | { 160 | argsIgnorePattern: '^_', 161 | varsIgnorePattern: '^_|html|css|LitElement', 162 | }, 163 | ], 164 | }, 165 | }, 166 | { 167 | files: ['src/integrations/angular/**/*.ts'], 168 | rules: { 169 | '@typescript-eslint/no-explicit-any': 'off', 170 | '@typescript-eslint/no-unused-vars': [ 171 | 'warn', 172 | { 173 | argsIgnorePattern: '^_', 174 | varsIgnorePattern: '^_|Injectable|Directive|Input', 175 | }, 176 | ], 177 | }, 178 | }, 179 | { 180 | files: ['build.js', 'update-tags.js', 'postbuild.js', '*.config.{js,mjs}'], 181 | rules: { 182 | 'no-console': 'off', 183 | '@typescript-eslint/no-var-requires': 'off', 184 | '@typescript-eslint/no-require-imports': 'off', 185 | }, 186 | }, 187 | { 188 | files: ['**/*.test.{js,ts}', '**/*.spec.{js,ts}'], 189 | rules: { 190 | 'no-console': 'off', 191 | }, 192 | }, 193 | { 194 | ignores: [ 195 | '**/node_modules/**', 196 | '**/packages/**', 197 | '**/dist/**', 198 | '**/*.min.js', 199 | '**/coverage/**', 200 | '**/*.d.ts', 201 | '**/test/**', 202 | ], 203 | }, 204 | ]; 205 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usal-monorepo", 3 | "version": "1.3.1", 4 | "private": true, 5 | "type": "module", 6 | "description": "Ultimate Scroll Animation Library - Lightweight, powerful, wonderfully simple ✨", 7 | "homepage": "https://usal.dev/", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/usaljs/usal.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/usaljs/usal/issues" 14 | }, 15 | "author": "Italo Almeida", 16 | "license": "MIT", 17 | "workspaces": [ 18 | "packages/*" 19 | ], 20 | "packages": { 21 | "react": { 22 | "dependencies": { 23 | "react": [ 24 | ">=16.8.0", 25 | false 26 | ] 27 | }, 28 | "jsx": true, 29 | "usage": { 30 | "color": "grey", 31 | "pseudonym": [ 32 | "Next.js" 33 | ], 34 | "file": "index.js/layout.ts", 35 | "import": "import { USALProvider } from '@usal/react';", 36 | "start": "{children}", 37 | "prop": "data-usal=\"VALUES\"" 38 | } 39 | }, 40 | "solid": { 41 | "dependencies": { 42 | "solid-js": [ 43 | ">=1.0.0", 44 | false 45 | ] 46 | }, 47 | "usage": { 48 | "color": "blue", 49 | "pseudonym": [ 50 | "SolidStart" 51 | ], 52 | "file": "app.tsx", 53 | "import": "import { USALProvider } from '@usal/solid';", 54 | "start": "{props.children}", 55 | "prop": "data-usal=\"VALUES\"" 56 | } 57 | }, 58 | "svelte": { 59 | "dependencies": { 60 | "svelte": [ 61 | ">=3.0.0", 62 | false 63 | ] 64 | }, 65 | "usage": { 66 | "color": "orange", 67 | "pseudonym": [ 68 | "SvelteKit" 69 | ], 70 | "file": "App.svelte/+layout.svelte", 71 | "import": "import { usal } from '@usal/svelte';", 72 | "start": "// USAL auto-initializes globally", 73 | "prop": "use:usal={'VALUES'}" 74 | } 75 | }, 76 | "vue": { 77 | "dependencies": { 78 | "vue": [ 79 | ">=3.0.0", 80 | false 81 | ], 82 | "@nuxt/kit": [ 83 | ">=3.0.0", 84 | true 85 | ] 86 | }, 87 | "plugins": [ 88 | "nuxt", 89 | "nuxt-plugin" 90 | ], 91 | "usage": { 92 | "color": "green", 93 | "pseudonym": [ 94 | "Nuxt" 95 | ], 96 | "file": "nuxt.config.ts", 97 | "import": "import { USALPlugin } from '@usal/vue';", 98 | "start": "createApp(App).use(USALPlugin).mount('#app');\n//for Nuxt\nexport default defineNuxtConfig({\nmodules: ['@usal/vue/nuxt']\n//...", 99 | "prop": "v-usal=\"'VALUES'\"" 100 | } 101 | }, 102 | "lit": { 103 | "dependencies": { 104 | "lit": [ 105 | ">=2.0.0", 106 | false 107 | ] 108 | }, 109 | "usage": { 110 | "color": "cyan", 111 | "pseudonym": [], 112 | "file": "main.ts", 113 | "import": "import { usal } from '@usal/lit';", 114 | "start": "// USAL auto-initializes globally", 115 | "prop": "${usal('VALUES')}" 116 | } 117 | }, 118 | "angular": { 119 | "format": "angular", 120 | "dependencies": { 121 | "@angular/core": [ 122 | ">=12.0.0", 123 | false 124 | ], 125 | "@angular/common": [ 126 | ">=12.0.0", 127 | false 128 | ] 129 | }, 130 | "usage": { 131 | "color": "red", 132 | "pseudonym": [], 133 | "file": "app.component.ts", 134 | "import": "import { USALModule } from '@usal/angular';", 135 | "start": "@Component({imports: [USALModule]})\nexport class AppComponent", 136 | "prop": "usal=\"VALUES\"" 137 | } 138 | } 139 | }, 140 | "scripts": { 141 | "dev": "node scripts/build.js --watch --skip-angular", 142 | "dev:with-angular": "node scripts/build.js --watch", 143 | "build": "npm run lint:fix && node scripts/build.js", 144 | "release:canary": "node scripts/publish.js --canary", 145 | "release:canary:dry": "node scripts/publish.js --canary --dry-run", 146 | "release:beta": "node scripts/publish.js --beta", 147 | "release:beta:dry": "node scripts/publish.js --beta --dry-run", 148 | "release:latest": "node scripts/publish.js --latest", 149 | "release:latest:dry": "node scripts/publish.js --latest --dry-run", 150 | "release:github": "node scripts/release-gh.js", 151 | "release:github:dry": "node scripts/release-gh.js --dry-run", 152 | "release:complete": "npm run release:latest && npm run release:github", 153 | "release:complete:dry": "npm run release:latest:dry && npm run release:github:dry", 154 | "format": "prettier --write --ignore-path .gitignore **/*.{js,ts,jsx,tsx,html,css} '!test/**' && npm run format:doc", 155 | "format:doc": "prettier --write --ignore-path .prettierignore *.{json,md} packages/**/*.{json,md}", 156 | "lint": "eslint src/**/*.{js,ts,jsx,tsx}", 157 | "lint:fix": "eslint src/**/*.{js,ts,jsx,tsx} --fix", 158 | "lint:pkgs": "npx eslint packages/** --no-ignore --rule {no-var:off,no-redeclare:off,prefer-const:off,eqeqeq:off,import/order:off,no-setter-return:off,no-sparse-arrays:off,no-cond-assign:off}", 159 | "lint:texts": "node scripts/lint-texts.js" 160 | }, 161 | "devDependencies": { 162 | "@angular/common": ">=12.0.0", 163 | "@angular/compiler": "^20.2.3", 164 | "@angular/compiler-cli": "^20.2.3", 165 | "@angular/core": ">=12.0.0", 166 | "@nuxt/kit": ">=3.0.0", 167 | "@typescript-eslint/eslint-plugin": "^8.42.0", 168 | "@typescript-eslint/parser": "^8.42.0", 169 | "browserslist-config-baseline": "^0.5.0", 170 | "chokidar": "^4.0.3", 171 | "dotenv-cli": "^10.0.0", 172 | "esbuild": "^0.25.9", 173 | "eslint": "^9.34.0", 174 | "eslint-config-prettier": "^10.1.8", 175 | "eslint-plugin-compat": "^6.0.2", 176 | "eslint-plugin-import": "^2.32.0", 177 | "eslint-plugin-sonarjs": "^3.0.5", 178 | "globals": "^16.3.0", 179 | "langdetect": "^0.2.1", 180 | "lit": ">=2.0.0", 181 | "ng-packagr": "^20.2.0", 182 | "nuxt": ">=3.0.0", 183 | "prettier": "^3.6.2", 184 | "react": ">=16.8.0", 185 | "solid-js": ">=1.0.0", 186 | "svelte": ">=3.0.0", 187 | "typescript": "^5.0.0", 188 | "vue": ">=3.0.0" 189 | }, 190 | "keywords": [ 191 | "javascript", 192 | "animation", 193 | "css-animations", 194 | "web-animation", 195 | "scroll-animations", 196 | "intersection-observer", 197 | "scroll-lib" 198 | ], 199 | "browserslist": "extends browserslist-config-baseline" 200 | } 201 | -------------------------------------------------------------------------------- /scripts/colorize.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/cognitive-complexity */ 2 | const colors = { 3 | reset: '\x1b[0m', 4 | bright: '\x1b[1m', 5 | dim: '\x1b[2m', 6 | underscore: '\x1b[4m', 7 | blink: '\x1b[5m', 8 | reverse: '\x1b[7m', 9 | hidden: '\x1b[8m', 10 | 11 | black: '\x1b[30m', 12 | red: '\x1b[31m', 13 | green: '\x1b[32m', 14 | yellow: '\x1b[33m', 15 | blue: '\x1b[34m', 16 | magenta: '\x1b[35m', 17 | cyan: '\x1b[36m', 18 | white: '\x1b[37m', 19 | 20 | brightBlack: '\x1b[90m', 21 | brightRed: '\x1b[91m', 22 | brightGreen: '\x1b[92m', 23 | brightYellow: '\x1b[93m', 24 | brightBlue: '\x1b[94m', 25 | brightMagenta: '\x1b[95m', 26 | brightCyan: '\x1b[96m', 27 | brightWhite: '\x1b[97m', 28 | }; 29 | 30 | const calculateBgColor = (colorCode) => { 31 | const match = colorCode.match(/(\d\d)m$/); 32 | const colorNum = parseInt(match[1]); 33 | if ((colorNum >= 30 && colorNum <= 37) || (colorNum >= 90 && colorNum <= 97)) 34 | return `\x1b[${colorNum + 10}m`; 35 | return '\x1b[40m'; 36 | }; 37 | 38 | const colorMap = { 39 | header: colors.cyan, 40 | success: colors.green, 41 | warning: colors.yellow, 42 | error: colors.red, 43 | info: colors.blue, 44 | highlight: colors.magenta, 45 | tag: colors.blue, 46 | package: colors.brightMagenta, 47 | version: colors.brightCyan, 48 | file: colors.cyan, 49 | command: colors.yellow, 50 | dim: colors.dim, 51 | accent: colors.brightMagenta, 52 | divider: colors.dim, 53 | update: colors.yellow, 54 | 55 | red: colors.red, 56 | green: colors.green, 57 | yellow: colors.yellow, 58 | blue: colors.blue, 59 | magenta: colors.magenta, 60 | cyan: colors.cyan, 61 | white: colors.white, 62 | black: colors.black, 63 | brightRed: colors.brightRed, 64 | brightGreen: colors.brightGreen, 65 | brightYellow: colors.brightYellow, 66 | brightBlue: colors.brightBlue, 67 | brightMagenta: colors.brightMagenta, 68 | brightCyan: colors.brightCyan, 69 | brightWhite: colors.brightWhite, 70 | brightBlack: colors.brightBlack, 71 | }; 72 | 73 | export const colorize = (text, parentColor = '') => { 74 | let result = ''; 75 | let i = 0; 76 | 77 | while (i < text.length) { 78 | if (text.slice(i, i + 2) === '/#') { 79 | const isTag = text[i + 2] === '['; 80 | const colorStart = i + (isTag ? 3 : 2); 81 | const colorEnd = text.indexOf(' ', colorStart); 82 | 83 | if (colorEnd === -1) { 84 | result += text[i]; 85 | i++; 86 | continue; 87 | } 88 | 89 | const colorName = text.slice(colorStart, colorEnd); 90 | const contentStart = colorEnd + 1; 91 | let depth = 1; 92 | let pos = contentStart; 93 | 94 | while (pos < text.length && depth > 0) { 95 | if (text.slice(pos, pos + 2) === '/#') { 96 | depth++; 97 | pos += 2; 98 | } else if (text.slice(pos, pos + 3) === ' #/') { 99 | depth--; 100 | if (depth === 0) break; 101 | pos += 3; 102 | } else { 103 | pos++; 104 | } 105 | } 106 | 107 | if (depth === 0) { 108 | const content = text.slice(contentStart, pos); 109 | 110 | const currentColor = 111 | isTag || parentColor.split('m').length === 3 112 | ? calculateBgColor(colorMap[colorName]) + colors.black 113 | : colorMap[colorName]; 114 | const processedContent = colorize(content, currentColor); 115 | 116 | if (currentColor) { 117 | let resetCode = colors.reset; 118 | resetCode += parentColor; 119 | result += `${currentColor}${processedContent}${resetCode}`; 120 | } else { 121 | result += processedContent; 122 | } 123 | 124 | i = pos + 3; 125 | } else { 126 | result += text[i]; 127 | i++; 128 | } 129 | } else { 130 | result += text[i]; 131 | i++; 132 | } 133 | } 134 | 135 | return result; 136 | }; 137 | 138 | export const colorLog = (text) => console.log(colorize(text)); 139 | export const hLog = (indentation, isTag, color, header = '', text = '', prefix = '') => { 140 | let logMethod; 141 | switch (color) { 142 | case 'error': 143 | logMethod = console.error; 144 | break; 145 | case 'warning': 146 | logMethod = console.warn; 147 | break; 148 | default: 149 | logMethod = console.log; 150 | } 151 | 152 | if (isTag) header = ' ' + header.toUpperCase() + ' '; 153 | 154 | logMethod( 155 | ' '.repeat(indentation) + 156 | colorize(`${prefix}/#${isTag ? '[' : ''}${color} ${header} #/ ${text}`) 157 | ); 158 | }; 159 | -------------------------------------------------------------------------------- /scripts/postbuild.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | import { hLog } from './colorize.js'; 7 | 8 | // Parse arguments 9 | const { projectName, packages, version } = JSON.parse( 10 | Buffer.from(process.argv[2], 'hex').toString('utf8') 11 | ); 12 | const isSilent = process.argv.includes('--silent') || process.argv.includes('-s'); 13 | if (!isSilent) { 14 | hLog(0, true, 'header', 'postbuild', 'Starting documentation update process...'); 15 | hLog(2, false, 'info', 'Project:', `/#accent ${projectName} #/`); 16 | hLog(2, false, 'info', 'Packages:', packages.map((p) => `/#package ${p} #/`).join(', ')); 17 | hLog(2, false, 'info', 'Version:', version); 18 | } 19 | 20 | // Track changes 21 | let updateCount = 0; 22 | let errorCount = 0; 23 | 24 | // File paths 25 | const files = { 26 | readme: 'README.md', 27 | index: 'docs/index.html', 28 | api: 'docs/API.md', 29 | vanilla: path.join('packages', 'vanilla', 'README.md'), 30 | }; 31 | 32 | // Check README exists 33 | if (!fs.existsSync(files.readme)) { 34 | hLog(2, true, 'error', 'error', 'README.md not found'); 35 | process.exit(1); 36 | } 37 | 38 | // Store original states 39 | const originalStates = {}; 40 | Object.entries(files).forEach(([key, path]) => { 41 | if (fs.existsSync(path) && key !== 'vanilla') { 42 | originalStates[key] = fs.readFileSync(path, 'utf-8'); 43 | } 44 | }); 45 | 46 | // Get package config 47 | const { packages: packagesConfig = {} } = JSON.parse(fs.readFileSync('package.json', 'utf-8')); 48 | 49 | // Generic update function 50 | function updateContent(content, updates, fileName) { 51 | if (!isSilent) 52 | hLog(0, true, 'header', `${fileName.toUpperCase()} UPDATE`, `Processing ${fileName}...`, '\n'); 53 | 54 | let result = content; 55 | updates.forEach(({ regex, replacement, name }) => { 56 | if (regex.test(result)) { 57 | result = result.replace(regex, replacement); 58 | if (!isSilent) hLog(2, false, 'success', '✓', name); 59 | updateCount++; 60 | } else { 61 | hLog(2, false, 'error', '✗', `${name} not found`); 62 | errorCount++; 63 | } 64 | }); 65 | 66 | return result; 67 | } 68 | 69 | // Generate badge URL 70 | function getBadgeUrl(packages = [], useNN = false, singlePackage = null, color = 'black') { 71 | const base = 'https://badge.usal.dev/'; 72 | const prefix = useNN ? '?nn&' : '?'; 73 | 74 | if (singlePackage) { 75 | return `${base}${prefix}p=${encodeURIComponent(singlePackage)}&color=${color}`; 76 | } 77 | if (packages.length > 0) { 78 | return `${base}${prefix}ps=${encodeURIComponent(packages.join(','))}`; 79 | } 80 | return base; 81 | } 82 | 83 | // Generate framework info 84 | function getFrameworkInfo() { 85 | const frameworkPackages = packages 86 | .filter((pkg) => pkg !== 'vanilla') 87 | .map((pkg) => `@${projectName}/${pkg}`); 88 | 89 | const examples = [ 90 | { 91 | framework: 'Vanilla JS', 92 | setup: '', 93 | example: '
Content
', 94 | pseudonyms: [], 95 | }, 96 | ]; 97 | 98 | Object.entries(packagesConfig).forEach(([framework, config]) => { 99 | if (config.usage && framework !== 'vanilla') { 100 | const name = framework.charAt(0).toUpperCase() + framework.slice(1); 101 | const { import: imp, start, prop, pseudonym = [] } = config.usage; 102 | 103 | examples.push({ 104 | framework: name, 105 | setup: [imp, start].filter(Boolean).join('\n').trim(), 106 | example: prop ? `
Content
` : '', 107 | pseudonyms: pseudonym, 108 | }); 109 | } 110 | }); 111 | 112 | return { frameworkPackages, examples }; 113 | } 114 | 115 | // Get all updates for a file 116 | function getUpdates(fileType, info) { 117 | const { frameworkPackages, examples } = info; 118 | const updates = []; 119 | 120 | // Common updates 121 | const frameworks = packages.map((p) => 122 | p === 'vanilla' ? 'Vanilla JS' : p.charAt(0).toUpperCase() + p.slice(1) 123 | ); 124 | const sorted = [ 125 | ...frameworks.filter((f) => f !== 'Vanilla JS'), 126 | ...(frameworks.includes('Vanilla JS') ? ['Vanilla JS'] : []), 127 | ]; 128 | const worksWithText = `Works with ${sorted.join(', ')} and more`; 129 | 130 | if (fileType === 'readme') { 131 | // Update "Works with" section 132 | updates.push({ 133 | regex: /\*\*Works with[^*]+\*\*/g, 134 | replacement: `**${worksWithText}**`, 135 | name: 'Works with section', 136 | }); 137 | 138 | // NPM installation section 139 | const installCmds = [`npm install ${projectName}`]; 140 | const frameworkInstalls = frameworkPackages.map((p) => { 141 | const framework = p.split('/')[1]; 142 | const displayName = framework.charAt(0).toUpperCase() + framework.slice(1); 143 | const pseudonyms = packagesConfig[framework]?.usage?.pseudonym || []; 144 | const label = pseudonyms.length > 0 ? `${displayName}/${pseudonyms.join('/')}` : displayName; 145 | return `npm install ${p} # For ${label}`; 146 | }); 147 | 148 | if (frameworkInstalls.length > 0) { 149 | installCmds.push('', '# Framework-specific packages', ...frameworkInstalls); 150 | } 151 | 152 | updates.push({ 153 | regex: /(### NPM\n\n```bash\n)([\s\S]*?)(```)/, 154 | replacement: `$1${installCmds.join('\n')}\n$3`, 155 | name: 'NPM installation', 156 | }); 157 | 158 | // Framework Setup section - Update individual framework sections 159 | examples 160 | .filter((e) => e.setup && e.framework !== 'Vanilla JS') 161 | .forEach(({ framework, setup, pseudonyms }) => { 162 | const frameworkName = framework; 163 | const pseudonymText = pseudonyms.length > 0 ? ` \\(${pseudonyms.join('/')}\\)` : ''; 164 | 165 | updates.push({ 166 | regex: new RegExp( 167 | `(### [\\u{1F7E0}-\\u{1F7EB}\\u{2B1B}\\u{2B1C}] ${frameworkName}${pseudonymText}\n\n\`\`\`jsx?\n)([\\s\\S]*?)(\`\`\`)`, 168 | 'iu' 169 | ), 170 | replacement: `$1${setup}\n$3`, 171 | name: `${frameworkName} setup`, 172 | }); 173 | }); 174 | 175 | // Package table in Packages Overview section 176 | let pkgTable = '| Package | Version |\n|---------|---------|\n'; 177 | pkgTable += `| \`${projectName}\` | ![npm](${getBadgeUrl(frameworkPackages, true)}) |\n`; 178 | 179 | packages 180 | .filter((p) => p !== 'vanilla') 181 | .forEach((pkg) => { 182 | const name = `@${projectName}/${pkg}`; 183 | const color = packagesConfig[pkg]?.usage?.color || 'grey'; 184 | pkgTable += `| \`${name}\` | ![npm](${getBadgeUrl([], true, name, color)}) |\n`; 185 | }); 186 | 187 | updates.push({ 188 | regex: /(## 📊 Packages Overview\n\n)([\s\S]*?)(\n\n## )/, 189 | replacement: `$1${pkgTable}$3`, 190 | name: 'Package overview table', 191 | }); 192 | } 193 | 194 | if (fileType === 'api' && fs.existsSync(files.api)) { 195 | // Framework Usage section agora em API.md 196 | const usageExamples = examples 197 | .map(({ framework, example, pseudonyms }) => { 198 | if (!example) return ''; 199 | const label = pseudonyms.length > 0 ? `${framework}/${pseudonyms.join('/')}` : framework; 200 | return `\n${example}`; 201 | }) 202 | .filter(Boolean) 203 | .join('\n\n'); 204 | 205 | updates.push({ 206 | regex: /(### Framework Usage\n\n```html\n)([\s\S]*?)(```)/, 207 | replacement: `$1${usageExamples}\n$3`, 208 | name: 'Framework usage examples', 209 | }); 210 | } 211 | 212 | if (fileType === 'index' && fs.existsSync(files.index)) { 213 | // Installation commands 214 | const installCmds = [`# Install\nnpm install ${projectName}`]; 215 | if (frameworkPackages.length > 0) { 216 | installCmds.push( 217 | '\n# Framework-specific packages:', 218 | ...frameworkPackages.map((p) => `npm install ${p}`) 219 | ); 220 | } 221 | 222 | updates.push({ 223 | regex: /(# Install[\s\S]*?)<\/code><\/pre>/, 224 | replacement: `${installCmds.join('\n')}`, 225 | name: 'Installation section', 226 | }); 227 | 228 | // Usage examples HTML 229 | const htmlExamples = examples 230 | .map(({ framework, example, pseudonyms }) => { 231 | if (!example) return ''; 232 | const label = pseudonyms.length > 0 ? `${framework}/${pseudonyms.join('/')}` : framework; 233 | const escapedExample = example.replace( 234 | /[<>"'&]/g, 235 | (m) => ({ '<': '<', '>': '>', '"': '"', "'": ''', '&': '&' })[m] 236 | ); 237 | return `<!-- ${label} -->\n${escapedExample}`; 238 | }) 239 | .filter(Boolean) 240 | .join('\n\n'); 241 | 242 | updates.push({ 243 | regex: 244 | /(
Main animation attribute<\/div>\s*
)([\s\S]*?)(<\/code><\/pre>)/,
245 |       replacement: `$1${htmlExamples}\n$3`,
246 |       name: 'Usage examples',
247 |     });
248 | 
249 |     // JavaScript API
250 |     const setupExamples = examples
251 |       .filter((e) => e.setup && e.framework !== 'Vanilla JS')
252 |       .map(({ framework, setup, pseudonyms }) => {
253 |         const label = framework + (pseudonyms.length ? ` (${pseudonyms.join(', ')})` : '');
254 |         return `// ${label}\n${setup}`;
255 |       })
256 |       .join('\n\n');
257 | 
258 |     updates.push({
259 |       regex:
260 |         /(Installed via CDN, initializes automatically, instance in window\.USAL\n)[\s\S]*?(<\/code><\/pre>)/,
261 |       replacement: `$1\n${setupExamples.replace(
262 |         /[<>"'&]/g,
263 |         (m) => ({ '<': '<', '>': '>', '"': '"', "'": ''', '&': '&' })[m]
264 |       )}$2`,
265 |       name: 'JavaScript API',
266 |     });
267 | 
268 |     // Badges
269 |     updates.push({
270 |       regex: /(