rune-scroller 0.1.1 → 0.1.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,5 +1,9 @@
1
1
  # ⚡ Rune Scroller
2
2
 
3
+ <div align="center">
4
+ <img src="./logo.png" alt="Rune Scroller Logo" width="200" />
5
+ </div>
6
+
3
7
  **Native Scroll Animations for Svelte 5** — Built with **Svelte 5 Runes** and **IntersectionObserver API**. No external dependencies, pure performance.
4
8
 
5
9
  > 🚀 **Open Source Project** by **[ludoloops](https://github.com/ludoloops)** at **[LeLab.dev](https://lelab.dev)**
@@ -57,9 +61,7 @@ For a typical SvelteKit app:
57
61
  ```
58
62
  rune-scroller-lib/
59
63
  ├── src/lib/
60
- │ ├── Rs.svelte # Main animation component (one-time or repeat)
61
- │ ├── BaseAnimated.svelte # Base animation implementation
62
- │ ├── runeScroller.svelte.ts # Sentinel-based animation action (recommended)
64
+ │ ├── runeScroller.svelte.ts # Main animation action (sentinel-based)
63
65
  │ ├── useIntersection.svelte.ts # IntersectionObserver composables
64
66
  │ ├── animate.svelte.ts # Animation action for direct DOM control
65
67
  │ ├── animations.ts # Animation configuration & validation
@@ -92,115 +94,54 @@ pnpm add rune-scroller
92
94
  yarn add rune-scroller
93
95
  ```
94
96
 
95
- ### Basic Usage with `Rs` Component
97
+ ### Basic Usage with `runeScroller` Action
96
98
 
97
- The `Rs` component is the main component for scroll animations. Use the `repeat` prop to control animation behavior:
99
+ Use the `runeScroller` action with the `use:` directive for sentinel-based animation triggering:
98
100
 
99
101
  ```svelte
100
102
  <script>
101
- import Rs from 'rune-scroller';
102
- import 'rune-scroller/animations.css';
103
+ import runeScroller from 'rune-scroller';
103
104
  </script>
104
105
 
105
- <!-- Play animation once when element enters viewport (default) -->
106
- <Rs animation="fade-in">
107
- <div class="card">
108
- <h2>Hello World</h2>
109
- <p>This element fades in once</p>
110
- </div>
111
- </Rs>
112
-
113
- <!-- Repeat animation every time element enters viewport -->
114
- <Rs animation="zoom-in" repeat>
115
- <div class="card">
116
- <h2>Repeating Animation</h2>
117
- <p>This triggers each time you scroll past it</p>
118
- </div>
119
- </Rs>
120
- ```
121
-
122
- ### In SvelteKit/Local Development
106
+ <!-- Simple fade in animation -->
107
+ <div use:runeScroller={{ animation: 'fade-in' }}>
108
+ <h2>Animated Heading</h2>
109
+ <p>Animates when scrolled into view</p>
110
+ </div>
123
111
 
124
- ```svelte
125
- <script>
126
- import Rs from '$lib/Rs.svelte';
127
- import '$lib/animations.css';
128
- </script>
112
+ <!-- With custom duration -->
113
+ <div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
114
+ <div class="card">Smooth fade and slide</div>
115
+ </div>
129
116
 
130
- <Rs animation="fade-in-up" duration={1000} delay={200}>
131
- <div class="card">
132
- <h2>Animated Content</h2>
133
- </div>
134
- </Rs>
117
+ <!-- Repeat animation on every scroll -->
118
+ <div use:runeScroller={{ animation: 'bounce-in', duration: 800, repeat: true }}>
119
+ <button>Bounces on every scroll</button>
120
+ </div>
135
121
  ```
136
122
 
137
- ### API Overview
123
+ ### How It Works
138
124
 
139
- The `Rs` component is the unified API for all scroll animations:
125
+ The `runeScroller` action uses an invisible **sentinel element** for precise animation triggering:
140
126
 
141
- ```svelte
142
- <!-- Animation plays once (default) -->
143
- <Rs animation="fade-in">Content</Rs>
144
-
145
- <!-- Animation repeats on every scroll -->
146
- <Rs animation="fade-in" repeat>Content</Rs>
147
-
148
- <!-- Customize timing -->
149
- <Rs animation="zoom-in" duration={1200} delay={300}>
150
- Content
151
- </Rs>
152
-
153
- <!-- Use with any HTML attribute -->
154
- <Rs animation="bounce-in" class="my-class" data-test="value">
155
- Content
156
- </Rs>
157
- ```
127
+ 1. **Sentinel Creation** - An invisible sentinel is created and positioned **absolutely** at your element's bottom
128
+ 2. **Parent Context** - Parent element automatically gets `position: relative` if needed (no visual impact)
129
+ 3. **Viewport Detection** - When the sentinel enters the viewport, it triggers the animation
130
+ 4. **Fixed Positioning** - The sentinel stays fixed while your element animates, ensuring accurate timing
131
+ 5. **Invisible by Default** - The sentinel is 1px tall and invisible (`visibility:hidden`)
132
+ 6. **Debug Mode** - Add `debug: true` to see the sentinel as a visible cyan line (useful for development)
158
133
 
159
134
  ---
160
135
 
161
- ## 🎯 Sentinel-Based Animation Triggering with `runeScroller`
162
-
163
- For more precise control over animation timing, use the `runeScroller` action. This approach uses an invisible **sentinel element** positioned below your content to trigger animations at exactly the right moment.
136
+ ## 🎯 Sentinel-Based Animation Triggering
164
137
 
165
138
  ### Why Sentinels?
166
139
 
167
- - **Accurate Timing** - Instead of triggering when the element enters, sentinel triggers slightly earlier
140
+ - **Accurate Timing** - Triggers at the perfect moment for smooth animations
168
141
  - **Consistent Behavior** - Same timing across all screen sizes and content heights
169
142
  - **Simple API** - No complex offset calculations needed
170
143
  - **Performance** - Minimal overhead, pure CSS animations
171
-
172
- ### How Sentinels Work
173
-
174
- 1. An invisible 20px sentinel element is automatically placed **below** your animated element
175
- 2. When the sentinel enters the viewport, it triggers the animation
176
- 3. This ensures content animates in perfectly as it becomes visible
177
-
178
- ```svelte
179
- <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
180
- <!-- Your content here -->
181
- <!-- Invisible sentinel is automatically placed below -->
182
- </div>
183
- ```
184
-
185
- ### Basic Usage
186
-
187
- ```svelte
188
- <script>
189
- import { runeScroller } from 'rune-scroller';
190
- import 'rune-scroller/animations.css';
191
- </script>
192
-
193
- <!-- Simple fade in with sentinel triggering -->
194
- <div use:runeScroller={{ animation: 'fade-in' }}>
195
- <h2>Animated Heading</h2>
196
- <p>Animates when sentinel enters viewport</p>
197
- </div>
198
-
199
- <!-- With duration control -->
200
- <div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
201
- <div class="card">Smooth animation</div>
202
- </div>
203
- ```
144
+ - **Reliable** - Automatic sentinel placement handles edge cases
204
145
 
205
146
  ### Sentinel-Based Examples
206
147
 
@@ -208,7 +149,7 @@ For more precise control over animation timing, use the `runeScroller` action. T
208
149
 
209
150
  ```svelte
210
151
  <script>
211
- import { runeScroller } from 'rune-scroller';
152
+ import runeScroller from 'rune-scroller';
212
153
  </script>
213
154
 
214
155
  <div class="grid">
@@ -247,95 +188,6 @@ interface RuneScrollerOptions {
247
188
  }
248
189
  ```
249
190
 
250
- ### Comparing: `Rs` Component vs `runeScroller` Action
251
-
252
- | Feature | `Rs` Component | `runeScroller` Action |
253
- |---------|---|---|
254
- | **Usage** | `<Rs>` wrapper | `use:` directive |
255
- | **Triggering** | IntersectionObserver on element | IntersectionObserver on sentinel |
256
- | **Timing Control** | offset, threshold props | Automatic sentinel placement |
257
- | **Repeat Support** | Yes (via `repeat` prop) | Yes (via `repeat` option) |
258
- | **Best For** | Complex layouts, component isolation | Direct DOM control, simple/mixed elements |
259
-
260
- ---
261
-
262
- ## ⚙️ Component Props
263
-
264
- ### Rs Component
265
-
266
- ```typescript
267
- interface RsProps {
268
- animation?: string; // Animation type (default: 'fade-in')
269
- threshold?: number; // Visibility threshold (default: 0.5)
270
- offset?: number; // Trigger offset 0-100% (optional, uses default if not set)
271
- rootMargin?: string; // Observer margin (overrides offset if set)
272
- duration?: number; // Duration in ms (default: 800)
273
- delay?: number; // Delay in ms (default: 0)
274
- repeat?: boolean; // Repeat animation on every scroll (default: false)
275
- children: Snippet; // Content to animate
276
- [key: string]: any; // Accepts any HTML attributes (e.g., data-testid, class, etc.)
277
- }
278
- ```
279
-
280
- #### `offset` Prop (Optional)
281
-
282
- Controls when the animation triggers as the element scrolls into view. If not specified, uses default behavior (`-10% 0px -10% 0px`).
283
-
284
- - `offset={0}` — Triggers when element touches bottom of screen
285
- - `offset={50}` — Triggers at middle of screen
286
- - `offset={100}` — Triggers when element reaches top of screen
287
- - **Not set** — Uses default behavior (triggers in middle ~80% band)
288
-
289
- **Examples:**
290
-
291
- ```svelte
292
- <!-- Early trigger (bottom of screen) -->
293
- <Rs animation="fade-in-up" offset={0}>
294
- <div class="card">Animates early</div>
295
- </Rs>
296
-
297
- <!-- Late trigger (top of screen) -->
298
- <Rs animation="fade-in-up" offset={100}>
299
- <div class="card">Animates late</div>
300
- </Rs>
301
-
302
- <!-- Custom timing -->
303
- <Rs animation="fade-in-up" offset={75}>
304
- <div class="card">Animates at 75%</div>
305
- </Rs>
306
-
307
- <!-- Repeat animation on every scroll -->
308
- <Rs animation="fade-in-up" offset={50} repeat>
309
- <div class="card">Animates every time</div>
310
- </Rs>
311
- ```
312
-
313
- **Full example with all props:**
314
-
315
- ```svelte
316
- <Rs
317
- animation="fade-in-up"
318
- duration={1200}
319
- delay={300}
320
- threshold={0.8}
321
- offset={25}
322
- repeat={false}
323
- data-testid="custom-animation"
324
- >
325
- <div class="card">
326
- <h2>Custom Timing</h2>
327
- <p>Duration: 1200ms, Delay: 300ms, Threshold: 80%, Offset: 25%</p>
328
- </div>
329
- </Rs>
330
- ```
331
-
332
- #### Repeat Behavior
333
-
334
- Use the `repeat` prop to control animation behavior:
335
-
336
- - `repeat={false}` (default) - Animation plays **once** when element enters viewport
337
- - `repeat={true}` - Animation **repeats** every time element enters viewport
338
-
339
191
  ---
340
192
 
341
193
  ## 🎨 All Animations with Examples
@@ -347,12 +199,14 @@ Use the `repeat` prop to control animation behavior:
347
199
  Simple opacity fade from transparent to visible.
348
200
 
349
201
  ```svelte
350
- <Rs animation="fade-in">
351
- <div class="card">
352
- <h2>Fade In</h2>
353
- <p>Simple fade entrance</p>
354
- </div>
355
- </Rs>
202
+ <script>
203
+ import runeScroller from 'rune-scroller';
204
+ </script>
205
+
206
+ <div use:runeScroller={{ animation: 'fade-in' }}>
207
+ <h2>Fade In</h2>
208
+ <p>Simple fade entrance</p>
209
+ </div>
356
210
  ```
357
211
 
358
212
  #### `fade-in-up`
@@ -360,12 +214,10 @@ Simple opacity fade from transparent to visible.
360
214
  Fades in while moving up 100px.
361
215
 
362
216
  ```svelte
363
- <Rs animation="fade-in-up">
364
- <div class="card">
365
- <h2>Fade In Up</h2>
366
- <p>Rises from below</p>
367
- </div>
368
- </Rs>
217
+ <div use:runeScroller={{ animation: 'fade-in-up' }}>
218
+ <h2>Fade In Up</h2>
219
+ <p>Rises from below</p>
220
+ </div>
369
221
  ```
370
222
 
371
223
  #### `fade-in-down`
@@ -373,12 +225,10 @@ Fades in while moving up 100px.
373
225
  Fades in while moving down 100px.
374
226
 
375
227
  ```svelte
376
- <Rs animation="fade-in-down">
377
- <div class="card">
378
- <h2>Fade In Down</h2>
379
- <p>Descends from above</p>
380
- </div>
381
- </Rs>
228
+ <div use:runeScroller={{ animation: 'fade-in-down' }}>
229
+ <h2>Fade In Down</h2>
230
+ <p>Descends from above</p>
231
+ </div>
382
232
  ```
383
233
 
384
234
  #### `fade-in-left`
@@ -386,12 +236,10 @@ Fades in while moving down 100px.
386
236
  Fades in while moving left 100px.
387
237
 
388
238
  ```svelte
389
- <Rs animation="fade-in-left">
390
- <div class="card">
391
- <h2>Fade In Left</h2>
392
- <p>Comes from the right</p>
393
- </div>
394
- </Rs>
239
+ <div use:runeScroller={{ animation: 'fade-in-left' }}>
240
+ <h2>Fade In Left</h2>
241
+ <p>Comes from the right</p>
242
+ </div>
395
243
  ```
396
244
 
397
245
  #### `fade-in-right`
@@ -399,12 +247,10 @@ Fades in while moving left 100px.
399
247
  Fades in while moving right 100px.
400
248
 
401
249
  ```svelte
402
- <Rs animation="fade-in-right">
403
- <div class="card">
404
- <h2>Fade In Right</h2>
405
- <p>Comes from the left</p>
406
- </div>
407
- </Rs>
250
+ <div use:runeScroller={{ animation: 'fade-in-right' }}>
251
+ <h2>Fade In Right</h2>
252
+ <p>Comes from the left</p>
253
+ </div>
408
254
  ```
409
255
 
410
256
  ---
@@ -416,12 +262,10 @@ Fades in while moving right 100px.
416
262
  Scales from 50% to 100% while fading in.
417
263
 
418
264
  ```svelte
419
- <Rs animation="zoom-in">
420
- <div class="card">
421
- <h2>Zoom In</h2>
422
- <p>Grows into view</p>
423
- </div>
424
- </Rs>
265
+ <div use:runeScroller={{ animation: 'zoom-in' }}>
266
+ <h2>Zoom In</h2>
267
+ <p>Grows into view</p>
268
+ </div>
425
269
  ```
426
270
 
427
271
  #### `zoom-out`
@@ -429,12 +273,10 @@ Scales from 50% to 100% while fading in.
429
273
  Scales from 150% to 100% while fading in.
430
274
 
431
275
  ```svelte
432
- <Rs animation="zoom-out">
433
- <div class="card">
434
- <h2>Zoom Out</h2>
435
- <p>Shrinks into view</p>
436
- </div>
437
- </Rs>
276
+ <div use:runeScroller={{ animation: 'zoom-out' }}>
277
+ <h2>Zoom Out</h2>
278
+ <p>Shrinks into view</p>
279
+ </div>
438
280
  ```
439
281
 
440
282
  #### `zoom-in-up`
@@ -442,12 +284,10 @@ Scales from 150% to 100% while fading in.
442
284
  Scales from 50% while translating up 50px.
443
285
 
444
286
  ```svelte
445
- <Rs animation="zoom-in-up">
446
- <div class="card">
447
- <h2>Zoom In Up</h2>
448
- <p>Grows while moving up</p>
449
- </div>
450
- </Rs>
287
+ <div use:runeScroller={{ animation: 'zoom-in-up' }}>
288
+ <h2>Zoom In Up</h2>
289
+ <p>Grows while moving up</p>
290
+ </div>
451
291
  ```
452
292
 
453
293
  #### `zoom-in-left`
@@ -455,12 +295,10 @@ Scales from 50% while translating up 50px.
455
295
  Scales from 50% while translating left 50px.
456
296
 
457
297
  ```svelte
458
- <Rs animation="zoom-in-left">
459
- <div class="card">
460
- <h2>Zoom In Left</h2>
461
- <p>Grows while moving left</p>
462
- </div>
463
- </Rs>
298
+ <div use:runeScroller={{ animation: 'zoom-in-left' }}>
299
+ <h2>Zoom In Left</h2>
300
+ <p>Grows while moving left</p>
301
+ </div>
464
302
  ```
465
303
 
466
304
  #### `zoom-in-right`
@@ -468,12 +306,10 @@ Scales from 50% while translating left 50px.
468
306
  Scales from 50% while translating right 50px.
469
307
 
470
308
  ```svelte
471
- <Rs animation="zoom-in-right">
472
- <div class="card">
473
- <h2>Zoom In Right</h2>
474
- <p>Grows while moving right</p>
475
- </div>
476
- </Rs>
309
+ <div use:runeScroller={{ animation: 'zoom-in-right' }}>
310
+ <h2>Zoom In Right</h2>
311
+ <p>Grows while moving right</p>
312
+ </div>
477
313
  ```
478
314
 
479
315
  ---
@@ -485,12 +321,10 @@ Scales from 50% while translating right 50px.
485
321
  3D rotation on Y axis (left to right).
486
322
 
487
323
  ```svelte
488
- <Rs animation="flip">
489
- <div class="card">
490
- <h2>Flip</h2>
491
- <p>Rotates on Y axis</p>
492
- </div>
493
- </Rs>
324
+ <div use:runeScroller={{ animation: 'flip' }}>
325
+ <h2>Flip</h2>
326
+ <p>Rotates on Y axis</p>
327
+ </div>
494
328
  ```
495
329
 
496
330
  #### `flip-x`
@@ -498,12 +332,10 @@ Scales from 50% while translating right 50px.
498
332
  3D rotation on X axis (top to bottom).
499
333
 
500
334
  ```svelte
501
- <Rs animation="flip-x">
502
- <div class="card">
503
- <h2>Flip X</h2>
504
- <p>Rotates on X axis</p>
505
- </div>
506
- </Rs>
335
+ <div use:runeScroller={{ animation: 'flip-x' }}>
336
+ <h2>Flip X</h2>
337
+ <p>Rotates on X axis</p>
338
+ </div>
507
339
  ```
508
340
 
509
341
  ---
@@ -515,12 +347,10 @@ Scales from 50% while translating right 50px.
515
347
  Slides from left while rotating 45 degrees.
516
348
 
517
349
  ```svelte
518
- <Rs animation="slide-rotate">
519
- <div class="card">
520
- <h2>Slide Rotate</h2>
521
- <p>Slides and spins</p>
522
- </div>
523
- </Rs>
350
+ <div use:runeScroller={{ animation: 'slide-rotate' }}>
351
+ <h2>Slide Rotate</h2>
352
+ <p>Slides and spins</p>
353
+ </div>
524
354
  ```
525
355
 
526
356
  ---
@@ -532,30 +362,28 @@ Slides from left while rotating 45 degrees.
532
362
  Bouncy entrance with scaling keyframe animation.
533
363
 
534
364
  ```svelte
535
- <Rs animation="bounce-in" duration={800}>
536
- <div class="card">
537
- <h2>Bounce In</h2>
538
- <p>Bounces into view</p>
539
- </div>
540
- </Rs>
365
+ <div use:runeScroller={{ animation: 'bounce-in', duration: 800 }}>
366
+ <h2>Bounce In</h2>
367
+ <p>Bounces into view</p>
368
+ </div>
541
369
  ```
542
370
 
543
371
  ---
544
372
 
545
- ### Compare: Once vs Repeat
373
+ ### One-Time vs Repeat Animations
546
374
 
547
- **Same animation, different behavior using the `repeat` prop:**
375
+ Control animation behavior with the `repeat` option:
548
376
 
549
377
  ```svelte
550
- <!-- Plays once on scroll down (default) -->
551
- <Rs animation="fade-in-up">
552
- <div class="card">Animates once</div>
553
- </Rs>
378
+ <!-- Plays once on scroll (default) -->
379
+ <div use:runeScroller={{ animation: 'fade-in-up' }}>
380
+ Animates once when scrolled into view
381
+ </div>
554
382
 
555
383
  <!-- Repeats each time you scroll by -->
556
- <Rs animation="fade-in-up" repeat>
557
- <div class="card">Animates on every scroll</div>
558
- </Rs>
384
+ <div use:runeScroller={{ animation: 'fade-in-up', repeat: true }}>
385
+ Animates every time you scroll past it
386
+ </div>
559
387
  ```
560
388
 
561
389
  ---
@@ -568,17 +396,17 @@ Animate cards with progressive delays:
568
396
 
569
397
  ```svelte
570
398
  <script>
571
- import Rs from '$lib/Rs.svelte';
399
+ import runeScroller from 'rune-scroller';
572
400
  </script>
573
401
 
574
402
  <div class="grid">
575
403
  {#each items as item, i}
576
- <Rs animation="fade-in-up" delay={i * 100}>
404
+ <div use:runeScroller={{ animation: 'fade-in-up', duration: 800 + i * 100 }}>
577
405
  <div class="card">
578
406
  <h3>{item.title}</h3>
579
407
  <p>{item.description}</p>
580
408
  </div>
581
- </Rs>
409
+ </div>
582
410
  {/each}
583
411
  </div>
584
412
  ```
@@ -586,40 +414,42 @@ Animate cards with progressive delays:
586
414
  ### Mixed Animations
587
415
 
588
416
  ```svelte
589
- <Rs animation="fade-in">
590
- <section>Content fades in</section>
591
- </Rs>
417
+ <script>
418
+ import runeScroller from 'rune-scroller';
419
+ </script>
592
420
 
593
- <Rs animation="slide-rotate">
594
- <section>Content slides and rotates</section>
595
- </Rs>
421
+ <section use:runeScroller={{ animation: 'fade-in' }}>
422
+ Content fades in
423
+ </section>
424
+
425
+ <section use:runeScroller={{ animation: 'slide-rotate' }}>
426
+ Content slides and rotates
427
+ </section>
596
428
 
597
- <Rs animation="zoom-in" repeat>
598
- <section>Content zooms in repeatedly</section>
599
- </Rs>
429
+ <section use:runeScroller={{ animation: 'zoom-in', repeat: true }}>
430
+ Content zooms in repeatedly
431
+ </section>
600
432
  ```
601
433
 
602
434
  ### Hero Section
603
435
 
604
436
  ```svelte
605
437
  <script>
606
- import Rs from '$lib/Rs.svelte';
438
+ import runeScroller from 'rune-scroller';
607
439
  </script>
608
440
 
609
441
  <section class="hero">
610
- <div class="hero-content">
611
- <Rs animation="fade-in" delay={0}>
612
- <h1>Welcome</h1>
613
- </Rs>
614
-
615
- <Rs animation="fade-in" delay={200}>
616
- <p>Scroll to reveal more</p>
617
- </Rs>
618
-
619
- <Rs animation="zoom-in" delay={400}>
620
- <button>Get Started</button>
621
- </Rs>
622
- </div>
442
+ <h1 use:runeScroller={{ animation: 'fade-in', duration: 1000 }}>
443
+ Welcome
444
+ </h1>
445
+
446
+ <p use:runeScroller={{ animation: 'fade-in', duration: 1200 }}>
447
+ Scroll to reveal more
448
+ </p>
449
+
450
+ <button use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
451
+ Get Started
452
+ </button>
623
453
  </section>
624
454
  ```
625
455
 
@@ -653,7 +483,7 @@ function runeScroller(
653
483
 
654
484
  ```svelte
655
485
  <script>
656
- import { runeScroller } from 'rune-scroller';
486
+ import runeScroller from 'rune-scroller';
657
487
  </script>
658
488
 
659
489
  <!-- Animation plays once when sentinel enters viewport -->
@@ -675,7 +505,7 @@ function runeScroller(
675
505
 
676
506
  ```svelte
677
507
  <script>
678
- import { runeScroller } from 'rune-scroller';
508
+ import runeScroller from 'rune-scroller';
679
509
  </script>
680
510
 
681
511
  <!-- Fade in once on scroll -->
@@ -713,7 +543,6 @@ function runeScroller(
713
543
  - ✅ Consistent timing across layouts
714
544
  - ✅ Minimal overhead applications
715
545
  - ✅ Both one-time and repeating animations
716
- - ❌ Complex layout with component isolation (use `Rs` component instead)
717
546
 
718
547
  ---
719
548
 
@@ -789,13 +618,9 @@ function animate(
789
618
  2. **dom-utils.svelte.ts** - Reusable DOM manipulation utilities (CSS variables, animation setup, sentinel creation)
790
619
  3. **useIntersection.svelte.ts** - IntersectionObserver composables for element visibility detection
791
620
 
792
- **Middle Layer - Base Implementation:**
793
- 4. **animate.svelte.ts** - Action for direct DOM node animation control
794
- 5. **runeScroller.svelte.ts** - **Recommended** - Sentinel-based action for precise animation timing
795
- 6. **BaseAnimated.svelte** - Base component handling intersection observer + animation logic
796
-
797
621
  **Top Layer - Consumer API:**
798
- 7. **Rs.svelte** - Main unified component (supports one-time & repeating via `repeat` prop)
622
+ 4. **runeScroller.svelte.ts** - **Recommended** - Sentinel-based action for scroll animation triggering
623
+ 5. **animate.svelte.ts** - Alternative action for direct DOM node animation control
799
624
 
800
625
  **Styles:**
801
626
  - **animations.css** - All animation keyframes & styles (14 animations, GPU-accelerated)
@@ -903,6 +728,8 @@ pnpm preview
903
728
 
904
729
  ## 🔗 Links
905
730
 
731
+ - [Live Demo](https://runescroller.lelab.dev/) - Interactive showcase of all animations
732
+ - [npm Package](https://www.npmjs.com/package/rune-scroller) - Install from npm
906
733
  - [Svelte 5 Documentation](https://svelte.dev)
907
734
  - [IntersectionObserver MDN](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
908
735
  - [LeLab.dev](https://lelab.dev)
@@ -1,13 +1,5 @@
1
1
  import type { Action } from 'svelte/action';
2
- import { type AnimationType } from './animations';
3
- export interface AnimateOptions {
4
- animation?: AnimationType;
5
- duration?: number;
6
- delay?: number;
7
- offset?: number;
8
- threshold?: number;
9
- rootMargin?: string;
10
- }
2
+ import type { AnimateOptions } from './types';
11
3
  /**
12
4
  * Svelte action for scroll animations
13
5
  * Triggers animation once when element enters viewport
@@ -13,8 +13,10 @@ export declare function setCSSVariables(element: HTMLElement, duration?: number,
13
13
  */
14
14
  export declare function setupAnimationElement(element: HTMLElement, animation: AnimationType): void;
15
15
  /**
16
- * Create and inject invisible sentinel element for observer-based triggering
17
- * @param element - Reference element (sentinel will be placed after it)
16
+ * Create sentinel element for observer-based triggering
17
+ * Positioned absolutely after element, stays fixed while element animates
18
+ * @param element - Reference element (used to position sentinel at its bottom)
19
+ * @param debug - If true, shows the sentinel as a visible line for debugging
18
20
  * @returns The created sentinel element
19
21
  */
20
- export declare function createSentinel(element: HTMLElement): HTMLElement;
22
+ export declare function createSentinel(element: HTMLElement, debug?: boolean): HTMLElement;
@@ -20,14 +20,27 @@ export function setupAnimationElement(element, animation) {
20
20
  element.setAttribute('data-animation', animation);
21
21
  }
22
22
  /**
23
- * Create and inject invisible sentinel element for observer-based triggering
24
- * @param element - Reference element (sentinel will be placed after it)
23
+ * Create sentinel element for observer-based triggering
24
+ * Positioned absolutely after element, stays fixed while element animates
25
+ * @param element - Reference element (used to position sentinel at its bottom)
26
+ * @param debug - If true, shows the sentinel as a visible line for debugging
25
27
  * @returns The created sentinel element
26
28
  */
27
- export function createSentinel(element) {
29
+ export function createSentinel(element, debug = false) {
28
30
  const sentinel = document.createElement('div');
29
- // Use cssText for efficient single-statement styling
30
- sentinel.style.cssText = 'height:20px;margin-top:0.5rem;visibility:hidden';
31
- element.parentNode?.insertBefore(sentinel, element.nextSibling);
31
+ // Get element dimensions to position sentinel at its bottom
32
+ const rect = element.getBoundingClientRect();
33
+ const elementHeight = rect.height;
34
+ if (debug) {
35
+ // Debug mode: visible primary color line (cyan #00e0ff)
36
+ sentinel.style.cssText =
37
+ `position:absolute;top:${elementHeight}px;left:0;right:0;height:3px;background:#00e0ff;margin:0;padding:0;box-sizing:border-box;z-index:999;pointer-events:none`;
38
+ sentinel.setAttribute('data-sentinel-debug', 'true');
39
+ }
40
+ else {
41
+ // Production: invisible positioned absolutely (no layout impact)
42
+ sentinel.style.cssText =
43
+ `position:absolute;top:${elementHeight}px;left:0;right:0;height:1px;visibility:hidden;margin:0;padding:0;box-sizing:border-box;pointer-events:none`;
44
+ }
32
45
  return sentinel;
33
46
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,7 @@
1
- export { default as rs } from './Rs.svelte';
1
+ export type { RuneScrollerOptions, AnimateOptions, IntersectionOptions, UseIntersectionReturn } from './types';
2
+ export type { AnimationType } from './animations';
3
+ export { runeScroller as default } from './runeScroller.svelte.ts';
4
+ export { default as RuneScroller } from './runeScroller.svelte';
2
5
  export { animate } from './animate.svelte';
3
- export type { AnimateOptions } from './animate.svelte';
4
- export { runeScroller } from './runeScroller.svelte';
5
- export type { RuneScrollerOptions } from './runeScroller.svelte';
6
6
  export { useIntersection, useIntersectionOnce } from './useIntersection.svelte';
7
- export type { IntersectionOptions, UseIntersectionReturn } from './useIntersection.svelte';
8
7
  export { calculateRootMargin } from './animations';
9
- export type { AnimationType } from './animations';
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- // Components
2
- export { default as rs } from './Rs.svelte';
3
- // Actions
1
+ // Main action (default export)
2
+ export { runeScroller as default } from './runeScroller.svelte.ts';
3
+ // Component
4
+ export { default as RuneScroller } from './runeScroller.svelte';
5
+ // Alternative actions
4
6
  export { animate } from './animate.svelte';
5
- export { runeScroller } from './runeScroller.svelte';
6
7
  // Composables
7
8
  export { useIntersection, useIntersectionOnce } from './useIntersection.svelte';
8
9
  // Utilities
@@ -1,29 +1,16 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { AnimationType } from './animations';
2
- export interface RuneScrollerOptions {
3
+ interface Props {
3
4
  animation?: AnimationType;
5
+ threshold?: number;
6
+ rootMargin?: string;
7
+ offset?: number;
4
8
  duration?: number;
9
+ delay?: number;
5
10
  repeat?: boolean;
11
+ children: Snippet;
12
+ [key: string]: any;
6
13
  }
7
- /**
8
- * Action pour animer un élément au scroll avec un sentinel invisible juste en dessous
9
- * @param element - L'élément à animer
10
- * @param options - Options d'animation (animation type, duration, et repeat)
11
- * @returns Objet action Svelte
12
- *
13
- * @example
14
- * ```svelte
15
- * <!-- Animation une seule fois -->
16
- * <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
17
- * Content
18
- * </div>
19
- *
20
- * <!-- Animation répétée à chaque scroll -->
21
- * <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000, repeat: true }}>
22
- * Content
23
- * </div>
24
- * ```
25
- */
26
- export declare function runeScroller(element: HTMLElement, options?: RuneScrollerOptions): {
27
- update(newOptions?: RuneScrollerOptions): void;
28
- destroy(): void;
29
- };
14
+ declare const RuneScroller: import("svelte").Component<Props, {}, "">;
15
+ type RuneScroller = ReturnType<typeof RuneScroller>;
16
+ export default RuneScroller;
@@ -24,8 +24,19 @@ export function runeScroller(element, options) {
24
24
  setupAnimationElement(element, options.animation);
25
25
  setCSSVariables(element, options.duration);
26
26
  }
27
- // Créer le sentinel invisible juste en dessous
28
- const sentinel = createSentinel(element);
27
+ // Créer le sentinel invisible (ou visible si debug=true)
28
+ // Sentinel positioned absolutely relative to parent (stays fixed while element animates)
29
+ const sentinel = createSentinel(element, options?.debug);
30
+ const parent = element.parentElement;
31
+ if (parent) {
32
+ // Ensure parent has position context for absolute positioning
33
+ const parentPosition = window.getComputedStyle(parent).position;
34
+ if (parentPosition === 'static') {
35
+ parent.style.position = 'relative';
36
+ }
37
+ // Insert sentinel after element, positioned absolutely
38
+ element.insertAdjacentElement('afterend', sentinel);
39
+ }
29
40
  // Observer le sentinel avec cleanup tracking
30
41
  let observerConnected = true;
31
42
  const observer = new IntersectionObserver((entries) => {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Centralized type definitions for Rune Scroller library
3
+ * All types are defined here for consistency and ease of maintenance
4
+ */
5
+ import type { AnimationType } from './animations';
6
+ /**
7
+ * Options for the runeScroller action
8
+ * Sentinel-based scroll animation triggering
9
+ */
10
+ export interface RuneScrollerOptions {
11
+ animation?: AnimationType;
12
+ duration?: number;
13
+ repeat?: boolean;
14
+ debug?: boolean;
15
+ }
16
+ /**
17
+ * Options for the animate action
18
+ * Direct DOM node animation control with threshold/offset support
19
+ */
20
+ export interface AnimateOptions {
21
+ animation?: AnimationType;
22
+ duration?: number;
23
+ delay?: number;
24
+ offset?: number;
25
+ threshold?: number;
26
+ rootMargin?: string;
27
+ }
28
+ /**
29
+ * Configuration options for IntersectionObserver
30
+ * Used by useIntersection and useIntersectionOnce composables
31
+ */
32
+ export interface IntersectionOptions {
33
+ threshold?: number | number[];
34
+ rootMargin?: string;
35
+ root?: Element | null;
36
+ }
37
+ /**
38
+ * Return type for useIntersection and useIntersectionOnce composables
39
+ * Provides reactive element reference and visibility state
40
+ */
41
+ export interface UseIntersectionReturn {
42
+ element: HTMLElement | null;
43
+ isVisible: boolean;
44
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Centralized type definitions for Rune Scroller library
3
+ * All types are defined here for consistency and ease of maintenance
4
+ */
5
+ export {};
@@ -1,16 +1,4 @@
1
- /**
2
- * Composable for handling IntersectionObserver logic
3
- * Reduces duplication between animation components
4
- */
5
- export interface IntersectionOptions {
6
- threshold?: number | number[];
7
- rootMargin?: string;
8
- root?: Element | null;
9
- }
10
- export interface UseIntersectionReturn {
11
- element: HTMLElement | null;
12
- isVisible: boolean;
13
- }
1
+ import type { IntersectionOptions } from './types';
14
2
  /**
15
3
  * Track element visibility with IntersectionObserver
16
4
  * Updates isVisible whenever visibility changes
@@ -1,4 +1,8 @@
1
1
  import { onMount } from 'svelte';
2
+ /**
3
+ * Composable for handling IntersectionObserver logic
4
+ * Reduces duplication between animation components
5
+ */
2
6
  /**
3
7
  * Factory function to create intersection observer composables
4
8
  * Eliminates duplication between useIntersection and useIntersectionOnce
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rune-scroller",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Lightweight, high-performance scroll animations for Svelte 5. ~2KB bundle, zero dependencies.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,16 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { AnimationType } from './animations';
3
- interface Props {
4
- animation?: AnimationType;
5
- threshold?: number;
6
- rootMargin?: string;
7
- offset?: number;
8
- duration?: number;
9
- delay?: number;
10
- repeat?: boolean;
11
- children: Snippet;
12
- [key: string]: any;
13
- }
14
- declare const Rs: import("svelte").Component<Props, {}, "">;
15
- type Rs = ReturnType<typeof Rs>;
16
- export default Rs;
File without changes