rune-scroller 0.1.0 → 0.1.1

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.
@@ -1,4 +1,5 @@
1
1
  import { calculateRootMargin } from './animations';
2
+ import { setCSSVariables, setupAnimationElement } from './dom-utils.svelte';
2
3
  /**
3
4
  * Svelte action for scroll animations
4
5
  * Triggers animation once when element enters viewport
@@ -11,17 +12,15 @@ import { calculateRootMargin } from './animations';
11
12
  * ```
12
13
  */
13
14
  export const animate = (node, options = {}) => {
14
- const { animation = 'fade-in', duration = 800, delay = 0, offset, threshold = 0, rootMargin } = options;
15
+ let { animation = 'fade-in', duration = 800, delay = 0, offset, threshold = 0, rootMargin } = options;
15
16
  // Calculate rootMargin from offset (0-100%)
16
- const finalRootMargin = calculateRootMargin(offset, rootMargin);
17
- // Set CSS custom properties for timing
18
- node.style.setProperty('--duration', `${duration}ms`);
19
- node.style.setProperty('--delay', `${delay}ms`);
20
- // Add base animation class and data attribute
21
- node.classList.add('scroll-animate');
22
- node.setAttribute('data-animation', animation);
17
+ let finalRootMargin = calculateRootMargin(offset, rootMargin);
18
+ // Setup animation with utilities
19
+ setupAnimationElement(node, animation);
20
+ setCSSVariables(node, duration, delay);
23
21
  // Track if animation has been triggered
24
22
  let animated = false;
23
+ let observerConnected = true;
25
24
  // Create IntersectionObserver for one-time animation
26
25
  const observer = new IntersectionObserver((entries) => {
27
26
  entries.forEach((entry) => {
@@ -31,6 +30,7 @@ export const animate = (node, options = {}) => {
31
30
  animated = true;
32
31
  // Stop observing after animation triggers
33
32
  observer.unobserve(node);
33
+ observerConnected = false;
34
34
  }
35
35
  });
36
36
  }, {
@@ -40,20 +40,40 @@ export const animate = (node, options = {}) => {
40
40
  observer.observe(node);
41
41
  return {
42
42
  update(newOptions) {
43
- const { duration: newDuration = 800, delay: newDelay = 0, animation: newAnimation } = newOptions;
43
+ const { duration: newDuration, delay: newDelay, animation: newAnimation, offset: newOffset, threshold: newThreshold, rootMargin: newRootMargin } = newOptions;
44
44
  // Update CSS properties
45
- if (newDuration !== duration) {
46
- node.style.setProperty('--duration', `${newDuration}ms`);
45
+ if (newDuration !== undefined) {
46
+ duration = newDuration;
47
+ setCSSVariables(node, duration, newDelay ?? delay);
47
48
  }
48
- if (newDelay !== delay) {
49
- node.style.setProperty('--delay', `${newDelay}ms`);
49
+ if (newDelay !== undefined && newDelay !== delay) {
50
+ delay = newDelay;
51
+ setCSSVariables(node, duration, delay);
50
52
  }
51
53
  if (newAnimation && newAnimation !== animation) {
54
+ animation = newAnimation;
52
55
  node.setAttribute('data-animation', newAnimation);
53
56
  }
57
+ // Recreate observer if threshold or rootMargin changed
58
+ if (newThreshold !== undefined || newOffset !== undefined || newRootMargin !== undefined) {
59
+ if (observerConnected) {
60
+ observer.disconnect();
61
+ observerConnected = false;
62
+ }
63
+ threshold = newThreshold ?? threshold;
64
+ offset = newOffset ?? offset;
65
+ rootMargin = newRootMargin ?? rootMargin;
66
+ finalRootMargin = calculateRootMargin(offset, rootMargin);
67
+ if (!animated) {
68
+ observer.observe(node);
69
+ observerConnected = true;
70
+ }
71
+ }
54
72
  },
55
73
  destroy() {
56
- observer.disconnect();
74
+ if (observerConnected) {
75
+ observer.disconnect();
76
+ }
57
77
  }
58
78
  };
59
79
  };
@@ -0,0 +1,20 @@
1
+ import type { AnimationType } from './animations';
2
+ /**
3
+ * Set CSS custom properties on an element
4
+ * @param element - Target DOM element
5
+ * @param duration - Animation duration in milliseconds
6
+ * @param delay - Animation delay in milliseconds
7
+ */
8
+ export declare function setCSSVariables(element: HTMLElement, duration?: number, delay?: number): void;
9
+ /**
10
+ * Setup animation element with required classes and attributes
11
+ * @param element - Target DOM element
12
+ * @param animation - Animation type to apply
13
+ */
14
+ export declare function setupAnimationElement(element: HTMLElement, animation: AnimationType): void;
15
+ /**
16
+ * Create and inject invisible sentinel element for observer-based triggering
17
+ * @param element - Reference element (sentinel will be placed after it)
18
+ * @returns The created sentinel element
19
+ */
20
+ export declare function createSentinel(element: HTMLElement): HTMLElement;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Set CSS custom properties on an element
3
+ * @param element - Target DOM element
4
+ * @param duration - Animation duration in milliseconds
5
+ * @param delay - Animation delay in milliseconds
6
+ */
7
+ export function setCSSVariables(element, duration, delay = 0) {
8
+ if (duration !== undefined) {
9
+ element.style.setProperty('--duration', `${duration}ms`);
10
+ }
11
+ element.style.setProperty('--delay', `${delay}ms`);
12
+ }
13
+ /**
14
+ * Setup animation element with required classes and attributes
15
+ * @param element - Target DOM element
16
+ * @param animation - Animation type to apply
17
+ */
18
+ export function setupAnimationElement(element, animation) {
19
+ element.classList.add('scroll-animate');
20
+ element.setAttribute('data-animation', animation);
21
+ }
22
+ /**
23
+ * Create and inject invisible sentinel element for observer-based triggering
24
+ * @param element - Reference element (sentinel will be placed after it)
25
+ * @returns The created sentinel element
26
+ */
27
+ export function createSentinel(element) {
28
+ 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);
32
+ return sentinel;
33
+ }
@@ -1,3 +1,4 @@
1
+ import { setCSSVariables, setupAnimationElement, createSentinel } from './dom-utils.svelte';
1
2
  /**
2
3
  * Action pour animer un élément au scroll avec un sentinel invisible juste en dessous
3
4
  * @param element - L'élément à animer
@@ -18,27 +19,15 @@
18
19
  * ```
19
20
  */
20
21
  export function runeScroller(element, options) {
21
- // Setup animation classes et variables CSS si options sont fournies
22
+ // Setup animation classes et variables CSS
22
23
  if (options?.animation || options?.duration) {
23
- element.classList.add('scroll-animate');
24
- if (options.animation) {
25
- element.setAttribute('data-animation', options.animation);
26
- }
27
- if (options.duration) {
28
- element.style.setProperty('--duration', `${options.duration}ms`);
29
- }
30
- element.style.setProperty('--delay', '0ms');
24
+ setupAnimationElement(element, options.animation);
25
+ setCSSVariables(element, options.duration);
31
26
  }
32
27
  // Créer le sentinel invisible juste en dessous
33
- const sentinel = document.createElement('div');
34
- sentinel.style.height = '20px';
35
- sentinel.style.margin = '0';
36
- sentinel.style.padding = '0';
37
- sentinel.style.marginTop = '0.5rem';
38
- sentinel.style.visibility = 'hidden';
39
- // Insérer le sentinel après l'élément
40
- element.parentNode?.insertBefore(sentinel, element.nextSibling);
41
- // Observer le sentinel
28
+ const sentinel = createSentinel(element);
29
+ // Observer le sentinel avec cleanup tracking
30
+ let observerConnected = true;
42
31
  const observer = new IntersectionObserver((entries) => {
43
32
  const isIntersecting = entries[0].isIntersecting;
44
33
  if (isIntersecting) {
@@ -47,6 +36,7 @@ export function runeScroller(element, options) {
47
36
  // Déconnecter si pas en mode repeat
48
37
  if (!options?.repeat) {
49
38
  observer.disconnect();
39
+ observerConnected = false;
50
40
  }
51
41
  }
52
42
  else if (options?.repeat) {
@@ -61,7 +51,7 @@ export function runeScroller(element, options) {
61
51
  element.setAttribute('data-animation', newOptions.animation);
62
52
  }
63
53
  if (newOptions?.duration) {
64
- element.style.setProperty('--duration', `${newOptions.duration}ms`);
54
+ setCSSVariables(element, newOptions.duration);
65
55
  }
66
56
  // Update repeat option
67
57
  if (newOptions?.repeat !== undefined && newOptions.repeat !== options?.repeat) {
@@ -69,7 +59,9 @@ export function runeScroller(element, options) {
69
59
  }
70
60
  },
71
61
  destroy() {
72
- observer.disconnect();
62
+ if (observerConnected) {
63
+ observer.disconnect();
64
+ }
73
65
  sentinel.remove();
74
66
  }
75
67
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rune-scroller",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Lightweight, high-performance scroll animations for Svelte 5. ~2KB bundle, zero dependencies.",
5
5
  "type": "module",
6
6
  "sideEffects": false,