rune-scroller 0.1.11 → 2.0.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.
Files changed (51) hide show
  1. package/README.md +195 -29
  2. package/dist/__mocks__/IntersectionObserver.d.ts +25 -0
  3. package/dist/__mocks__/IntersectionObserver.js +116 -0
  4. package/dist/__mocks__/svelte-runes.d.ts +25 -0
  5. package/dist/__mocks__/svelte-runes.js +117 -0
  6. package/dist/__test-helpers__/dom.d.ts +118 -0
  7. package/dist/__test-helpers__/dom.js +305 -0
  8. package/dist/animate.d.ts +4 -0
  9. package/dist/animate.js +152 -0
  10. package/dist/animate.test.js +370 -0
  11. package/dist/animations.comprehensive.test.d.ts +1 -0
  12. package/dist/animations.comprehensive.test.js +432 -0
  13. package/dist/animations.css +21 -12
  14. package/dist/animations.d.ts +12 -9
  15. package/dist/animations.js +31 -6
  16. package/dist/animations.test.js +23 -41
  17. package/dist/dom-utils.d.ts +40 -0
  18. package/dist/dom-utils.js +111 -0
  19. package/dist/dom-utils.test.d.ts +1 -0
  20. package/dist/dom-utils.test.js +220 -0
  21. package/dist/index.d.ts +6 -6
  22. package/dist/index.js +17 -4
  23. package/dist/observer-utils.d.ts +40 -0
  24. package/dist/observer-utils.js +50 -0
  25. package/dist/robustness.test.d.ts +1 -0
  26. package/dist/robustness.test.js +317 -0
  27. package/dist/runeScroller.d.ts +25 -0
  28. package/dist/runeScroller.integration.test.d.ts +1 -0
  29. package/dist/runeScroller.integration.test.js +419 -0
  30. package/dist/runeScroller.js +183 -0
  31. package/dist/runeScroller.test.d.ts +1 -0
  32. package/dist/runeScroller.test.js +375 -0
  33. package/dist/types.d.ts +104 -24
  34. package/dist/types.js +58 -0
  35. package/dist/useIntersection.svelte.d.ts +7 -12
  36. package/dist/useIntersection.svelte.js +75 -54
  37. package/dist/useIntersection.test.d.ts +1 -0
  38. package/dist/useIntersection.test.js +98 -0
  39. package/package.json +19 -18
  40. package/dist/BaseAnimated.svelte +0 -48
  41. package/dist/BaseAnimated.svelte.d.ts +0 -16
  42. package/dist/RuneScroller.svelte +0 -37
  43. package/dist/RuneScroller.svelte.d.ts +0 -16
  44. package/dist/animate.svelte.d.ts +0 -14
  45. package/dist/animate.svelte.js +0 -79
  46. package/dist/dom-utils.svelte.d.ts +0 -22
  47. package/dist/dom-utils.svelte.js +0 -46
  48. package/dist/runeScroller.svelte.d.ts +0 -24
  49. package/dist/runeScroller.svelte.js +0 -79
  50. package/dist/scroll-animate.test.js +0 -57
  51. /package/dist/{scroll-animate.test.d.ts → animate.test.d.ts} +0 -0
@@ -1,75 +1,96 @@
1
- import { onMount } from 'svelte';
2
1
  /**
3
2
  * Composable for handling IntersectionObserver logic
4
3
  * Reduces duplication between animation components
5
4
  */
5
+
6
6
  /**
7
7
  * Factory function to create intersection observer composables
8
8
  * Eliminates duplication between useIntersection and useIntersectionOnce
9
- * @param options - IntersectionObserver configuration
10
- * @param onIntersect - Callback handler for intersection changes
11
- * @param once - Whether to trigger only once (default: false)
9
+ * @param {import('./types.js').IntersectionOptions} [options={}] - IntersectionObserver configuration
10
+ * @param {((entry: IntersectionObserverEntry, isVisible: boolean) => void) | undefined} onIntersect - Callback handler for intersection changes
11
+ * @param {boolean} [once=false] - Whether to trigger only once
12
+ * @returns {import('./types.js').UseIntersectionReturn}
12
13
  */
13
- function createIntersectionObserver(options = {}, onIntersect, once = false) {
14
- const { threshold = 0.5, rootMargin = '-10% 0px -10% 0px', root = null } = options;
15
- let element = $state(null);
16
- let isVisible = $state(false);
17
- let hasTriggeredOnce = false;
18
- let observer = null;
19
- onMount(() => {
20
- if (!element)
21
- return;
22
- observer = new IntersectionObserver((entries) => {
23
- entries.forEach((entry) => {
24
- // For once-only behavior, check if already triggered
25
- if (once && hasTriggeredOnce)
26
- return;
27
- isVisible = entry.isIntersecting;
28
- onIntersect(entry, entry.isIntersecting);
29
- // Unobserve after first trigger if once=true
30
- if (once && entry.isIntersecting) {
31
- hasTriggeredOnce = true;
32
- observer?.unobserve(entry.target);
33
- }
34
- });
35
- }, {
36
- threshold,
37
- rootMargin,
38
- root
39
- });
40
- observer.observe(element);
41
- return () => {
42
- observer?.disconnect();
43
- };
44
- });
45
- return {
46
- get element() {
47
- return element;
48
- },
49
- set element(value) {
50
- element = value;
51
- },
52
- get isVisible() {
53
- return isVisible;
54
- }
55
- };
14
+ function createIntersectionObserver(options = {}, onIntersect = undefined, once = false) {
15
+ const { threshold = 0.5, rootMargin = '-10% 0px -10% 0px', root = null } = options;
16
+
17
+ let element = $state(null);
18
+ let isVisible = $state(false);
19
+ let hasTriggeredOnce = false;
20
+ /** @type {IntersectionObserver | null} */
21
+ let observer = null;
22
+
23
+ $effect(() => {
24
+ if (!element) return;
25
+
26
+ observer = new IntersectionObserver(
27
+ (entries) => {
28
+ entries.forEach((entry) => {
29
+ // For once-only behavior, check if already triggered
30
+ if (once && hasTriggeredOnce) return;
31
+
32
+ isVisible = entry.isIntersecting;
33
+ if (onIntersect) {
34
+ onIntersect(entry, entry.isIntersecting);
35
+ }
36
+
37
+ // Unobserve after first trigger if once=true
38
+ if (once && entry.isIntersecting) {
39
+ hasTriggeredOnce = true;
40
+ observer?.unobserve(entry.target);
41
+ }
42
+ });
43
+ },
44
+ {
45
+ threshold,
46
+ rootMargin,
47
+ root
48
+ }
49
+ );
50
+
51
+ observer.observe(element);
52
+
53
+ return () => {
54
+ observer?.disconnect();
55
+ };
56
+ });
57
+
58
+ return {
59
+ get element() {
60
+ return element;
61
+ },
62
+ set element(value) {
63
+ element = value;
64
+ },
65
+ get isVisible() {
66
+ return isVisible;
67
+ }
68
+ };
56
69
  }
70
+
57
71
  /**
58
72
  * Track element visibility with IntersectionObserver
59
73
  * Updates isVisible whenever visibility changes
60
- * @param options - IntersectionObserver configuration
61
- * @param onVisible - Optional callback when visibility changes
74
+ * @param {import('./types.js').IntersectionOptions} [options={}] - IntersectionObserver configuration
75
+ * @param {(isVisible: boolean) => void} [onVisible] - Optional callback when visibility changes
76
+ * @returns {import('./types.js').UseIntersectionReturn}
62
77
  */
63
78
  export function useIntersection(options = {}, onVisible) {
64
- return createIntersectionObserver(options, (_entry, isVisible) => {
65
- onVisible?.(isVisible);
66
- }, false);
79
+ return createIntersectionObserver(
80
+ options,
81
+ (_entry, isVisible) => {
82
+ onVisible?.(isVisible);
83
+ },
84
+ false
85
+ );
67
86
  }
87
+
68
88
  /**
69
89
  * Track element visibility once (until first trigger)
70
90
  * Unobserves after first visibility
71
- * @param options - IntersectionObserver configuration
91
+ * @param {import('./types.js').IntersectionOptions} [options={}] - IntersectionObserver configuration
92
+ * @returns {import('./types.js').UseIntersectionReturn}
72
93
  */
73
94
  export function useIntersectionOnce(options = {}) {
74
- return createIntersectionObserver(options, () => { }, true);
95
+ return createIntersectionObserver(options, () => {}, true);
75
96
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
+
3
+ /**
4
+ * Structural tests for useIntersection and useIntersectionOnce composables
5
+ *
6
+ * Note: Full functional testing requires Svelte 5 runtime environment.
7
+ * These composables use Svelte runes ($state) and onMount hook which cannot
8
+ * be tested outside of a Svelte component context.
9
+ *
10
+ * These tests verify:
11
+ * - Module exports exist
12
+ * - File structure is valid
13
+ * - No syntax errors when imported
14
+ */
15
+ describe('useIntersection Composable', () => {
16
+ let useIntersection;
17
+ let useIntersectionOnce;
18
+
19
+ beforeEach(() => {
20
+ // Import composables
21
+ const module = require('./useIntersection.svelte.js');
22
+ useIntersection = module.useIntersection;
23
+ useIntersectionOnce = module.useIntersectionOnce;
24
+ });
25
+
26
+ describe('Module Exports', () => {
27
+ it('exports useIntersection function', () => {
28
+ expect(useIntersection).toBeDefined();
29
+ expect(typeof useIntersection).toBe('function');
30
+ });
31
+
32
+ it('exports useIntersectionOnce function', () => {
33
+ expect(useIntersectionOnce).toBeDefined();
34
+ expect(typeof useIntersectionOnce).toBe('function');
35
+ });
36
+
37
+ it('both exports are functions', () => {
38
+ expect(typeof useIntersection).toBe('function');
39
+ expect(typeof useIntersectionOnce).toBe('function');
40
+ });
41
+ });
42
+
43
+ describe('File Structure and Syntax', () => {
44
+ it('module is importable without errors', () => {
45
+ expect(() => {
46
+ require('./useIntersection.svelte.js');
47
+ }).not.toThrow();
48
+ });
49
+
50
+ it('has proper JSDoc comments', () => {
51
+ // This verifies the file has documentation
52
+ const fs = require('fs');
53
+ const { fileURLToPath } = require('url');
54
+ const { dirname } = require('path');
55
+ const __filename = fileURLToPath(import.meta.url);
56
+ const __dirname = dirname(__filename);
57
+ const filePath = `${__dirname}/useIntersection.svelte.js`;
58
+ const content = fs.readFileSync(filePath, 'utf8');
59
+ expect(content).toContain('@param');
60
+ expect(content).toContain('Composable');
61
+ });
62
+
63
+ it('uses Svelte 5 runes correctly', () => {
64
+ const fs = require('fs');
65
+ const { fileURLToPath } = require('url');
66
+ const { dirname } = require('path');
67
+ const __filename = fileURLToPath(import.meta.url);
68
+ const __dirname = dirname(__filename);
69
+ const filePath = `${__dirname}/useIntersection.svelte.js`;
70
+ const content = fs.readFileSync(filePath, 'utf8');
71
+ expect(content).toContain('$state');
72
+ expect(content).toContain('$effect');
73
+ });
74
+ });
75
+
76
+ describe('Integration Notes', () => {
77
+ it('documentation indicates Svelte environment requirement', () => {
78
+ const fs = require('fs');
79
+ const { fileURLToPath } = require('url');
80
+ const { dirname } = require('path');
81
+ const __filename = fileURLToPath(import.meta.url);
82
+ const __dirname = dirname(__filename);
83
+ const filePath = `${__dirname}/useIntersection.svelte.js`;
84
+ const content = fs.readFileSync(filePath, 'utf8');
85
+ // Should mention IntersectionObserver usage
86
+ expect(content).toContain('IntersectionObserver');
87
+ // Should mention $effect lifecycle (Svelte 5 best practice)
88
+ expect(content).toContain('$effect');
89
+ });
90
+
91
+ it('exports are accessible for import', () => {
92
+ // Verify we can destructure the exports
93
+ const { useIntersection, useIntersectionOnce } = require('./useIntersection.svelte.js');
94
+ expect(useIntersection).toBeDefined();
95
+ expect(useIntersectionOnce).toBeDefined();
96
+ });
97
+ });
98
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rune-scroller",
3
- "version": "0.1.11",
4
- "description": "Lightweight, high-performance scroll animations for Svelte 5. ~2KB bundle, zero dependencies.",
3
+ "version": "2.0.0",
4
+ "description": "Lightweight, high-performance scroll animations for Svelte 5. ~3.4KB gzipped, zero dependencies.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "license": "MIT",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "https://github.com/lelabdev/rune-scroller"
14
+ "url": "git+https://github.com/lelabdev/rune-scroller.git"
15
15
  },
16
16
  "keywords": [
17
17
  "svelte",
@@ -40,6 +40,19 @@
40
40
  "peerDependencies": {
41
41
  "svelte": "^5.0.0"
42
42
  },
43
+ "scripts": {
44
+ "dev": "vite dev",
45
+ "build": "bunx svelte-package",
46
+ "preview": "vite preview",
47
+ "prepare": "svelte-kit sync || echo ''",
48
+ "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
49
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
50
+ "format": "prettier --write .",
51
+ "lint": "prettier --check . && eslint .",
52
+ "prepublishonly": "bun run check && bun run build && bun test",
53
+ "test": "bun test"
54
+ },
55
+ "packageManager": "bun@1.3.4",
43
56
  "devDependencies": {
44
57
  "@eslint/compat": "^1.4.0",
45
58
  "@eslint/js": "^9.36.0",
@@ -51,25 +64,13 @@
51
64
  "eslint-config-prettier": "^10.1.8",
52
65
  "eslint-plugin-svelte": "^3.12.4",
53
66
  "globals": "^16.4.0",
67
+ "happy-dom": "^20.0.11",
54
68
  "prettier": "^3.6.2",
55
69
  "prettier-plugin-svelte": "^3.4.0",
56
70
  "svelte": "^5.39.5",
57
71
  "svelte-check": "^4.3.2",
58
72
  "typescript": "^5.9.2",
59
73
  "typescript-eslint": "^8.44.1",
60
- "vite": "^7.1.7",
61
- "vitest": "^3.2.4"
62
- },
63
- "scripts": {
64
- "dev": "vite dev",
65
- "build": "svelte-package && node scripts/fix-dist.js",
66
- "preview": "vite preview",
67
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
68
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
69
- "format": "prettier --write .",
70
- "lint": "prettier --check . && eslint .",
71
- "prepublishonly": "pnpm run check && pnpm run build",
72
- "test:unit": "vitest",
73
- "test": "npm run test:unit -- --run"
74
+ "vite": "^7.1.7"
74
75
  }
75
- }
76
+ }
@@ -1,48 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { useIntersection, useIntersectionOnce } from './useIntersection.svelte';
4
- import { calculateRootMargin, type AnimationType } from './animations';
5
- import './animations.css';
6
-
7
- interface Props {
8
- animation?: AnimationType;
9
- threshold?: number;
10
- rootMargin?: string;
11
- offset?: number;
12
- duration?: number;
13
- delay?: number;
14
- once?: boolean;
15
- children: Snippet;
16
- }
17
-
18
- const {
19
- animation = 'fade-in',
20
- threshold = 0.5,
21
- rootMargin,
22
- offset,
23
- duration = 800,
24
- delay = 0,
25
- once = false,
26
- children
27
- }: Props = $props();
28
-
29
- // Calculate rootMargin from offset if provided
30
- const finalRootMargin = calculateRootMargin(offset, rootMargin);
31
-
32
- // Use appropriate composable based on once prop
33
- const intersection = once
34
- ? useIntersectionOnce({ threshold, rootMargin: finalRootMargin })
35
- : useIntersection({ threshold, rootMargin: finalRootMargin });
36
- </script>
37
-
38
- <div
39
- bind:this={intersection.element}
40
- class="scroll-animate"
41
- class:is-visible={intersection.isVisible}
42
- data-animation={animation}
43
- style="--duration: {duration}ms; --delay: {delay}ms;"
44
- >
45
- {@render children()}
46
- </div>
47
-
48
- <!-- Styles are imported from animations.css -->
@@ -1,16 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import { type AnimationType } from './animations';
3
- import './animations.css';
4
- interface Props {
5
- animation?: AnimationType;
6
- threshold?: number;
7
- rootMargin?: string;
8
- offset?: number;
9
- duration?: number;
10
- delay?: number;
11
- once?: boolean;
12
- children: Snippet;
13
- }
14
- declare const BaseAnimated: import("svelte").Component<Props, {}, "">;
15
- type BaseAnimated = ReturnType<typeof BaseAnimated>;
16
- export default BaseAnimated;
@@ -1,37 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { AnimationType } from './animations';
4
- import BaseAnimated from './BaseAnimated.svelte';
5
-
6
- interface Props {
7
- animation?: AnimationType;
8
- threshold?: number;
9
- rootMargin?: string;
10
- offset?: number;
11
- duration?: number;
12
- delay?: number;
13
- repeat?: boolean;
14
- children: Snippet;
15
- [key: string]: any;
16
- }
17
-
18
- const {
19
- animation = 'fade-in',
20
- threshold = 0.5,
21
- rootMargin,
22
- offset,
23
- duration = 800,
24
- delay = 0,
25
- repeat = false,
26
- children,
27
- ...rest
28
- }: Props = $props();
29
- </script>
30
-
31
- <!--
32
- * Wrapper component for scroll animations
33
- * Triggers animation based on repeat setting:
34
- * - repeat={false} (default): triggers once when element enters viewport
35
- * - repeat={true}: triggers each time element enters viewport
36
- -->
37
- <BaseAnimated {animation} {threshold} {rootMargin} {offset} {duration} {delay} once={!repeat} {children} {...rest} />
@@ -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 RuneScroller: import("svelte").Component<Props, {}, "">;
15
- type RuneScroller = ReturnType<typeof RuneScroller>;
16
- export default RuneScroller;
@@ -1,14 +0,0 @@
1
- import type { Action } from 'svelte/action';
2
- import type { AnimateOptions } from './types';
3
- /**
4
- * Svelte action for scroll animations
5
- * Triggers animation once when element enters viewport
6
- *
7
- * @example
8
- * ```svelte
9
- * <div use:animate={{ animation: 'fade-up', duration: 1000 }}>
10
- * Content
11
- * </div>
12
- * ```
13
- */
14
- export declare const animate: Action<HTMLElement, AnimateOptions>;
@@ -1,79 +0,0 @@
1
- import { calculateRootMargin } from './animations';
2
- import { setCSSVariables, setupAnimationElement } from './dom-utils.svelte';
3
- /**
4
- * Svelte action for scroll animations
5
- * Triggers animation once when element enters viewport
6
- *
7
- * @example
8
- * ```svelte
9
- * <div use:animate={{ animation: 'fade-up', duration: 1000 }}>
10
- * Content
11
- * </div>
12
- * ```
13
- */
14
- export const animate = (node, options = {}) => {
15
- let { animation = 'fade-in', duration = 800, delay = 0, offset, threshold = 0, rootMargin } = options;
16
- // Calculate rootMargin from offset (0-100%)
17
- let finalRootMargin = calculateRootMargin(offset, rootMargin);
18
- // Setup animation with utilities
19
- setupAnimationElement(node, animation);
20
- setCSSVariables(node, duration, delay);
21
- // Track if animation has been triggered
22
- let animated = false;
23
- let observerConnected = true;
24
- // Create IntersectionObserver for one-time animation
25
- const observer = new IntersectionObserver((entries) => {
26
- entries.forEach((entry) => {
27
- // Trigger animation once when element enters viewport
28
- if (entry.isIntersecting && !animated) {
29
- node.classList.add('is-visible');
30
- animated = true;
31
- // Stop observing after animation triggers
32
- observer.unobserve(node);
33
- observerConnected = false;
34
- }
35
- });
36
- }, {
37
- threshold,
38
- rootMargin: finalRootMargin
39
- });
40
- observer.observe(node);
41
- return {
42
- update(newOptions) {
43
- const { duration: newDuration, delay: newDelay, animation: newAnimation, offset: newOffset, threshold: newThreshold, rootMargin: newRootMargin } = newOptions;
44
- // Update CSS properties
45
- if (newDuration !== undefined) {
46
- duration = newDuration;
47
- setCSSVariables(node, duration, newDelay ?? delay);
48
- }
49
- if (newDelay !== undefined && newDelay !== delay) {
50
- delay = newDelay;
51
- setCSSVariables(node, duration, delay);
52
- }
53
- if (newAnimation && newAnimation !== animation) {
54
- animation = newAnimation;
55
- node.setAttribute('data-animation', newAnimation);
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
- }
72
- },
73
- destroy() {
74
- if (observerConnected) {
75
- observer.disconnect();
76
- }
77
- }
78
- };
79
- };
@@ -1,22 +0,0 @@
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 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
20
- * @returns The created sentinel element
21
- */
22
- export declare function createSentinel(element: HTMLElement, debug?: boolean): HTMLElement;
@@ -1,46 +0,0 @@
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 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
27
- * @returns The created sentinel element
28
- */
29
- export function createSentinel(element, debug = false) {
30
- const sentinel = document.createElement('div');
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
- }
45
- return sentinel;
46
- }
@@ -1,24 +0,0 @@
1
- import type { RuneScrollerOptions } from './types';
2
- /**
3
- * Action pour animer un élément au scroll avec un sentinel invisible juste en dessous
4
- * @param element - L'élément à animer
5
- * @param options - Options d'animation (animation type, duration, et repeat)
6
- * @returns Objet action Svelte
7
- *
8
- * @example
9
- * ```svelte
10
- * <!-- Animation une seule fois -->
11
- * <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000 }}>
12
- * Content
13
- * </div>
14
- *
15
- * <!-- Animation répétée à chaque scroll -->
16
- * <div use:runeScroller={{ animation: 'fade-in-up', duration: 1000, repeat: true }}>
17
- * Content
18
- * </div>
19
- * ```
20
- */
21
- export declare function runeScroller(element: HTMLElement, options?: RuneScrollerOptions): {
22
- update(newOptions?: RuneScrollerOptions): void;
23
- destroy(): void;
24
- };