rune-scroller 2.1.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,4 @@
1
- # ⚡ Rune Scroller - Full Reference
2
-
3
- **📚 Complete API Reference** — Detailed documentation for all features and options.
4
-
5
- ---
1
+ # ⚡ Rune Scroller
6
2
 
7
3
  <div align="center">
8
4
  <img src="./logo.png" alt="Rune Scroller Logo" width="200" />
@@ -26,16 +22,13 @@
26
22
 
27
23
  ## ✨ Features
28
24
 
29
- - **12.7KB gzipped** (40.3KB uncompressed) - Minimal overhead, optimized for production
25
+ - **5.7KB gzipped** (JS+CSS) - Minimal overhead
30
26
  - **Zero dependencies** - Pure Svelte 5 + IntersectionObserver
31
- - **14 animations** - Fade, Zoom, Flip, Slide, Bounce variants
32
- - **Full TypeScript support** - Type definitions generated from JSDoc
27
+ - **14 animations** - Fade, Zoom, Flip, Slide, Bounce
28
+ - **TypeScript support** - Full type definitions
33
29
  - **SSR-ready** - SvelteKit compatible
34
30
  - **GPU-accelerated** - Pure CSS transforms
35
31
  - **Accessible** - Respects `prefers-reduced-motion`
36
- - **v2.0.0 New** - `onVisible` callback, ResizeObserver support, animation validation, sentinel customization
37
- - **✨ Latest** - `useIntersection` migrated to Svelte 5 `$effect` rune for better lifecycle management
38
- - **🚀 Bundle optimized** - CSS with custom properties, production build minification
39
32
 
40
33
  ---
41
34
 
@@ -43,47 +36,15 @@
43
36
 
44
37
  ```bash
45
38
  npm install rune-scroller
46
- # or
47
- pnpm add rune-scroller
48
- # or
49
- yarn add rune-scroller
50
39
  ```
51
40
 
52
41
  ---
53
42
 
54
43
  ## 🚀 Quick Start
55
44
 
56
- ### Step 1: Import CSS (required)
57
-
58
- **⚠️ Important:** You must import the CSS file once in your app.
59
-
60
- **Option A - In your root layout (recommended for SvelteKit):**
61
-
62
- ```svelte
63
- <!-- src/routes/+layout.svelte -->
64
- <script>
65
- import 'rune-scroller/animations.css';
66
- let { children } = $props();
67
- </script>
68
-
69
- {@render children()}
70
- ```
71
-
72
- **Option B - In each component that uses animations:**
73
-
74
- ```svelte
75
- <script>
76
- import runeScroller from 'rune-scroller';
77
- import 'rune-scroller/animations.css';
78
- </script>
79
- ```
80
-
81
- ### Step 2: Use the animations
82
-
83
45
  ```svelte
84
46
  <script>
85
47
  import runeScroller from 'rune-scroller';
86
- // CSS already imported in layout or above
87
48
  </script>
88
49
 
89
50
  <!-- Simple animation -->
@@ -107,6 +68,7 @@ yarn add rune-scroller
107
68
  ## 🎨 Available Animations
108
69
 
109
70
  ### Fade (5)
71
+
110
72
  - `fade-in` - Simple opacity fade
111
73
  - `fade-in-up` - Fade + move up 300px
112
74
  - `fade-in-down` - Fade + move down 300px
@@ -114,6 +76,7 @@ yarn add rune-scroller
114
76
  - `fade-in-right` - Fade + move from left 300px
115
77
 
116
78
  ### Zoom (5)
79
+
117
80
  - `zoom-in` - Scale from 0.3 to 1
118
81
  - `zoom-out` - Scale from 2 to 1
119
82
  - `zoom-in-up` - Zoom (0.5→1) + move up 300px
@@ -121,6 +84,7 @@ yarn add rune-scroller
121
84
  - `zoom-in-right` - Zoom (0.5→1) + move from left 300px
122
85
 
123
86
  ### Others (4)
87
+
124
88
  - `flip` - 3D flip on Y-axis
125
89
  - `flip-x` - 3D flip on X-axis
126
90
  - `slide-rotate` - Slide + rotate 10°
@@ -132,114 +96,45 @@ yarn add rune-scroller
132
96
 
133
97
  ```typescript
134
98
  interface RuneScrollerOptions {
135
- animation?: AnimationType; // Animation name (default: 'fade-in')
136
- duration?: number; // Duration in ms (default: 800)
137
- repeat?: boolean; // Repeat on scroll (default: false)
138
- debug?: boolean; // Show sentinel as visible line (default: false)
139
- offset?: number; // Sentinel offset in px (default: 0, negative = above)
140
- onVisible?: (element: HTMLElement) => void; // Callback when animation triggers (v2.0.0+)
141
- sentinelColor?: string; // Sentinel debug color, e.g. '#ff6b6b' (v2.0.0+)
142
- sentinelId?: string; // Custom ID for sentinel identification (v2.0.0+)
99
+ animation?: AnimationType // Animation name (default: 'fade-in')
100
+ duration?: number // Duration in ms (default: 800)
101
+ repeat?: boolean // Repeat on scroll (default: false)
102
+ debug?: boolean // Show sentinel as visible line (default: false)
103
+ offset?: number // Sentinel offset in px (default: 0, negative = earlier)
104
+ onVisible?: (element: HTMLElement) => void // Callback when visible
105
+ sentinelColor?: string // Debug sentinel color (e.g. '#ff6b6b')
106
+ sentinelId?: string // Custom sentinel ID
143
107
  }
144
108
  ```
145
109
 
146
- ### Option Details
147
-
148
- - **`animation`** - Type of animation to play. Choose from 14 built-in animations listed above. Invalid animations automatically fallback to 'fade-in' with a console warning.
149
- - **`duration`** - How long the animation lasts in milliseconds (default: 800ms).
150
- - **`repeat`** - If `true`, animation plays every time sentinel enters viewport. If `false`, plays only once.
151
- - **`debug`** - If `true`, displays the sentinel element as a visible line below your element. Useful for seeing exactly when animations trigger. Default color is cyan (#00e0ff), customize with `sentinelColor`.
152
- - **`offset`** - Offset of the sentinel in pixels. Positive values move sentinel down (delays animation), negative values move it up (triggers earlier). Useful for large elements where you want animation to trigger before the entire element is visible.
153
- - **`onVisible`** *(v2.0.0+)* - Callback function triggered when the animation becomes visible. Receives the animated element as parameter. Useful for analytics, lazy loading, or triggering custom effects.
154
- - **`sentinelColor`** *(v2.0.0+)* - Customize the debug sentinel color (e.g., '#ff6b6b' for red). Only visible when `debug: true`. Useful for distinguishing multiple sentinels on the same page.
155
- - **`sentinelId`** *(v2.0.0+)* - Set a custom ID for the sentinel element. If not provided, an auto-ID is generated (`sentinel-1`, `sentinel-2`, etc.). Useful for identifying sentinels in DevTools and tracking which element owns which sentinel.
156
-
157
110
  ### Examples
158
111
 
159
112
  ```svelte
160
113
  <!-- Basic -->
161
- <div use:runeScroller={{ animation: 'zoom-in' }}>
162
- Content
163
- </div>
114
+ <div use:runeScroller={{ animation: 'zoom-in' }}>Content</div>
164
115
 
165
116
  <!-- Custom duration -->
166
- <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
167
- Fast animation
168
- </div>
117
+ <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>Fast</div>
169
118
 
170
119
  <!-- Repeat mode -->
171
- <div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
172
- Repeats every time you scroll
173
- </div>
174
-
175
- <!-- Debug mode - shows cyan line marking sentinel position -->
176
- <div use:runeScroller={{ animation: 'fade-in', debug: true }}>
177
- The cyan line below this shows when animation will trigger
178
- </div>
179
-
180
- <!-- Multiple options -->
181
- <div use:runeScroller={{
182
- animation: 'fade-in-up',
183
- duration: 1200,
184
- repeat: true,
185
- debug: true
186
- }}>
187
- Full featured example
188
- </div>
120
+ <div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>Repeats</div>
189
121
 
190
- <!-- Large element - trigger animation earlier with negative offset -->
191
- <div use:runeScroller={{
192
- animation: 'fade-in-up',
193
- offset: -200 // Trigger 200px before element bottom
194
- }}>
195
- Large content that needs early triggering
196
- </div>
122
+ <!-- Debug mode -->
123
+ <div use:runeScroller={{ animation: 'fade-in', debug: true }}>Debug</div>
197
124
 
198
- <!-- Delay animation by moving sentinel down -->
199
- <div use:runeScroller={{
200
- animation: 'zoom-in',
201
- offset: 300 // Trigger 300px after element bottom
202
- }}>
203
- Content with delayed animation
125
+ <!-- Trigger earlier with negative offset -->
126
+ <div use:runeScroller={{ animation: 'fade-in-up', offset: -200 }}>
127
+ Triggers 200px before element bottom
204
128
  </div>
205
129
 
206
- <!-- v2.0.0: onVisible callback for analytics tracking -->
130
+ <!-- onVisible callback for analytics -->
207
131
  <div use:runeScroller={{
208
132
  animation: 'fade-in-up',
209
133
  onVisible: (el) => {
210
- console.log('Animation visible!', el);
211
- // Track analytics, load images, trigger API calls, etc.
212
- window.gtag?.('event', 'animation_visible', { element: el.id });
134
+ window.gtag?.('event', 'section_viewed', { id: el.id });
213
135
  }
214
136
  }}>
215
- Tracked animation
216
- </div>
217
-
218
- <!-- v2.0.0: Custom sentinel color for debugging -->
219
- <div use:runeScroller={{
220
- animation: 'fade-in',
221
- debug: true,
222
- sentinelColor: '#ff6b6b' // Red instead of default cyan
223
- }}>
224
- Red debug sentinel
225
- </div>
226
-
227
- <!-- v2.0.0: Custom sentinel ID for identification -->
228
- <div use:runeScroller={{
229
- animation: 'zoom-in',
230
- sentinelId: 'hero-zoom',
231
- debug: true
232
- }}>
233
- Identified sentinel (shows "hero-zoom" in debug mode)
234
- </div>
235
-
236
- <!-- v2.0.0: Auto-ID (sentinel-1, sentinel-2, etc) -->
237
- <div use:runeScroller={{
238
- animation: 'fade-in-up',
239
- debug: true
240
- // sentinelId omitted → auto generates "sentinel-1", "sentinel-2", etc
241
- }}>
242
- Auto-identified sentinel
137
+ Tracked section
243
138
  </div>
244
139
  ```
245
140
 
@@ -247,58 +142,36 @@ interface RuneScrollerOptions {
247
142
 
248
143
  ## 🎯 How It Works
249
144
 
250
- Rune Scroller uses **sentinel-based triggering**:
145
+ **Sentinel-based triggering:**
251
146
 
252
- 1. An invisible 1px sentinel element is created below your element
253
- 2. When the sentinel enters the viewport, animation triggers
254
- 3. This ensures precise timing regardless of element size
255
- 4. Uses native IntersectionObserver for performance
256
- 5. Pure CSS animations (GPU-accelerated)
257
- 6. *(v2.0.0)* Sentinel automatically repositions on element resize via ResizeObserver
147
+ 1. Invisible 1px sentinel created below your element
148
+ 2. When sentinel enters viewport, animation triggers
149
+ 3. Uses native IntersectionObserver for performance
150
+ 4. Pure CSS animations (GPU-accelerated)
151
+ 5. ResizeObserver auto-repositions sentinel
258
152
 
259
153
  **Why sentinels?**
154
+
260
155
  - Accurate timing across all screen sizes
261
156
  - No complex offset calculations
262
- - Handles staggered animations naturally
263
- - Sentinel stays fixed while element animates (no observer confusion with transforms)
264
-
265
- **Automatic ResizeObserver** *(v2.0.0+)*
266
- - Sentinel repositions automatically when element resizes
267
- - Works with responsive layouts and dynamic content
268
- - No configuration needed—it just works
157
+ - Works with animated elements (transforms don't affect observer)
269
158
 
270
159
  ---
271
160
 
272
161
  ## 🌐 SSR Compatibility
273
162
 
274
- Works seamlessly with SvelteKit. Import CSS in your root layout:
163
+ Works seamlessly with SvelteKit:
275
164
 
276
165
  ```svelte
277
166
  <!-- src/routes/+layout.svelte -->
278
167
  <script>
279
- import 'rune-scroller/animations.css';
168
+ import runeScroller from 'rune-scroller';
280
169
  let { children } = $props();
281
170
  </script>
282
171
 
283
172
  {@render children()}
284
173
  ```
285
174
 
286
- Then use animations anywhere in your app:
287
-
288
- ```svelte
289
- <!-- src/routes/+page.svelte -->
290
- <script>
291
- import runeScroller from 'rune-scroller';
292
- </script>
293
-
294
- <!-- No special handling needed -->
295
- <div use:runeScroller={{ animation: 'fade-in-up' }}>
296
- Works in SvelteKit SSR!
297
- </div>
298
- ```
299
-
300
- The library checks for browser environment and gracefully handles server-side rendering.
301
-
302
175
  ---
303
176
 
304
177
  ## ♿ Accessibility
@@ -306,73 +179,32 @@ The library checks for browser environment and gracefully handles server-side re
306
179
  Respects `prefers-reduced-motion`:
307
180
 
308
181
  ```css
309
- /* In animations.css */
310
182
  @media (prefers-reduced-motion: reduce) {
311
- .scroll-animate {
312
- animation: none !important;
313
- opacity: 1 !important;
314
- transform: none !important;
315
- }
183
+ .scroll-animate {
184
+ animation: none !important;
185
+ opacity: 1 !important;
186
+ transform: none !important;
187
+ }
316
188
  }
317
189
  ```
318
190
 
319
- Users who prefer reduced motion will see content without animations.
320
-
321
191
  ---
322
192
 
323
193
  ## 📚 API Reference
324
- ### Public API
325
-
326
- Rune Scroller exports a **single action-based API** (no components):
327
-
328
- 1. **`runeScroller`** (default) - Sentinel-based, simple, powerful
329
-
330
- **Why actions instead of components?**
331
- - Actions are lightweight directives
332
- - No DOM wrapper overhead
333
- - Better performance
334
- - More flexible
335
-
336
- ### Main Export
337
194
 
338
195
  ```typescript
339
196
  // Default export
340
- import runeScroller from 'rune-scroller';
197
+ import runeScroller from "rune-scroller"
341
198
 
342
199
  // Named exports
343
200
  import {
344
- useIntersection, // Composable
345
- useIntersectionOnce, // Composable
346
- calculateRootMargin // Utility
347
- } from 'rune-scroller';
201
+ useIntersection, // Composable
202
+ useIntersectionOnce, // Composable
203
+ calculateRootMargin, // Utility
204
+ } from "rune-scroller"
348
205
 
349
206
  // Types
350
- import type {
351
- AnimationType,
352
- RuneScrollerOptions,
353
- IntersectionOptions,
354
- UseIntersectionReturn
355
- } from 'rune-scroller';
356
- ```
357
-
358
- ### TypeScript Types
359
-
360
- ```typescript
361
- type AnimationType =
362
- | 'fade-in' | 'fade-in-up' | 'fade-in-down' | 'fade-in-left' | 'fade-in-right'
363
- | 'zoom-in' | 'zoom-out' | 'zoom-in-up' | 'zoom-in-left' | 'zoom-in-right'
364
- | 'flip' | 'flip-x' | 'slide-rotate' | 'bounce-in';
365
-
366
- interface RuneScrollerOptions {
367
- animation?: AnimationType;
368
- duration?: number;
369
- repeat?: boolean;
370
- debug?: boolean;
371
- offset?: number;
372
- onVisible?: (element: HTMLElement) => void; // v2.0.0+
373
- sentinelColor?: string; // v2.0.0+
374
- sentinelId?: string; // v2.0.0+
375
- }
207
+ import type { AnimationType, RuneScrollerOptions } from "rune-scroller"
376
208
  ```
377
209
 
378
210
  ---
@@ -384,46 +216,33 @@ interface RuneScrollerOptions {
384
216
  ```svelte
385
217
  <script>
386
218
  import runeScroller from 'rune-scroller';
387
- import 'rune-scroller/animations.css';
388
-
389
- const items = [
390
- { title: 'Feature 1', description: 'Description 1' },
391
- { title: 'Feature 2', description: 'Description 2' },
392
- { title: 'Feature 3', description: 'Description 3' }
393
- ];
219
+ const items = ['Item 1', 'Item 2', 'Item 3'];
394
220
  </script>
395
221
 
396
- <div class="grid">
397
- {#each items as item}
398
- <div use:runeScroller={{ animation: 'fade-in-up', duration: 800 }}>
399
- <h3>{item.title}</h3>
400
- <p>{item.description}</p>
401
- </div>
402
- {/each}
403
- </div>
222
+ {#each items as item, i}
223
+ <div use:runeScroller={{
224
+ animation: 'fade-in-up',
225
+ duration: 800,
226
+ style: `--delay: ${i * 100}ms`
227
+ }}>
228
+ {item}
229
+ </div>
230
+ {/each}
404
231
  ```
405
232
 
406
233
  ### Hero Section
407
234
 
408
235
  ```svelte
409
- <div use:runeScroller={{ animation: 'fade-in-down', duration: 1000 }}>
410
- <h1>Welcome</h1>
411
- </div>
412
-
413
- <div use:runeScroller={{ animation: 'fade-in-up', duration: 1200 }}>
414
- <p>Engaging content</p>
415
- </div>
416
-
417
- <div use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
418
- <button>Get Started</button>
419
- </div>
236
+ <h1 use:runeScroller={{ animation: 'fade-in-down', duration: 1000 }}>Welcome</h1>
237
+ <p use:runeScroller={{ animation: 'fade-in-up', duration: 1200 }}>Subtitle</p>
238
+ <button use:runeScroller={{ animation: 'zoom-in', duration: 800 }}>Get Started</button>
420
239
  ```
421
240
 
422
241
  ---
423
242
 
424
243
  ## 🔗 Links
425
244
 
426
- - **npm Package**: [rune-scroller](https://www.npmjs.com/package/rune-scroller)
245
+ - **npm**: [rune-scroller](https://www.npmjs.com/package/rune-scroller)
427
246
  - **GitHub**: [lelabdev/rune-scroller](https://github.com/lelabdev/rune-scroller)
428
247
  - **Changelog**: [CHANGELOG.md](https://github.com/lelabdev/rune-scroller/blob/main/lib/CHANGELOG.md)
429
248
 
@@ -435,18 +254,4 @@ MIT © [ludoloops](https://github.com/ludoloops)
435
254
 
436
255
  ---
437
256
 
438
- ## 🤝 Contributing
439
-
440
- Contributions welcome! Please open an issue or PR on GitHub.
441
-
442
- ```bash
443
- # Development
444
- bun install
445
- bun run dev
446
- bun test
447
- bun run build
448
- ```
449
-
450
- ---
451
-
452
257
  Made with ❤️ by [LeLab.dev](https://lelab.dev)
@@ -24,6 +24,7 @@ export function createSentinel(element: HTMLElement, debug?: boolean, offset?: n
24
24
  };
25
25
  /**
26
26
  * Check if CSS animations are loaded and warn if not (dev only)
27
+ * Uses cache to avoid expensive getComputedStyle() on every element creation
27
28
  * @returns {boolean} True if CSS appears to be loaded
28
29
  */
29
30
  export function checkAndWarnIfCSSNotLoaded(): boolean;
package/dist/dom-utils.js CHANGED
@@ -4,6 +4,13 @@
4
4
  */
5
5
  let sentinelCounter = 0;
6
6
 
7
+ /**
8
+ * Cache to check CSS only once per page load
9
+ * Avoids expensive getComputedStyle() calls
10
+ * @type {boolean | null}
11
+ */
12
+ let cssCheckResult = null;
13
+
7
14
  /**
8
15
  * @param {HTMLElement} element
9
16
  * @param {number} [duration]
@@ -38,7 +45,7 @@ export function createSentinel(element, debug = false, offset = 0, sentinelColor
38
45
  const sentinel = document.createElement('div');
39
46
  // Use offsetHeight instead of getBoundingClientRect for accurate dimensions
40
47
  // getBoundingClientRect returns transformed dimensions (affected by scale, etc)
41
- // offsetHeight returns the actual element height independent of CSS transforms
48
+ // offsetHeight returns actual element height independent of CSS transforms
42
49
  const elementHeight = element.offsetHeight;
43
50
  const sentinelTop = elementHeight + offset;
44
51
 
@@ -48,7 +55,7 @@ export function createSentinel(element, debug = false, offset = 0, sentinelColor
48
55
  sentinelId = `sentinel-${sentinelCounter}`;
49
56
  }
50
57
 
51
- // Always set the data-sentinel-id attribute
58
+ // Always set to data-sentinel-id attribute
52
59
  sentinel.setAttribute('data-sentinel-id', sentinelId);
53
60
 
54
61
  if (debug) {
@@ -71,11 +78,15 @@ export function createSentinel(element, debug = false, offset = 0, sentinelColor
71
78
 
72
79
  /**
73
80
  * Check if CSS animations are loaded and warn if not (dev only)
81
+ * Uses cache to avoid expensive getComputedStyle() on every element creation
74
82
  * @returns {boolean} True if CSS appears to be loaded
75
83
  */
76
84
  export function checkAndWarnIfCSSNotLoaded() {
77
- if (typeof document === 'undefined') return;
78
- if (process.env.NODE_ENV === 'production') return;
85
+ if (typeof document === 'undefined') return false;
86
+ if (process.env.NODE_ENV === 'production') return true;
87
+
88
+ // Return cached result if already checked (avoids expensive reflows)
89
+ if (cssCheckResult !== null) return cssCheckResult;
79
90
 
80
91
  // Try to detect if animations.css is loaded by checking for animation classes
81
92
  const test = document.createElement('div');
@@ -94,4 +105,8 @@ export function checkAndWarnIfCSSNotLoaded() {
94
105
  'Documentation: https://github.com/lelabdev/rune-scroller#installation'
95
106
  );
96
107
  }
108
+
109
+ // Cache the result for future calls
110
+ cssCheckResult = hasAnimation;
111
+ return hasAnimation;
97
112
  }
package/dist/index.js CHANGED
@@ -6,6 +6,9 @@
6
6
  * @module rune-scroller
7
7
  */
8
8
 
9
+ // Import CSS animations automatically
10
+ import './animations.css';
11
+
9
12
  // Main action (default export - recommended)
10
13
  import { runeScroller } from './runeScroller.js';
11
14
  export default runeScroller;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rune-scroller",
3
- "version": "2.1.1",
4
- "description": "Lightweight, high-performance scroll animations for Svelte 5. 12.7KB gzipped, zero dependencies.",
3
+ "version": "2.2.2",
4
+ "description": "Lightweight, high-performance scroll animations for Svelte 5. 14KB gzipped, zero dependencies.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "license": "MIT",