rune-scroller 1.0.0 → 2.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 +137 -59
- package/dist/animations.css +42 -139
- package/dist/animations.d.ts +12 -9
- package/dist/animations.js +31 -6
- package/dist/dom-utils.d.ts +29 -0
- package/dist/dom-utils.js +97 -0
- package/dist/index.d.ts +5 -6
- package/dist/index.js +17 -5
- package/dist/observer-utils.d.ts +21 -0
- package/dist/observer-utils.js +31 -0
- package/dist/runeScroller.d.ts +9 -0
- package/dist/runeScroller.js +166 -0
- package/dist/types.d.ts +62 -29
- package/dist/types.js +43 -0
- package/dist/useIntersection.svelte.d.ts +7 -16
- package/dist/useIntersection.svelte.js +75 -60
- package/package.json +14 -12
- package/dist/BaseAnimated.svelte +0 -48
- package/dist/BaseAnimated.svelte.d.ts +0 -16
- package/dist/RuneScroller.svelte +0 -37
- package/dist/RuneScroller.svelte.d.ts +0 -16
- package/dist/animate.svelte.d.ts +0 -14
- package/dist/animate.svelte.js +0 -79
- package/dist/animations.test.d.ts +0 -1
- package/dist/animations.test.js +0 -43
- package/dist/dom-utils.svelte.d.ts +0 -23
- package/dist/dom-utils.svelte.js +0 -48
- package/dist/runeScroller.svelte.d.ts +0 -24
- package/dist/runeScroller.svelte.js +0 -83
- package/dist/scroll-animate.test.d.ts +0 -1
- package/dist/scroll-animate.test.js +0 -57
|
@@ -1,75 +1,90 @@
|
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @param
|
|
10
|
-
* @
|
|
11
|
-
* @param once - Whether to trigger only once (default: false)
|
|
7
|
+
* @param {import('./types.js').IntersectionOptions} [options={}]
|
|
8
|
+
* @param {((entry: IntersectionObserverEntry, isVisible: boolean) => void) | undefined} onIntersect
|
|
9
|
+
* @param {boolean} [once=false]
|
|
10
|
+
* @returns {import('./types.js').UseIntersectionReturn}
|
|
12
11
|
*/
|
|
13
|
-
function createIntersectionObserver(options = {}, onIntersect, once = false) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
12
|
+
function createIntersectionObserver(options = {}, onIntersect = undefined, once = false) {
|
|
13
|
+
const { threshold = 0.5, rootMargin = '-10% 0px -10% 0px', root = null } = options;
|
|
14
|
+
|
|
15
|
+
let element = $state(null);
|
|
16
|
+
let isVisible = $state(false);
|
|
17
|
+
let hasTriggeredOnce = false;
|
|
18
|
+
/** @type {IntersectionObserver | null} */
|
|
19
|
+
let observer = null;
|
|
20
|
+
|
|
21
|
+
$effect(() => {
|
|
22
|
+
if (!element) return;
|
|
23
|
+
|
|
24
|
+
observer = new IntersectionObserver(
|
|
25
|
+
(entries) => {
|
|
26
|
+
entries.forEach((entry) => {
|
|
27
|
+
// For once-only behavior, check if already triggered
|
|
28
|
+
if (once && hasTriggeredOnce) return;
|
|
29
|
+
|
|
30
|
+
isVisible = entry.isIntersecting;
|
|
31
|
+
if (onIntersect) {
|
|
32
|
+
onIntersect(entry, entry.isIntersecting);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Unobserve after first trigger if once=true
|
|
36
|
+
if (once && entry.isIntersecting) {
|
|
37
|
+
hasTriggeredOnce = true;
|
|
38
|
+
observer?.unobserve(entry.target);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
threshold,
|
|
44
|
+
rootMargin,
|
|
45
|
+
root
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
observer.observe(element);
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
observer?.disconnect();
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
get element() {
|
|
58
|
+
return element;
|
|
59
|
+
},
|
|
60
|
+
set element(value) {
|
|
61
|
+
element = value;
|
|
62
|
+
},
|
|
63
|
+
get isVisible() {
|
|
64
|
+
return isVisible;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
56
67
|
}
|
|
68
|
+
|
|
57
69
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* @
|
|
61
|
-
* @param onVisible - Optional callback when visibility changes
|
|
70
|
+
* @param {import('./types.js').IntersectionOptions} [options={}]
|
|
71
|
+
* @param {(isVisible: boolean) => void} [onVisible]
|
|
72
|
+
* @returns {import('./types.js').UseIntersectionReturn}
|
|
62
73
|
*/
|
|
63
74
|
export function useIntersection(options = {}, onVisible) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
return createIntersectionObserver(
|
|
76
|
+
options,
|
|
77
|
+
(_entry, isVisible) => {
|
|
78
|
+
onVisible?.(isVisible);
|
|
79
|
+
},
|
|
80
|
+
false
|
|
81
|
+
);
|
|
67
82
|
}
|
|
83
|
+
|
|
68
84
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* @param options - IntersectionObserver configuration
|
|
85
|
+
* @param {import('./types.js').IntersectionOptions} [options={}]
|
|
86
|
+
* @returns {import('./types.js').UseIntersectionReturn}
|
|
72
87
|
*/
|
|
73
88
|
export function useIntersectionOnce(options = {}) {
|
|
74
|
-
|
|
89
|
+
return createIntersectionObserver(options, () => {}, true);
|
|
75
90
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rune-scroller",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Lightweight, high-performance scroll animations for Svelte 5.
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Lightweight, high-performance scroll animations for Svelte 5. 12.7KB 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",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"./animations.css": "./dist/animations.css"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
|
-
"dist"
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
36
38
|
],
|
|
37
39
|
"main": "./dist/index.js",
|
|
38
40
|
"svelte": "./dist/index.js",
|
|
@@ -42,17 +44,17 @@
|
|
|
42
44
|
},
|
|
43
45
|
"scripts": {
|
|
44
46
|
"dev": "vite dev",
|
|
45
|
-
"build": "svelte-package
|
|
47
|
+
"build": "bunx svelte-package",
|
|
46
48
|
"preview": "vite preview",
|
|
47
49
|
"prepare": "svelte-kit sync || echo ''",
|
|
48
|
-
"check": "svelte-kit sync && svelte-check --tsconfig ./
|
|
49
|
-
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./
|
|
50
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
|
51
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
|
50
52
|
"format": "prettier --write .",
|
|
51
53
|
"lint": "prettier --check . && eslint .",
|
|
52
|
-
"prepublishonly": "
|
|
53
|
-
"test
|
|
54
|
-
"test": "npm run test:unit -- --run"
|
|
54
|
+
"prepublishonly": "bun run check && bun run build && bun test",
|
|
55
|
+
"test": "bun test"
|
|
55
56
|
},
|
|
57
|
+
"packageManager": "bun@1.3.4",
|
|
56
58
|
"devDependencies": {
|
|
57
59
|
"@eslint/compat": "^1.4.0",
|
|
58
60
|
"@eslint/js": "^9.36.0",
|
|
@@ -64,13 +66,13 @@
|
|
|
64
66
|
"eslint-config-prettier": "^10.1.8",
|
|
65
67
|
"eslint-plugin-svelte": "^3.12.4",
|
|
66
68
|
"globals": "^16.4.0",
|
|
69
|
+
"happy-dom": "^20.0.11",
|
|
67
70
|
"prettier": "^3.6.2",
|
|
68
71
|
"prettier-plugin-svelte": "^3.4.0",
|
|
69
72
|
"svelte": "^5.39.5",
|
|
70
73
|
"svelte-check": "^4.3.2",
|
|
71
74
|
"typescript": "^5.9.2",
|
|
72
75
|
"typescript-eslint": "^8.44.1",
|
|
73
|
-
"vite": "^7.1.7"
|
|
74
|
-
"vitest": "^3.2.4"
|
|
76
|
+
"vite": "^7.1.7"
|
|
75
77
|
}
|
|
76
78
|
}
|
package/dist/BaseAnimated.svelte
DELETED
|
@@ -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;
|
package/dist/RuneScroller.svelte
DELETED
|
@@ -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;
|
package/dist/animate.svelte.d.ts
DELETED
|
@@ -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>;
|
package/dist/animate.svelte.js
DELETED
|
@@ -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 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/animations.test.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { calculateRootMargin } from './animations';
|
|
3
|
-
describe('calculateRootMargin', () => {
|
|
4
|
-
it('calculates rootMargin from offset (0-100%)', () => {
|
|
5
|
-
expect(calculateRootMargin(0)).toBe('-100% 0px -0% 0px');
|
|
6
|
-
expect(calculateRootMargin(50)).toBe('-50% 0px -50% 0px');
|
|
7
|
-
expect(calculateRootMargin(100)).toBe('-0% 0px -100% 0px');
|
|
8
|
-
});
|
|
9
|
-
it('uses custom rootMargin when provided', () => {
|
|
10
|
-
const custom = '-10% 0px -10% 0px';
|
|
11
|
-
expect(calculateRootMargin(50, custom)).toBe(custom);
|
|
12
|
-
});
|
|
13
|
-
it('uses default rootMargin when nothing provided', () => {
|
|
14
|
-
expect(calculateRootMargin()).toBe('-10% 0px -10% 0px');
|
|
15
|
-
});
|
|
16
|
-
it('prioritizes custom rootMargin over offset', () => {
|
|
17
|
-
const custom = '-20% 0px';
|
|
18
|
-
expect(calculateRootMargin(50, custom)).toBe(custom);
|
|
19
|
-
expect(calculateRootMargin(0, custom)).toBe(custom);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
describe('AnimationType', () => {
|
|
23
|
-
it('includes all expected animation types', () => {
|
|
24
|
-
const validAnimations = [
|
|
25
|
-
'fade-in',
|
|
26
|
-
'fade-in-up',
|
|
27
|
-
'fade-in-down',
|
|
28
|
-
'fade-in-left',
|
|
29
|
-
'fade-in-right',
|
|
30
|
-
'zoom-in',
|
|
31
|
-
'zoom-out',
|
|
32
|
-
'zoom-in-up',
|
|
33
|
-
'zoom-in-left',
|
|
34
|
-
'zoom-in-right',
|
|
35
|
-
'flip',
|
|
36
|
-
'flip-x',
|
|
37
|
-
'slide-rotate',
|
|
38
|
-
'bounce-in'
|
|
39
|
-
];
|
|
40
|
-
// If this compiles, all types are valid
|
|
41
|
-
expect(validAnimations.length).toBe(14);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -1,23 +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 relative to element (no layout impact)
|
|
18
|
-
* @param element - Reference element (used to position sentinel)
|
|
19
|
-
* @param debug - If true, shows the sentinel as a visible line for debugging
|
|
20
|
-
* @param offset - Offset in pixels from element bottom (default: 0, negative = above element)
|
|
21
|
-
* @returns The created sentinel element
|
|
22
|
-
*/
|
|
23
|
-
export declare function createSentinel(element: HTMLElement, debug?: boolean, offset?: number): HTMLElement;
|
package/dist/dom-utils.svelte.js
DELETED
|
@@ -1,48 +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 relative to element (no layout impact)
|
|
25
|
-
* @param element - Reference element (used to position sentinel)
|
|
26
|
-
* @param debug - If true, shows the sentinel as a visible line for debugging
|
|
27
|
-
* @param offset - Offset in pixels from element bottom (default: 0, negative = above element)
|
|
28
|
-
* @returns The created sentinel element
|
|
29
|
-
*/
|
|
30
|
-
export function createSentinel(element, debug = false, offset = 0) {
|
|
31
|
-
const sentinel = document.createElement('div');
|
|
32
|
-
// Get element dimensions to position sentinel at its bottom + offset
|
|
33
|
-
const rect = element.getBoundingClientRect();
|
|
34
|
-
const elementHeight = rect.height;
|
|
35
|
-
const sentinelTop = elementHeight + offset;
|
|
36
|
-
if (debug) {
|
|
37
|
-
// Debug mode: visible primary color line (cyan #00e0ff)
|
|
38
|
-
sentinel.style.cssText =
|
|
39
|
-
`position:absolute;top:${sentinelTop}px;left:0;right:0;height:3px;background:#00e0ff;margin:0;padding:0;box-sizing:border-box;z-index:999;pointer-events:none`;
|
|
40
|
-
sentinel.setAttribute('data-sentinel-debug', 'true');
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
// Production: invisible positioned absolutely (no layout impact)
|
|
44
|
-
sentinel.style.cssText =
|
|
45
|
-
`position:absolute;top:${sentinelTop}px;left:0;right:0;height:1px;visibility:hidden;margin:0;padding:0;box-sizing:border-box;pointer-events:none`;
|
|
46
|
-
}
|
|
47
|
-
return sentinel;
|
|
48
|
-
}
|
|
@@ -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
|
-
};
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { setCSSVariables, setupAnimationElement, createSentinel } from './dom-utils.svelte';
|
|
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 function runeScroller(element, options) {
|
|
22
|
-
// Setup animation classes et variables CSS
|
|
23
|
-
if (options?.animation || options?.duration) {
|
|
24
|
-
setupAnimationElement(element, options.animation);
|
|
25
|
-
setCSSVariables(element, options.duration);
|
|
26
|
-
}
|
|
27
|
-
// Créer un wrapper div autour de l'élément pour le sentinel en position absolute
|
|
28
|
-
// Ceci évite de casser le flex/grid flow du parent
|
|
29
|
-
const wrapper = document.createElement('div');
|
|
30
|
-
wrapper.style.cssText = 'position:relative;display:contents';
|
|
31
|
-
// Insérer le wrapper avant l'élément
|
|
32
|
-
element.insertAdjacentElement('beforebegin', wrapper);
|
|
33
|
-
wrapper.appendChild(element);
|
|
34
|
-
// Créer le sentinel invisible (ou visible si debug=true)
|
|
35
|
-
// Sentinel positioned absolutely relative to wrapper
|
|
36
|
-
const sentinel = createSentinel(element, options?.debug, options?.offset);
|
|
37
|
-
wrapper.appendChild(sentinel);
|
|
38
|
-
// Observer le sentinel avec cleanup tracking
|
|
39
|
-
let observerConnected = true;
|
|
40
|
-
const observer = new IntersectionObserver((entries) => {
|
|
41
|
-
const isIntersecting = entries[0].isIntersecting;
|
|
42
|
-
if (isIntersecting) {
|
|
43
|
-
// Ajouter la classe is-visible à l'élément
|
|
44
|
-
element.classList.add('is-visible');
|
|
45
|
-
// Déconnecter si pas en mode repeat
|
|
46
|
-
if (!options?.repeat) {
|
|
47
|
-
observer.disconnect();
|
|
48
|
-
observerConnected = false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
else if (options?.repeat) {
|
|
52
|
-
// En mode repeat, retirer la classe quand le sentinel sort
|
|
53
|
-
element.classList.remove('is-visible');
|
|
54
|
-
}
|
|
55
|
-
}, { threshold: 0 });
|
|
56
|
-
observer.observe(sentinel);
|
|
57
|
-
return {
|
|
58
|
-
update(newOptions) {
|
|
59
|
-
if (newOptions?.animation) {
|
|
60
|
-
element.setAttribute('data-animation', newOptions.animation);
|
|
61
|
-
}
|
|
62
|
-
if (newOptions?.duration) {
|
|
63
|
-
setCSSVariables(element, newOptions.duration);
|
|
64
|
-
}
|
|
65
|
-
// Update repeat option
|
|
66
|
-
if (newOptions?.repeat !== undefined && newOptions.repeat !== options?.repeat) {
|
|
67
|
-
options = { ...options, repeat: newOptions.repeat };
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
destroy() {
|
|
71
|
-
if (observerConnected) {
|
|
72
|
-
observer.disconnect();
|
|
73
|
-
}
|
|
74
|
-
sentinel.remove();
|
|
75
|
-
// Unwrap element (move it out of wrapper)
|
|
76
|
-
const parent = wrapper.parentElement;
|
|
77
|
-
if (parent) {
|
|
78
|
-
wrapper.insertAdjacentElement('beforebegin', element);
|
|
79
|
-
}
|
|
80
|
-
wrapper.remove();
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|