rune-scroller 0.0.1 → 0.1.0

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
@@ -15,7 +15,7 @@
15
15
  - ✅ **Svelte 5 Runes** : `$state`, `$props()` with snippets
16
16
  - ✅ **Zero Dependencies** : Pure Svelte 5 + IntersectionObserver
17
17
  - ✅ **Native Performance** : GPU-accelerated CSS animations
18
- - ✅ **26+ Animations** : Fade, Zoom, Flip, Slide, Bounce, and more
18
+ - ✅ **14 Built-in Animations** : Fade (5), Zoom (5), Flip (2), Slide Rotate, Bounce
19
19
  - ✅ **TypeScript** : Full type coverage with strict mode
20
20
  - ✅ **Customizable** : Duration, delay, threshold, offset per element
21
21
  - ✅ **Play Once or Repeat** : Control animation behavior
@@ -55,73 +55,225 @@ For a typical SvelteKit app:
55
55
  ## 📦 Project Structure
56
56
 
57
57
  ```
58
- rune-scroller/
58
+ rune-scroller-lib/
59
59
  ├── src/lib/
60
- │ ├── ScrollAnimate.svelte # One-time animation component
61
- │ ├── AnimatedElements.svelte # Repeating animation component
62
- │ ├── useIntersection.svelte.ts # IntersectionObserver composable
63
- │ ├── animations.ts # Animation configuration
64
- │ ├── animations.css # Animation styles
65
- └── viking-theme.css # Modern granite theme
66
- ├── src/routes/
67
- │ ├── +layout.svelte
68
- └── +page.svelte # Demo page
69
- └── package.json
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)
63
+ │ ├── useIntersection.svelte.ts # IntersectionObserver composables
64
+ │ ├── animate.svelte.ts # Animation action for direct DOM control
65
+ ├── animations.ts # Animation configuration & validation
66
+ ├── animations.css # Animation styles (14 animations)
67
+ │ ├── animations.test.ts # Animation configuration tests
68
+ ├── scroll-animate.test.ts # Component behavior tests
69
+ └── index.ts # Library entry point
70
+ ├── dist/ # Built library (created by pnpm build)
71
+ ├── package.json # npm package configuration
72
+ ├── svelte.config.js # SvelteKit configuration
73
+ ├── vite.config.ts # Vite build configuration
74
+ ├── tsconfig.json # TypeScript configuration
75
+ └── eslint.config.js # ESLint configuration
70
76
  ```
71
77
 
72
78
  ---
73
79
 
74
80
  ## 🚀 Quick Start
75
81
 
76
- ### 1. ScrollAnimate Component (Plays Once)
82
+ ### Installation
77
83
 
78
- Use `ScrollAnimate` for animations that play once when elements enter the viewport:
84
+ ```bash
85
+ npm install rune-scroller
86
+ ```
87
+
88
+ Or with other package managers:
89
+
90
+ ```bash
91
+ pnpm add rune-scroller
92
+ yarn add rune-scroller
93
+ ```
94
+
95
+ ### Basic Usage with `Rs` Component
96
+
97
+ The `Rs` component is the main component for scroll animations. Use the `repeat` prop to control animation behavior:
79
98
 
80
99
  ```svelte
81
100
  <script>
82
- import ScrollAnimate from '$lib/ScrollAnimate.svelte';
101
+ import Rs from 'rune-scroller';
102
+ import 'rune-scroller/animations.css';
83
103
  </script>
84
104
 
85
- <ScrollAnimate animation="fade-in">
86
- <div class="rune-card">
105
+ <!-- Play animation once when element enters viewport (default) -->
106
+ <Rs animation="fade-in">
107
+ <div class="card">
87
108
  <h2>Hello World</h2>
88
109
  <p>This element fades in once</p>
89
110
  </div>
90
- </ScrollAnimate>
91
- ```
111
+ </Rs>
92
112
 
93
- ### 2. AnimatedElements Component (Repeating)
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
+ ```
94
121
 
95
- Use `AnimatedElements` for animations that trigger every time the element enters the viewport:
122
+ ### In SvelteKit/Local Development
96
123
 
97
124
  ```svelte
98
125
  <script>
99
- import AnimatedElements from '$lib/AnimatedElements.svelte';
126
+ import Rs from '$lib/Rs.svelte';
127
+ import '$lib/animations.css';
100
128
  </script>
101
129
 
102
- <AnimatedElements animation="zoom-in">
103
- <div class="rune-card">
104
- <h2>Repeating Animation</h2>
105
- <p>This triggers each time you scroll past it</p>
130
+ <Rs animation="fade-in-up" duration={1000} delay={200}>
131
+ <div class="card">
132
+ <h2>Animated Content</h2>
106
133
  </div>
107
- </AnimatedElements>
134
+ </Rs>
135
+ ```
136
+
137
+ ### API Overview
138
+
139
+ The `Rs` component is the unified API for all scroll animations:
140
+
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
+ ```
158
+
159
+ ---
160
+
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.
164
+
165
+ ### Why Sentinels?
166
+
167
+ - **Accurate Timing** - Instead of triggering when the element enters, sentinel triggers slightly earlier
168
+ - **Consistent Behavior** - Same timing across all screen sizes and content heights
169
+ - **Simple API** - No complex offset calculations needed
170
+ - **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
+ ```
204
+
205
+ ### Sentinel-Based Examples
206
+
207
+ **Staggered animations with sentinels:**
208
+
209
+ ```svelte
210
+ <script>
211
+ import { runeScroller } from 'rune-scroller';
212
+ </script>
213
+
214
+ <div class="grid">
215
+ {#each items as item, i}
216
+ <div use:runeScroller={{ animation: 'fade-in-up', duration: 800 }}>
217
+ <h3>{item.title}</h3>
218
+ <p>{item.description}</p>
219
+ </div>
220
+ {/each}
221
+ </div>
222
+ ```
223
+
224
+ **Hero section with sentinel triggering:**
225
+
226
+ ```svelte
227
+ <div use:runeScroller={{ animation: 'fade-in-down', duration: 1000 }}>
228
+ <h1>Welcome to Our Site</h1>
229
+ </div>
230
+
231
+ <div use:runeScroller={{ animation: 'fade-in-up', duration: 1200 }}>
232
+ <p>Engaging content appears as you scroll</p>
233
+ </div>
234
+
235
+ <div use:runeScroller={{ animation: 'zoom-in', duration: 1000 }}>
236
+ <button class="cta">Get Started</button>
237
+ </div>
238
+ ```
239
+
240
+ ### `runeScroller` Options
241
+
242
+ ```typescript
243
+ interface RuneScrollerOptions {
244
+ animation?: AnimationType; // Animation type (e.g., 'fade-in-up')
245
+ duration?: number; // Duration in milliseconds (default: 2000)
246
+ repeat?: boolean; // Repeat animation on each scroll (default: false)
247
+ }
108
248
  ```
109
249
 
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
+
110
260
  ---
111
261
 
112
262
  ## ⚙️ Component Props
113
263
 
114
- ### ScrollAnimate
264
+ ### Rs Component
115
265
 
116
266
  ```typescript
117
- interface ScrollAnimateProps {
267
+ interface RsProps {
118
268
  animation?: string; // Animation type (default: 'fade-in')
119
269
  threshold?: number; // Visibility threshold (default: 0.5)
120
270
  offset?: number; // Trigger offset 0-100% (optional, uses default if not set)
121
271
  rootMargin?: string; // Observer margin (overrides offset if set)
122
272
  duration?: number; // Duration in ms (default: 800)
123
273
  delay?: number; // Delay in ms (default: 0)
274
+ repeat?: boolean; // Repeat animation on every scroll (default: false)
124
275
  children: Snippet; // Content to animate
276
+ [key: string]: any; // Accepts any HTML attributes (e.g., data-testid, class, etc.)
125
277
  }
126
278
  ```
127
279
 
@@ -138,45 +290,51 @@ Controls when the animation triggers as the element scrolls into view. If not sp
138
290
 
139
291
  ```svelte
140
292
  <!-- Early trigger (bottom of screen) -->
141
- <ScrollAnimate animation="fade-in-up" offset={0}>
142
- <div class="rune-card">Animates early</div>
143
- </ScrollAnimate>
293
+ <Rs animation="fade-in-up" offset={0}>
294
+ <div class="card">Animates early</div>
295
+ </Rs>
144
296
 
145
297
  <!-- Late trigger (top of screen) -->
146
- <ScrollAnimate animation="fade-in-up" offset={100}>
147
- <div class="rune-card">Animates late</div>
148
- </ScrollAnimate>
298
+ <Rs animation="fade-in-up" offset={100}>
299
+ <div class="card">Animates late</div>
300
+ </Rs>
149
301
 
150
302
  <!-- Custom timing -->
151
- <ScrollAnimate animation="fade-in-up" offset={75}>
152
- <div class="rune-card">Animates at 75%</div>
153
- </ScrollAnimate>
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>
154
311
  ```
155
312
 
156
313
  **Full example with all props:**
157
314
 
158
315
  ```svelte
159
- <ScrollAnimate animation="fade-in-up" duration={1200} delay={300} threshold={0.8} offset={25}>
160
- <div class="rune-card">
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">
161
326
  <h2>Custom Timing</h2>
162
327
  <p>Duration: 1200ms, Delay: 300ms, Threshold: 80%, Offset: 25%</p>
163
328
  </div>
164
- </ScrollAnimate>
329
+ </Rs>
165
330
  ```
166
331
 
167
- ### AnimatedElements
332
+ #### Repeat Behavior
168
333
 
169
- ```typescript
170
- interface AnimatedElementsProps {
171
- animation?: string; // Animation type (default: 'fade-in')
172
- threshold?: number; // Visibility threshold (default: 0.5)
173
- offset?: number; // Trigger offset 0-100% (optional, uses default if not set)
174
- rootMargin?: string; // Observer margin (overrides offset if set)
175
- children: Snippet; // Content to animate
176
- }
177
- ```
334
+ Use the `repeat` prop to control animation behavior:
178
335
 
179
- Same `offset` behavior as `ScrollAnimate`, but animation **repeats on every scroll pass** instead of playing once.
336
+ - `repeat={false}` (default) - Animation plays **once** when element enters viewport
337
+ - `repeat={true}` - Animation **repeats** every time element enters viewport
180
338
 
181
339
  ---
182
340
 
@@ -189,12 +347,12 @@ Same `offset` behavior as `ScrollAnimate`, but animation **repeats on every scro
189
347
  Simple opacity fade from transparent to visible.
190
348
 
191
349
  ```svelte
192
- <ScrollAnimate animation="fade-in">
193
- <div class="rune-card">
350
+ <Rs animation="fade-in">
351
+ <div class="card">
194
352
  <h2>Fade In</h2>
195
353
  <p>Simple fade entrance</p>
196
354
  </div>
197
- </ScrollAnimate>
355
+ </Rs>
198
356
  ```
199
357
 
200
358
  #### `fade-in-up`
@@ -202,12 +360,12 @@ Simple opacity fade from transparent to visible.
202
360
  Fades in while moving up 100px.
203
361
 
204
362
  ```svelte
205
- <ScrollAnimate animation="fade-in-up">
206
- <div class="rune-card">
363
+ <Rs animation="fade-in-up">
364
+ <div class="card">
207
365
  <h2>Fade In Up</h2>
208
366
  <p>Rises from below</p>
209
367
  </div>
210
- </ScrollAnimate>
368
+ </Rs>
211
369
  ```
212
370
 
213
371
  #### `fade-in-down`
@@ -215,12 +373,12 @@ Fades in while moving up 100px.
215
373
  Fades in while moving down 100px.
216
374
 
217
375
  ```svelte
218
- <ScrollAnimate animation="fade-in-down">
219
- <div class="rune-card">
376
+ <Rs animation="fade-in-down">
377
+ <div class="card">
220
378
  <h2>Fade In Down</h2>
221
379
  <p>Descends from above</p>
222
380
  </div>
223
- </ScrollAnimate>
381
+ </Rs>
224
382
  ```
225
383
 
226
384
  #### `fade-in-left`
@@ -228,12 +386,12 @@ Fades in while moving down 100px.
228
386
  Fades in while moving left 100px.
229
387
 
230
388
  ```svelte
231
- <ScrollAnimate animation="fade-in-left">
232
- <div class="rune-card">
389
+ <Rs animation="fade-in-left">
390
+ <div class="card">
233
391
  <h2>Fade In Left</h2>
234
392
  <p>Comes from the right</p>
235
393
  </div>
236
- </ScrollAnimate>
394
+ </Rs>
237
395
  ```
238
396
 
239
397
  #### `fade-in-right`
@@ -241,29 +399,29 @@ Fades in while moving left 100px.
241
399
  Fades in while moving right 100px.
242
400
 
243
401
  ```svelte
244
- <ScrollAnimate animation="fade-in-right">
245
- <div class="rune-card">
402
+ <Rs animation="fade-in-right">
403
+ <div class="card">
246
404
  <h2>Fade In Right</h2>
247
405
  <p>Comes from the left</p>
248
406
  </div>
249
- </ScrollAnimate>
407
+ </Rs>
250
408
  ```
251
409
 
252
410
  ---
253
411
 
254
- ### Zoom (2 variants)
412
+ ### Zoom (5 variants)
255
413
 
256
414
  #### `zoom-in`
257
415
 
258
416
  Scales from 50% to 100% while fading in.
259
417
 
260
418
  ```svelte
261
- <ScrollAnimate animation="zoom-in">
262
- <div class="rune-card">
419
+ <Rs animation="zoom-in">
420
+ <div class="card">
263
421
  <h2>Zoom In</h2>
264
422
  <p>Grows into view</p>
265
423
  </div>
266
- </ScrollAnimate>
424
+ </Rs>
267
425
  ```
268
426
 
269
427
  #### `zoom-out`
@@ -271,12 +429,51 @@ Scales from 50% to 100% while fading in.
271
429
  Scales from 150% to 100% while fading in.
272
430
 
273
431
  ```svelte
274
- <ScrollAnimate animation="zoom-out">
275
- <div class="rune-card">
432
+ <Rs animation="zoom-out">
433
+ <div class="card">
276
434
  <h2>Zoom Out</h2>
277
435
  <p>Shrinks into view</p>
278
436
  </div>
279
- </ScrollAnimate>
437
+ </Rs>
438
+ ```
439
+
440
+ #### `zoom-in-up`
441
+
442
+ Scales from 50% while translating up 50px.
443
+
444
+ ```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>
451
+ ```
452
+
453
+ #### `zoom-in-left`
454
+
455
+ Scales from 50% while translating left 50px.
456
+
457
+ ```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>
464
+ ```
465
+
466
+ #### `zoom-in-right`
467
+
468
+ Scales from 50% while translating right 50px.
469
+
470
+ ```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>
280
477
  ```
281
478
 
282
479
  ---
@@ -288,12 +485,12 @@ Scales from 150% to 100% while fading in.
288
485
  3D rotation on Y axis (left to right).
289
486
 
290
487
  ```svelte
291
- <ScrollAnimate animation="flip">
292
- <div class="rune-card">
488
+ <Rs animation="flip">
489
+ <div class="card">
293
490
  <h2>Flip</h2>
294
491
  <p>Rotates on Y axis</p>
295
492
  </div>
296
- </ScrollAnimate>
493
+ </Rs>
297
494
  ```
298
495
 
299
496
  #### `flip-x`
@@ -301,12 +498,12 @@ Scales from 150% to 100% while fading in.
301
498
  3D rotation on X axis (top to bottom).
302
499
 
303
500
  ```svelte
304
- <ScrollAnimate animation="flip-x">
305
- <div class="rune-card">
501
+ <Rs animation="flip-x">
502
+ <div class="card">
306
503
  <h2>Flip X</h2>
307
504
  <p>Rotates on X axis</p>
308
505
  </div>
309
- </ScrollAnimate>
506
+ </Rs>
310
507
  ```
311
508
 
312
509
  ---
@@ -318,12 +515,12 @@ Scales from 150% to 100% while fading in.
318
515
  Slides from left while rotating 45 degrees.
319
516
 
320
517
  ```svelte
321
- <ScrollAnimate animation="slide-rotate">
322
- <div class="rune-card">
518
+ <Rs animation="slide-rotate">
519
+ <div class="card">
323
520
  <h2>Slide Rotate</h2>
324
521
  <p>Slides and spins</p>
325
522
  </div>
326
- </ScrollAnimate>
523
+ </Rs>
327
524
  ```
328
525
 
329
526
  ---
@@ -335,30 +532,30 @@ Slides from left while rotating 45 degrees.
335
532
  Bouncy entrance with scaling keyframe animation.
336
533
 
337
534
  ```svelte
338
- <ScrollAnimate animation="bounce-in" duration={800}>
339
- <div class="rune-card">
535
+ <Rs animation="bounce-in" duration={800}>
536
+ <div class="card">
340
537
  <h2>Bounce In</h2>
341
538
  <p>Bounces into view</p>
342
539
  </div>
343
- </ScrollAnimate>
540
+ </Rs>
344
541
  ```
345
542
 
346
543
  ---
347
544
 
348
545
  ### Compare: Once vs Repeat
349
546
 
350
- **Same animation, different behavior:**
547
+ **Same animation, different behavior using the `repeat` prop:**
351
548
 
352
549
  ```svelte
353
- <!-- Plays once on scroll down -->
354
- <ScrollAnimate animation="fade-in-up">
355
- <div class="rune-card">Animates once</div>
356
- </ScrollAnimate>
550
+ <!-- Plays once on scroll down (default) -->
551
+ <Rs animation="fade-in-up">
552
+ <div class="card">Animates once</div>
553
+ </Rs>
357
554
 
358
555
  <!-- Repeats each time you scroll by -->
359
- <AnimatedElements animation="fade-in-up">
360
- <div class="rune-card">Animates on every scroll</div>
361
- </AnimatedElements>
556
+ <Rs animation="fade-in-up" repeat>
557
+ <div class="card">Animates on every scroll</div>
558
+ </Rs>
362
559
  ```
363
560
 
364
561
  ---
@@ -371,17 +568,17 @@ Animate cards with progressive delays:
371
568
 
372
569
  ```svelte
373
570
  <script>
374
- import ScrollAnimate from '$lib/ScrollAnimate.svelte';
571
+ import Rs from '$lib/Rs.svelte';
375
572
  </script>
376
573
 
377
574
  <div class="grid">
378
575
  {#each items as item, i}
379
- <ScrollAnimate animation="fade-in-up" delay={i * 100}>
380
- <div class="rune-card">
576
+ <Rs animation="fade-in-up" delay={i * 100}>
577
+ <div class="card">
381
578
  <h3>{item.title}</h3>
382
579
  <p>{item.description}</p>
383
580
  </div>
384
- </ScrollAnimate>
581
+ </Rs>
385
582
  {/each}
386
583
  </div>
387
584
  ```
@@ -389,100 +586,154 @@ Animate cards with progressive delays:
389
586
  ### Mixed Animations
390
587
 
391
588
  ```svelte
392
- <ScrollAnimate animation="fade-in">
589
+ <Rs animation="fade-in">
393
590
  <section>Content fades in</section>
394
- </ScrollAnimate>
591
+ </Rs>
395
592
 
396
- <ScrollAnimate animation="slide-rotate">
593
+ <Rs animation="slide-rotate">
397
594
  <section>Content slides and rotates</section>
398
- </ScrollAnimate>
595
+ </Rs>
399
596
 
400
- <AnimatedElements animation="zoom-in">
597
+ <Rs animation="zoom-in" repeat>
401
598
  <section>Content zooms in repeatedly</section>
402
- </AnimatedElements>
599
+ </Rs>
403
600
  ```
404
601
 
405
602
  ### Hero Section
406
603
 
407
604
  ```svelte
408
605
  <script>
409
- import ScrollAnimate from '$lib/ScrollAnimate.svelte';
606
+ import Rs from '$lib/Rs.svelte';
410
607
  </script>
411
608
 
412
609
  <section class="hero">
413
610
  <div class="hero-content">
414
- <ScrollAnimate animation="fade-in" delay={0}>
611
+ <Rs animation="fade-in" delay={0}>
415
612
  <h1>Welcome</h1>
416
- </ScrollAnimate>
613
+ </Rs>
417
614
 
418
- <ScrollAnimate animation="fade-in" delay={200}>
615
+ <Rs animation="fade-in" delay={200}>
419
616
  <p>Scroll to reveal more</p>
420
- </ScrollAnimate>
617
+ </Rs>
421
618
 
422
- <ScrollAnimate animation="zoom-in" delay={400}>
619
+ <Rs animation="zoom-in" delay={400}>
423
620
  <button>Get Started</button>
424
- </ScrollAnimate>
621
+ </Rs>
425
622
  </div>
426
623
  </section>
427
624
  ```
428
625
 
429
626
  ---
430
627
 
431
- ## 🎨 Theming
628
+ ## 🔧 Composables & Actions
432
629
 
433
- The project includes a modern **Granite + Electric Blue** theme in `src/lib/viking-theme.css`.
630
+ ### runeScroller (Recommended)
434
631
 
435
- ### Color Palette
632
+ The `runeScroller` action provides sentinel-based animation triggering for precise timing control:
436
633
 
437
- ```css
438
- --granite-dark: #0f1419;
439
- --granite-medium: #1a1f2e;
440
- --granite-light: #252d3d;
441
- --electric-blue: #00d9ff;
442
- --text-primary: #f0f2f5;
443
- --text-secondary: #a8b0be;
634
+ ```typescript
635
+ function runeScroller(
636
+ element: HTMLElement,
637
+ options?: {
638
+ animation?: AnimationType; // Animation type
639
+ duration?: number; // Duration in ms (default: 2000)
640
+ repeat?: boolean; // Repeat animation on each scroll (default: false)
641
+ }
642
+ ): { update?: (newOptions) => void; destroy?: () => void }
643
+ ```
644
+
645
+ **Key Features:**
646
+ - Automatically creates an invisible 20px sentinel element below your content
647
+ - Triggers animation when sentinel enters viewport
648
+ - Provides consistent timing across all screen sizes
649
+ - Minimal configuration needed
650
+ - Supports both one-time and repeating animations
651
+
652
+ **Basic Example (One-time animation):**
653
+
654
+ ```svelte
655
+ <script>
656
+ import { runeScroller } from 'rune-scroller';
657
+ </script>
658
+
659
+ <!-- Animation plays once when sentinel enters viewport -->
660
+ <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
661
+ Animated content with sentinel-based triggering
662
+ </div>
663
+ ```
664
+
665
+ **Repeating Animation:**
666
+
667
+ ```svelte
668
+ <!-- Animation repeats each time sentinel enters viewport -->
669
+ <div use:runeScroller={{ animation: 'bounce-in', duration: 800, repeat: true }}>
670
+ This animates every time you scroll past it
671
+ </div>
444
672
  ```
445
673
 
446
- ### Card Classes
674
+ **Complete Examples:**
447
675
 
448
676
  ```svelte
449
- <!-- Large card -->
450
- <div class="rune-card">
451
- <h2>Title</h2>
452
- <p>Content</p>
677
+ <script>
678
+ import { runeScroller } from 'rune-scroller';
679
+ </script>
680
+
681
+ <!-- Fade in once on scroll -->
682
+ <div use:runeScroller={{ animation: 'fade-in', duration: 600 }}>
683
+ <h2>Section Title</h2>
684
+ <p>Fades in when scrolled into view</p>
453
685
  </div>
454
686
 
455
- <!-- Small card (for grids) -->
456
- <div class="rune-card small">
457
- <h3>Small Title</h3>
458
- <p>Small content</p>
687
+ <!-- Zoom in with longer duration -->
688
+ <div use:runeScroller={{ animation: 'zoom-in-up', duration: 1200 }}>
689
+ <div class="card">
690
+ <h3>Card Title</h3>
691
+ <p>Zooms in from below</p>
692
+ </div>
459
693
  </div>
460
694
 
461
- <!-- Divider line -->
462
- <div class="rune-divider"></div>
695
+ <!-- Repeating animation for interactive effect -->
696
+ <div use:runeScroller={{ animation: 'bounce-in', duration: 700, repeat: true }}>
697
+ <button class="interactive-button">Bounces on each scroll</button>
698
+ </div>
699
+
700
+ <!-- Complex staggered layout -->
701
+ <div class="grid">
702
+ {#each items as item, i}
703
+ <div use:runeScroller={{ animation: 'fade-in-up', duration: 800 }}>
704
+ <h3>{item.title}</h3>
705
+ <p>{item.description}</p>
706
+ </div>
707
+ {/each}
708
+ </div>
463
709
  ```
464
710
 
465
- ---
711
+ **When to use:**
712
+ - ✅ Simple element animations
713
+ - ✅ Consistent timing across layouts
714
+ - ✅ Minimal overhead applications
715
+ - ✅ Both one-time and repeating animations
716
+ - ❌ Complex layout with component isolation (use `Rs` component instead)
466
717
 
467
- ## 🔧 Composables
718
+ ---
468
719
 
469
720
  ### useIntersectionOnce
470
721
 
471
- For animations that play only once (used by `ScrollAnimate`):
722
+ For one-time animations:
472
723
 
473
724
  ```typescript
474
725
  function useIntersectionOnce(options?: {
475
726
  threshold?: number;
476
727
  rootMargin?: string;
477
728
  root?: Element | null;
478
- });
729
+ }): { element: HTMLElement | null; isVisible: boolean }
479
730
  ```
480
731
 
481
- Returns `{ element, isVisible }` — bind `element` to your target, `isVisible` becomes `true` once.
732
+ Returns `{ element, isVisible }` — bind `element` to your target, `isVisible` becomes `true` once, then observer unobserves.
482
733
 
483
734
  ### useIntersection
484
735
 
485
- For repeating animations (used by `AnimatedElements`):
736
+ For repeating animations:
486
737
 
487
738
  ```typescript
488
739
  function useIntersection(
@@ -492,22 +743,62 @@ function useIntersection(
492
743
  root?: Element | null;
493
744
  },
494
745
  onVisible?: (isVisible: boolean) => void
495
- );
746
+ ): { element: HTMLElement | null; isVisible: boolean }
747
+ ```
748
+
749
+ Returns `{ element, isVisible }` — `isVisible` toggles `true`/`false` on each scroll pass.
750
+
751
+ ### animate Action
752
+
753
+ For direct DOM animation control without component wrapper:
754
+
755
+ ```typescript
756
+ function animate(
757
+ node: HTMLElement,
758
+ options?: {
759
+ animation?: AnimationType; // Default: 'fade-in'
760
+ duration?: number; // Default: 800
761
+ delay?: number; // Default: 0
762
+ offset?: number; // Optional trigger offset
763
+ threshold?: number; // Default: 0
764
+ rootMargin?: string; // Optional custom margin
765
+ }
766
+ ): { update?: (newOptions) => void; destroy?: () => void }
496
767
  ```
497
768
 
498
- Returns `{ element, isVisible }` — `isVisible` toggles on each scroll.
769
+ **Example:**
770
+
771
+ ```svelte
772
+ <script>
773
+ import { animate } from 'rune-scroller';
774
+ </script>
775
+
776
+ <div use:animate={{ animation: 'fade-in-up', duration: 1000, delay: 200 }}>
777
+ Animated content
778
+ </div>
779
+ ```
499
780
 
500
781
  ---
501
782
 
502
783
  ## 🏗️ Architecture
503
784
 
504
- ### Animation System
785
+ ### Core Layer Architecture
786
+
787
+ **Bottom Layer - Browser APIs & Utilities:**
788
+ 1. **animations.ts** - Animation type definitions, validation, and utilities
789
+ 2. **dom-utils.svelte.ts** - Reusable DOM manipulation utilities (CSS variables, animation setup, sentinel creation)
790
+ 3. **useIntersection.svelte.ts** - IntersectionObserver composables for element visibility detection
791
+
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
+ **Top Layer - Consumer API:**
798
+ 7. **Rs.svelte** - Main unified component (supports one-time & repeating via `repeat` prop)
505
799
 
506
- 1. **animations.ts** - Configuration and validation
507
- 2. **animations.css** - Reusable animation styles
508
- 3. **useIntersection.svelte.ts** - IntersectionObserver logic
509
- 4. **ScrollAnimate.svelte** - One-time wrapper component
510
- 5. **AnimatedElements.svelte** - Repeating wrapper component
800
+ **Styles:**
801
+ - **animations.css** - All animation keyframes & styles (14 animations, GPU-accelerated)
511
802
 
512
803
  ### Key Principles
513
804
 
@@ -515,6 +806,45 @@ Returns `{ element, isVisible }` — `isVisible` toggles on each scroll.
515
806
  - **CSS-Based** : Animations use CSS transforms + transitions (hardware-accelerated)
516
807
  - **Type-Safe** : Full TypeScript support
517
808
  - **Composable** : Use hooks directly or wrapped components
809
+ - **DRY (Don't Repeat Yourself)** : Utility functions eliminate code duplication
810
+ - **Optimal DOM Manipulation** : Uses `cssText` for efficient single-statement styling
811
+
812
+ ---
813
+
814
+ ## 🚀 Optimizations
815
+
816
+ ### Recent Improvements (v1.1.0)
817
+
818
+ **DOM Utility Extraction**
819
+ - Extracted repeated DOM manipulation patterns into reusable utilities (`dom-utils.svelte.ts`)
820
+ - `setCSSVariables()` - Centralizes CSS custom property management
821
+ - `setupAnimationElement()` - Consistent animation class/attribute setup
822
+ - `createSentinel()` - Optimized sentinel creation using single `cssText` statement
823
+ - **Result**: Reduced code duplication, improved maintainability, cleaner codebase
824
+
825
+ **Memory Leak Fixes**
826
+ - Fixed potential memory leaks in repeat mode by tracking observer connection state
827
+ - Observer now properly disconnects in destroy lifecycle
828
+ - Prevents accumulation of observers on long-scroll pages
829
+ - **Result**: Better performance on content-heavy sites with many animations
830
+
831
+ **Observer Logic Improvements**
832
+ - Fixed `animate.svelte.ts` to properly handle dynamic threshold/rootMargin changes
833
+ - Observer now recreates when trigger options change at runtime
834
+ - Maintains correct state throughout component lifecycle
835
+ - **Result**: More reliable dynamic animation updates
836
+
837
+ **Bundle Size Optimization**
838
+ - Updated `.npmignore` to exclude test files from npm distribution
839
+ - Removes `*.test.ts`, `*.test.js` and built test files
840
+ - **Result**: ~3.6 KB reduction in package size
841
+
842
+ ### Performance Impact
843
+
844
+ - **Code Size**: Reduced duplication without sacrificing readability
845
+ - **Runtime Performance**: Fewer DOM operations via optimized `cssText` usage
846
+ - **Memory Efficiency**: Proper observer cleanup prevents memory leaks
847
+ - **Bundle Size**: Test files excluded from distribution
518
848
 
519
849
  ---
520
850
 
@@ -540,10 +870,22 @@ pnpm dev
540
870
  # Type checking
541
871
  pnpm check
542
872
 
873
+ # Type checking in watch mode
874
+ pnpm check:watch
875
+
543
876
  # Format code
544
877
  pnpm format
545
878
 
546
- # Preview build
879
+ # Lint code
880
+ pnpm lint
881
+
882
+ # Build library for npm
883
+ pnpm build
884
+
885
+ # Run tests
886
+ pnpm test
887
+
888
+ # Preview built library
547
889
  pnpm preview
548
890
  ```
549
891
 
@@ -552,9 +894,10 @@ pnpm preview
552
894
  ## 📝 Notes
553
895
 
554
896
  - **Why "Rune"?** Svelte 5 uses **Runes** (`$state`, `$props()`) as core reactivity primitives
555
- - **Theme Name** : Granite + Electric Blue = Modern, minimalist aesthetic
556
- - **No Dependencies** : Pure Svelte 5 + Browser APIs
897
+ - **Zero Dependencies** : Pure Svelte 5 + Native Browser APIs (IntersectionObserver)
557
898
  - **Extensible** : Add new animations by extending `animations.ts` and `animations.css`
899
+ - **Library Only** : This is the library repository. The demo website is in `rune-scroller-site`
900
+ - **Published as npm Package** : `rune-scroller` on npm registry
558
901
 
559
902
  ---
560
903