rune-scroller 0.1.10 → 1.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.
package/README.md CHANGED
@@ -37,11 +37,37 @@ yarn add rune-scroller
37
37
 
38
38
  ## 🚀 Quick Start
39
39
 
40
+ ### Step 1: Import CSS (required)
41
+
42
+ **⚠️ Important:** You must import the CSS file once in your app.
43
+
44
+ **Option A - In your root layout (recommended for SvelteKit):**
45
+
46
+ ```svelte
47
+ <!-- src/routes/+layout.svelte -->
48
+ <script>
49
+ import 'rune-scroller/animations.css';
50
+ </script>
51
+
52
+ <slot />
53
+ ```
54
+
55
+ **Option B - In each component that uses animations:**
56
+
40
57
  ```svelte
41
58
  <script>
42
59
  import runeScroller from 'rune-scroller';
43
60
  import 'rune-scroller/animations.css';
44
61
  </script>
62
+ ```
63
+
64
+ ### Step 2: Use the animations
65
+
66
+ ```svelte
67
+ <script>
68
+ import runeScroller from 'rune-scroller';
69
+ // CSS already imported in layout or above
70
+ </script>
45
71
 
46
72
  <!-- Simple animation -->
47
73
  <div use:runeScroller={{ animation: 'fade-in' }}>
@@ -92,10 +118,19 @@ interface RuneScrollerOptions {
92
118
  animation?: AnimationType; // Animation name (default: 'fade-in')
93
119
  duration?: number; // Duration in ms (default: 2000)
94
120
  repeat?: boolean; // Repeat on scroll (default: false)
95
- debug?: boolean; // Show sentinel element (default: false)
121
+ debug?: boolean; // Show sentinel as visible line (default: false)
122
+ offset?: number; // Sentinel offset in px (default: 0, negative = above)
96
123
  }
97
124
  ```
98
125
 
126
+ ### Option Details
127
+
128
+ - **`animation`** - Type of animation to play. Choose from 14 built-in animations listed above.
129
+ - **`duration`** - How long the animation lasts in milliseconds (default: 2000ms).
130
+ - **`repeat`** - If `true`, animation plays every time sentinel enters viewport. If `false`, plays only once.
131
+ - **`debug`** - If `true`, displays the sentinel element as a visible cyan line below your element. Useful for seeing exactly when animations trigger.
132
+ - **`offset`** - Offset of the sentinel in pixels. Positive values move sentinel down (delays animation), negative values move it up (triggers earlier). Useful for large elements where you want animation to trigger before the entire element is visible.
133
+
99
134
  ### Examples
100
135
 
101
136
  ```svelte
@@ -114,9 +149,35 @@ interface RuneScrollerOptions {
114
149
  Repeats every time you scroll
115
150
  </div>
116
151
 
117
- <!-- Debug mode (shows invisible sentinel) -->
152
+ <!-- Debug mode - shows cyan line marking sentinel position -->
118
153
  <div use:runeScroller={{ animation: 'fade-in', debug: true }}>
119
- You'll see a cyan line (the sentinel trigger)
154
+ The cyan line below this shows when animation will trigger
155
+ </div>
156
+
157
+ <!-- Multiple options -->
158
+ <div use:runeScroller={{
159
+ animation: 'fade-in-up',
160
+ duration: 1200,
161
+ repeat: true,
162
+ debug: true
163
+ }}>
164
+ Full featured example
165
+ </div>
166
+
167
+ <!-- Large element - trigger animation earlier with negative offset -->
168
+ <div use:runeScroller={{
169
+ animation: 'fade-in-up',
170
+ offset: -200 // Trigger 200px before element bottom
171
+ }}>
172
+ Large content that needs early triggering
173
+ </div>
174
+
175
+ <!-- Delay animation by moving sentinel down -->
176
+ <div use:runeScroller={{
177
+ animation: 'zoom-in',
178
+ offset: 300 // Trigger 300px after element bottom
179
+ }}>
180
+ Content with delayed animation
120
181
  </div>
121
182
  ```
122
183
 
@@ -194,14 +255,25 @@ Rune Scroller uses **sentinel-based triggering**:
194
255
 
195
256
  ## 🌐 SSR Compatibility
196
257
 
197
- Works seamlessly with SvelteKit:
258
+ Works seamlessly with SvelteKit. Import CSS in your root layout:
198
259
 
199
260
  ```svelte
261
+ <!-- src/routes/+layout.svelte -->
200
262
  <script>
201
- import runeScroller from 'rune-scroller';
202
263
  import 'rune-scroller/animations.css';
203
264
  </script>
204
265
 
266
+ <slot />
267
+ ```
268
+
269
+ Then use animations anywhere in your app:
270
+
271
+ ```svelte
272
+ <!-- src/routes/+page.svelte -->
273
+ <script>
274
+ import runeScroller from 'rune-scroller';
275
+ </script>
276
+
205
277
  <!-- No special handling needed -->
206
278
  <div use:runeScroller={{ animation: 'fade-in-up' }}>
207
279
  Works in SvelteKit SSR!
@@ -270,6 +342,7 @@ interface RuneScrollerOptions {
270
342
  duration?: number;
271
343
  repeat?: boolean;
272
344
  debug?: boolean;
345
+ offset?: number;
273
346
  }
274
347
 
275
348
  interface AnimateOptions {
@@ -21,6 +21,16 @@
21
21
  /* Animation states - transform-specific initial states */
22
22
  /* (opacity: 0 is already set in .scroll-animate base class) */
23
23
 
24
+ /* Fade In (no transform, just opacity) */
25
+ [data-animation='fade-in'] {
26
+ /* No transform needed, uses base opacity: 0 from .scroll-animate */
27
+ }
28
+
29
+ [data-animation='fade-in'].is-visible {
30
+ /* Inherits opacity: 1 from .scroll-animate.is-visible */
31
+ transform: none;
32
+ }
33
+
24
34
  /* Fade In Up */
25
35
  [data-animation='fade-in-up'] {
26
36
  transform: translateY(300px);
@@ -14,9 +14,10 @@ export declare function setCSSVariables(element: HTMLElement, duration?: number,
14
14
  export declare function setupAnimationElement(element: HTMLElement, animation: AnimationType): void;
15
15
  /**
16
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)
17
+ * Positioned absolutely relative to element (no layout impact)
18
+ * @param element - Reference element (used to position sentinel)
19
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)
20
21
  * @returns The created sentinel element
21
22
  */
22
- export declare function createSentinel(element: HTMLElement, debug?: boolean): HTMLElement;
23
+ export declare function createSentinel(element: HTMLElement, debug?: boolean, offset?: number): HTMLElement;
@@ -21,26 +21,28 @@ export function setupAnimationElement(element, animation) {
21
21
  }
22
22
  /**
23
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)
24
+ * Positioned absolutely relative to element (no layout impact)
25
+ * @param element - Reference element (used to position sentinel)
26
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)
27
28
  * @returns The created sentinel element
28
29
  */
29
- export function createSentinel(element, debug = false) {
30
+ export function createSentinel(element, debug = false, offset = 0) {
30
31
  const sentinel = document.createElement('div');
31
- // Get element dimensions to position sentinel at its bottom
32
+ // Get element dimensions to position sentinel at its bottom + offset
32
33
  const rect = element.getBoundingClientRect();
33
34
  const elementHeight = rect.height;
35
+ const sentinelTop = elementHeight + offset;
34
36
  if (debug) {
35
37
  // Debug mode: visible primary color line (cyan #00e0ff)
36
38
  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`;
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`;
38
40
  sentinel.setAttribute('data-sentinel-debug', 'true');
39
41
  }
40
42
  else {
41
43
  // Production: invisible positioned absolutely (no layout impact)
42
44
  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`;
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`;
44
46
  }
45
47
  return sentinel;
46
48
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { RuneScrollerOptions, AnimateOptions, IntersectionOptions, UseIntersectionReturn } from './types';
2
2
  export type { AnimationType } from './animations';
3
- export { runeScroller as default } from './runeScroller.svelte.js';
4
- export { animate } from './animate.svelte.js';
5
- export { useIntersection, useIntersectionOnce } from './useIntersection.svelte.js';
3
+ export { runeScroller as default } from './runeScroller.svelte';
4
+ export { animate } from './animate.svelte';
5
+ export { useIntersection, useIntersectionOnce } from './useIntersection.svelte';
6
6
  export { calculateRootMargin } from './animations';
@@ -24,19 +24,17 @@ export function runeScroller(element, options) {
24
24
  setupAnimationElement(element, options.animation);
25
25
  setCSSVariables(element, options.duration);
26
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);
27
34
  // Créer le sentinel invisible (ou visible si debug=true)
28
- // Sentinel positioned absolutely relative to parent (stays fixed while element animates)
29
- const sentinel = createSentinel(element, options?.debug);
30
- const parent = element.parentElement;
31
- if (parent) {
32
- // Ensure parent has position context for absolute positioning
33
- const parentPosition = window.getComputedStyle(parent).position;
34
- if (parentPosition === 'static') {
35
- parent.style.position = 'relative';
36
- }
37
- // Insert sentinel after element, positioned absolutely
38
- element.insertAdjacentElement('afterend', sentinel);
39
- }
35
+ // Sentinel positioned absolutely relative to wrapper
36
+ const sentinel = createSentinel(element, options?.debug, options?.offset);
37
+ wrapper.appendChild(sentinel);
40
38
  // Observer le sentinel avec cleanup tracking
41
39
  let observerConnected = true;
42
40
  const observer = new IntersectionObserver((entries) => {
@@ -74,6 +72,12 @@ export function runeScroller(element, options) {
74
72
  observer.disconnect();
75
73
  }
76
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();
77
81
  }
78
82
  };
79
83
  }
package/dist/types.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface RuneScrollerOptions {
12
12
  duration?: number;
13
13
  repeat?: boolean;
14
14
  debug?: boolean;
15
+ offset?: number;
15
16
  }
16
17
  /**
17
18
  * Options for the animate action
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rune-scroller",
3
- "version": "0.1.10",
3
+ "version": "1.0.0",
4
4
  "description": "Lightweight, high-performance scroll animations for Svelte 5. ~2KB bundle, zero dependencies.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -40,6 +40,19 @@
40
40
  "peerDependencies": {
41
41
  "svelte": "^5.0.0"
42
42
  },
43
+ "scripts": {
44
+ "dev": "vite dev",
45
+ "build": "svelte-package && node scripts/fix-dist.js",
46
+ "preview": "vite preview",
47
+ "prepare": "svelte-kit sync || echo ''",
48
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
49
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
50
+ "format": "prettier --write .",
51
+ "lint": "prettier --check . && eslint .",
52
+ "prepublishonly": "pnpm run check && pnpm run build",
53
+ "test:unit": "vitest",
54
+ "test": "npm run test:unit -- --run"
55
+ },
43
56
  "devDependencies": {
44
57
  "@eslint/compat": "^1.4.0",
45
58
  "@eslint/js": "^9.36.0",
@@ -59,17 +72,5 @@
59
72
  "typescript-eslint": "^8.44.1",
60
73
  "vite": "^7.1.7",
61
74
  "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
75
  }
75
- }
76
+ }