From a5d0e29551c0e43e37b088a5493bba4d38053985 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 12 Jan 2026 20:42:31 +0100 Subject: [PATCH 01/42] Refactor ScrollAnimation exports - Add ScrollAnimationTimeline (new name for ScrollAnimationParent) - Add ScrollAnimationTarget (new name for ScrollAnimationChild) - Deprecate ScrollAnimation, ScrollAnimationWithEase, ScrollAnimationChild, ScrollAnimationChildWithEase, ScrollAnimationParent and animationScrollWithEase - Add tests for the new components Fixes #441 Co-authored-by: Claude --- CLAUDE.md | 5 + .../ScrollAnimationTarget.spec.ts | 53 +++++++++ .../ScrollAnimationTimeline.spec.ts | 93 +++++++++++++++ packages/tests/ScrollAnimation/index.spec.ts | 28 +++-- .../ui/ScrollAnimation/ScrollAnimation.ts | 15 +++ .../ScrollAnimation/ScrollAnimationChild.ts | 16 ++- .../ScrollAnimationChildWithEase.ts | 17 ++- .../ScrollAnimation/ScrollAnimationParent.ts | 15 +++ .../ScrollAnimation/ScrollAnimationTarget.ts | 107 ++++++++++++++++++ .../ScrollAnimationTimeline.ts | 48 ++++++++ .../ScrollAnimationWithEase.ts | 17 ++- .../animationScrollWithEase.ts | 16 ++- packages/ui/ScrollAnimation/index.ts | 4 + 13 files changed, 423 insertions(+), 11 deletions(-) create mode 100644 packages/tests/ScrollAnimation/ScrollAnimationTarget.spec.ts create mode 100644 packages/tests/ScrollAnimation/ScrollAnimationTimeline.spec.ts create mode 100644 packages/ui/ScrollAnimation/ScrollAnimationTarget.ts create mode 100644 packages/ui/ScrollAnimation/ScrollAnimationTimeline.ts diff --git a/CLAUDE.md b/CLAUDE.md index 89d9d949..5916beb6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,10 @@ # @studiometa/ui packages +## Commit messages + +- Use English for commit messages +- Use simple verb-first sentences (e.g., "Add...", "Fix...", "Refactor...") + ## Project structure - Monorepo managed by NPM with packages in the `./packages` folder diff --git a/packages/tests/ScrollAnimation/ScrollAnimationTarget.spec.ts b/packages/tests/ScrollAnimation/ScrollAnimationTarget.spec.ts new file mode 100644 index 00000000..1c851c64 --- /dev/null +++ b/packages/tests/ScrollAnimation/ScrollAnimationTarget.spec.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { ScrollAnimationTarget } from '@studiometa/ui'; +import { h, mount, destroy } from '#test-utils'; + +describe('ScrollAnimationTarget', () => { + let element: HTMLDivElement; + let animation: ScrollAnimationTarget; + + beforeEach(async () => { + element = h('div'); + animation = new ScrollAnimationTarget(element); + await mount(animation); + }); + + afterEach(async () => { + await destroy(animation); + }); + + it('should have the correct config', () => { + expect(ScrollAnimationTarget.config.name).toBe('ScrollAnimationTarget'); + expect(ScrollAnimationTarget.config.options.dampFactor.default).toBe(0.1); + expect(ScrollAnimationTarget.config.options.dampPrecision.default).toBe(0.001); + }); + + it('should initialize with correct default damped values', () => { + expect(animation.dampedCurrent).toEqual({ x: 0, y: 0 }); + expect(animation.dampedProgress).toEqual({ x: 0, y: 0 }); + }); + + it('should have damping options accessible', () => { + expect(animation.$options.dampFactor).toBe(0.1); + expect(animation.$options.dampPrecision).toBe(0.001); + }); + + it('should override scrolledInView method', () => { + const mockProps = { + current: { x: 0.5, y: 0.8 }, + dampedCurrent: { x: 0.4, y: 0.7 }, + start: { x: 0, y: 0 }, + end: { x: 1, y: 1 }, + dampedProgress: { x: 0.4, y: 0.7 }, + progress: { x: 0.5, y: 0.8 }, + }; + + expect(() => animation.scrolledInView(mockProps)).not.toThrow(); + }); + + it('should inherit from AbstractScrollAnimation', () => { + expect(animation.render).toBeDefined(); + expect(animation.target).toBe(element); + expect(animation.playRange).toEqual([0, 1]); + }); +}); diff --git a/packages/tests/ScrollAnimation/ScrollAnimationTimeline.spec.ts b/packages/tests/ScrollAnimation/ScrollAnimationTimeline.spec.ts new file mode 100644 index 00000000..924d5003 --- /dev/null +++ b/packages/tests/ScrollAnimation/ScrollAnimationTimeline.spec.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; +import { + h, + mockIsIntersecting, + intersectionObserverBeforeAllCallback, + intersectionObserverAfterEachCallback, +} from '#test-utils'; + +describe('ScrollAnimationTimeline', () => { + let parentElement: HTMLDivElement; + let childElement1: HTMLDivElement; + let childElement2: HTMLDivElement; + let parent: ScrollAnimationTimeline; + + beforeAll(() => { + intersectionObserverBeforeAllCallback(); + }); + + afterEach(() => { + intersectionObserverAfterEachCallback(); + }); + + beforeEach(async () => { + parentElement = h('div'); + childElement1 = h('div', { 'data-component': 'ScrollAnimationTarget' }); + childElement2 = h('div', { 'data-component': 'ScrollAnimationTarget' }); + + parentElement.appendChild(childElement1); + parentElement.appendChild(childElement2); + + parent = new ScrollAnimationTimeline(parentElement); + await mockIsIntersecting(parentElement, true); + }); + + afterEach(async () => { + await mockIsIntersecting(parentElement, false); + }); + + it('should have the correct config', () => { + expect(ScrollAnimationTimeline.config.name).toBe('ScrollAnimationTimeline'); + expect(ScrollAnimationTimeline.config.components.ScrollAnimationTarget).toBe(ScrollAnimationTarget); + }); + + it('should have ScrollAnimationTarget components', () => { + expect(parent.$children.ScrollAnimationTarget).toHaveLength(2); + expect(parent.$children.ScrollAnimationTarget[0]).toBeInstanceOf(ScrollAnimationTarget); + expect(parent.$children.ScrollAnimationTarget[1]).toBeInstanceOf(ScrollAnimationTarget); + }); + + it('should propagate scrolledInView to all children', () => { + const child1Spy = vi.spyOn(parent.$children.ScrollAnimationTarget[0], 'scrolledInView'); + const child2Spy = vi.spyOn(parent.$children.ScrollAnimationTarget[1], 'scrolledInView'); + + const mockProps = { + current: { x: 0.5, y: 0.8 }, + dampedCurrent: { x: 0.4, y: 0.7 }, + start: { x: 0, y: 0 }, + end: { x: 1, y: 1 }, + dampedProgress: { x: 0.4, y: 0.7 }, + progress: { x: 0.5, y: 0.8 }, + }; + + parent.scrolledInView(mockProps); + + expect(child1Spy).toHaveBeenCalledWith(mockProps); + expect(child2Spy).toHaveBeenCalledWith(mockProps); + }); + + it('should work with no children', async () => { + const emptyParent = new ScrollAnimationTimeline(h('div')); + await mockIsIntersecting(emptyParent.$el, true); + + expect(emptyParent.$children.ScrollAnimationTarget).toHaveLength(0); + + const mockProps = { + current: { x: 0.5, y: 0.8 }, + dampedCurrent: { x: 0.4, y: 0.7 }, + start: { x: 0, y: 0 }, + end: { x: 1, y: 1 }, + dampedProgress: { x: 0.4, y: 0.7 }, + progress: { x: 0.5, y: 0.8 }, + }; + + expect(() => emptyParent.scrolledInView(mockProps)).not.toThrow(); + + await mockIsIntersecting(emptyParent.$el, false); + }); + + it('should be extended from withScrolledInView(Base)', () => { + expect(parent.scrolledInView).toBeDefined(); + }); +}); diff --git a/packages/tests/ScrollAnimation/index.spec.ts b/packages/tests/ScrollAnimation/index.spec.ts index 9271a828..f9659a72 100644 --- a/packages/tests/ScrollAnimation/index.spec.ts +++ b/packages/tests/ScrollAnimation/index.spec.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; import { AbstractScrollAnimation, + ScrollAnimationTimeline, + ScrollAnimationTarget, + // Deprecated exports ScrollAnimation, ScrollAnimationChild, ScrollAnimationChildWithEase, @@ -15,33 +18,44 @@ describe('ScrollAnimation exports', () => { expect(AbstractScrollAnimation.config.name).toBe('AbstractScrollAnimation'); }); - it('should export ScrollAnimation', () => { + it('should export ScrollAnimationTimeline', () => { + expect(ScrollAnimationTimeline).toBeDefined(); + expect(ScrollAnimationTimeline.config.name).toBe('ScrollAnimationTimeline'); + }); + + it('should export ScrollAnimationTarget', () => { + expect(ScrollAnimationTarget).toBeDefined(); + expect(ScrollAnimationTarget.config.name).toBe('ScrollAnimationTarget'); + }); + + // Deprecated exports - kept for backward compatibility + it('should export ScrollAnimation (deprecated)', () => { expect(ScrollAnimation).toBeDefined(); expect(ScrollAnimation.config.name).toBe('ScrollAnimation'); }); - it('should export ScrollAnimationChild', () => { + it('should export ScrollAnimationChild (deprecated)', () => { expect(ScrollAnimationChild).toBeDefined(); expect(ScrollAnimationChild.config.name).toBe('AbstractScrollAnimation'); }); - it('should export ScrollAnimationChildWithEase', () => { + it('should export ScrollAnimationChildWithEase (deprecated)', () => { expect(ScrollAnimationChildWithEase).toBeDefined(); expect(ScrollAnimationChildWithEase.config.name).toBe('ScrollAnimationChildWithEase'); }); - it('should export ScrollAnimationParent', () => { + it('should export ScrollAnimationParent (deprecated)', () => { expect(ScrollAnimationParent).toBeDefined(); expect(ScrollAnimationParent.config.name).toBe('ScrollAnimationParent'); }); - it('should export ScrollAnimationWithEase', () => { + it('should export ScrollAnimationWithEase (deprecated)', () => { expect(ScrollAnimationWithEase).toBeDefined(); expect(ScrollAnimationWithEase.config.name).toBe('ScrollAnimationWithEase'); }); - it('should export animationScrollWithEase decorator', () => { + it('should export animationScrollWithEase decorator (deprecated)', () => { expect(animationScrollWithEase).toBeDefined(); expect(typeof animationScrollWithEase).toBe('function'); }); -}); \ No newline at end of file +}); diff --git a/packages/ui/ScrollAnimation/ScrollAnimation.ts b/packages/ui/ScrollAnimation/ScrollAnimation.ts index 36a45413..80d01a24 100644 --- a/packages/ui/ScrollAnimation/ScrollAnimation.ts +++ b/packages/ui/ScrollAnimation/ScrollAnimation.ts @@ -1,5 +1,6 @@ import { withScrolledInView } from '@studiometa/js-toolkit'; import type { BaseConfig, BaseProps } from '@studiometa/js-toolkit'; +import { isDev } from '@studiometa/js-toolkit/utils'; import { AbstractScrollAnimation } from './AbstractScrollAnimation.js'; export interface ScrollAnimationProps extends BaseProps { @@ -10,6 +11,8 @@ export interface ScrollAnimationProps extends BaseProps { /** * ScrollAnimation class. + * + * @deprecated Use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children instead. * @link https://ui.studiometa.dev/components/ScrollAnimation/ */ export class ScrollAnimation< @@ -32,4 +35,16 @@ export class ScrollAnimation< get target(): HTMLElement { return this.$refs.target; } + + /** + * Display deprecation warning. + */ + mounted() { + if (isDev) { + console.warn( + `The ${this.$options.name} component is deprecated.`, + '\nUse `ScrollAnimationTimeline` with `ScrollAnimationTarget` children instead.', + ); + } + } } diff --git a/packages/ui/ScrollAnimation/ScrollAnimationChild.ts b/packages/ui/ScrollAnimation/ScrollAnimationChild.ts index 40f9547f..ed217012 100644 --- a/packages/ui/ScrollAnimation/ScrollAnimationChild.ts +++ b/packages/ui/ScrollAnimation/ScrollAnimationChild.ts @@ -4,7 +4,7 @@ import type { ScrollInViewProps, WithScrolledInViewProps, } from '@studiometa/js-toolkit'; -import { damp, clamp01, domScheduler } from '@studiometa/js-toolkit/utils'; +import { damp, clamp01, domScheduler, isDev } from '@studiometa/js-toolkit/utils'; import { AbstractScrollAnimation } from './AbstractScrollAnimation.js'; export interface ScrollAnimationChildProps extends BaseProps { @@ -32,6 +32,8 @@ function updateProps( /** * ScrollAnimationChild class. + * + * @deprecated Use `ScrollAnimationTarget` instead. */ export class ScrollAnimationChild extends AbstractScrollAnimation< T & ScrollAnimationChildProps @@ -71,6 +73,18 @@ export class ScrollAnimationChild extends Abstr y: 0, }; + /** + * Display deprecation warning. + */ + mounted() { + if (isDev) { + console.warn( + `The ${this.$options.name} component is deprecated.`, + '\nUse `ScrollAnimationTarget` instead.', + ); + } + } + /** * Compute local damped progress. */ diff --git a/packages/ui/ScrollAnimation/ScrollAnimationChildWithEase.ts b/packages/ui/ScrollAnimation/ScrollAnimationChildWithEase.ts index 39170b59..00c17ecb 100644 --- a/packages/ui/ScrollAnimation/ScrollAnimationChildWithEase.ts +++ b/packages/ui/ScrollAnimation/ScrollAnimationChildWithEase.ts @@ -1,9 +1,12 @@ import type { BaseConfig } from '@studiometa/js-toolkit'; +import { isDev } from '@studiometa/js-toolkit/utils'; import { ScrollAnimationChild } from './ScrollAnimationChild.js'; import { animationScrollWithEase } from './animationScrollWithEase.js'; /** - * ScrollAnimationChild class. + * ScrollAnimationChildWithEase class. + * + * @deprecated Use `ScrollAnimationTarget` instead. */ export class ScrollAnimationChildWithEase extends animationScrollWithEase(ScrollAnimationChild) { /** @@ -13,4 +16,16 @@ export class ScrollAnimationChildWithEase extends animationScrollWithEase(Scroll ...ScrollAnimationChild.config, name: 'ScrollAnimationChildWithEase', }; + + /** + * Display deprecation warning. + */ + mounted() { + if (isDev) { + console.warn( + `The ${this.$options.name} component is deprecated.`, + '\nUse `ScrollAnimationTarget` instead.', + ); + } + } } diff --git a/packages/ui/ScrollAnimation/ScrollAnimationParent.ts b/packages/ui/ScrollAnimation/ScrollAnimationParent.ts index a9885d10..d7cb3fef 100644 --- a/packages/ui/ScrollAnimation/ScrollAnimationParent.ts +++ b/packages/ui/ScrollAnimation/ScrollAnimationParent.ts @@ -1,5 +1,6 @@ import { Base, ScrollInViewProps, withScrolledInView } from '@studiometa/js-toolkit'; import type { BaseConfig, BaseProps } from '@studiometa/js-toolkit'; +import { isDev } from '@studiometa/js-toolkit/utils'; import { ScrollAnimationChild } from './ScrollAnimationChild.js'; export interface ScrollAnimationParentProps extends BaseProps { @@ -10,6 +11,8 @@ export interface ScrollAnimationParentProps extends BaseProps { /** * ScrollAnimationParent class. + * + * @deprecated Use `ScrollAnimationTimeline` instead. */ export class ScrollAnimationParent extends withScrolledInView( Base, @@ -25,6 +28,18 @@ export class ScrollAnimationParent extends with }, }; + /** + * Display deprecation warning. + */ + mounted() { + if (isDev) { + console.warn( + `The ${this.$options.name} component is deprecated.`, + '\nUse `ScrollAnimationTimeline` instead.', + ); + } + } + /** * Scrolled in view hook. */ diff --git a/packages/ui/ScrollAnimation/ScrollAnimationTarget.ts b/packages/ui/ScrollAnimation/ScrollAnimationTarget.ts new file mode 100644 index 00000000..63e1b91d --- /dev/null +++ b/packages/ui/ScrollAnimation/ScrollAnimationTarget.ts @@ -0,0 +1,107 @@ +import type { + BaseConfig, + BaseProps, + ScrollInViewProps, + WithScrolledInViewProps, +} from '@studiometa/js-toolkit'; +import { damp, clamp01, domScheduler } from '@studiometa/js-toolkit/utils'; +import { AbstractScrollAnimation } from './AbstractScrollAnimation.js'; + +export interface ScrollAnimationTargetProps extends BaseProps { + $options: WithScrolledInViewProps['$options']; +} + +function updateProps( + // eslint-disable-next-line no-use-before-define + that: ScrollAnimationTarget, + props: ScrollInViewProps, + dampFactor: number, + dampPrecision: number, + axis: 'x' | 'y' = 'x', +) { + that.dampedCurrent[axis] = damp( + props.current[axis], + that.dampedCurrent[axis], + dampFactor, + dampPrecision, + ); + that.dampedProgress[axis] = clamp01( + (that.dampedCurrent[axis] - props.start[axis]) / (props.end[axis] - props.start[axis]), + ); +} + +/** + * ScrollAnimationTarget class. + * + * A component that animates based on scroll progress from a parent `ScrollAnimationTimeline`. + * Each target can have its own animation keyframes and play range. + * + * @example + * ```html + *
+ *
+ * Animated content + *
+ *
+ * ``` + */ +export class ScrollAnimationTarget extends AbstractScrollAnimation< + T & ScrollAnimationTargetProps +> { + /** + * Config. + */ + static config: BaseConfig = { + ...AbstractScrollAnimation.config, + name: 'ScrollAnimationTarget', + options: { + ...AbstractScrollAnimation.config.options, + dampFactor: { + type: Number, + default: 0.1, + }, + dampPrecision: { + type: Number, + default: 0.001, + }, + }, + }; + + /** + * Local damped current values. + */ + dampedCurrent: ScrollInViewProps['dampedCurrent'] = { + x: 0, + y: 0, + }; + + /** + * Local damped progress. + */ + dampedProgress: ScrollInViewProps['dampedCurrent'] = { + x: 0, + y: 0, + }; + + /** + * Compute local damped progress. + */ + scrolledInView(props: ScrollInViewProps) { + domScheduler.read(() => { + const { dampFactor, dampPrecision } = this.$options; + updateProps(this, props, dampFactor, dampPrecision, 'x'); + updateProps(this, props, dampFactor, dampPrecision, 'y'); + props.dampedCurrent = this.dampedCurrent; + props.dampedProgress = this.dampedProgress; + }); + + domScheduler.write(() => { + super.scrolledInView(props); + }); + } +} diff --git a/packages/ui/ScrollAnimation/ScrollAnimationTimeline.ts b/packages/ui/ScrollAnimation/ScrollAnimationTimeline.ts new file mode 100644 index 00000000..bfbc017f --- /dev/null +++ b/packages/ui/ScrollAnimation/ScrollAnimationTimeline.ts @@ -0,0 +1,48 @@ +import { Base, ScrollInViewProps, withScrolledInView } from '@studiometa/js-toolkit'; +import type { BaseConfig, BaseProps } from '@studiometa/js-toolkit'; +import { ScrollAnimationTarget } from './ScrollAnimationTarget.js'; + +export interface ScrollAnimationTimelineProps extends BaseProps { + $children: { + ScrollAnimationTarget: ScrollAnimationTarget[]; + }; +} + +/** + * ScrollAnimationTimeline class. + * + * A component that manages scroll-based animations for its children. + * Use with `ScrollAnimationTarget` children components. + * + * @example + * ```html + *
+ *
+ * Content + *
+ *
+ * ``` + */ +export class ScrollAnimationTimeline extends withScrolledInView( + Base, + {}, +) { + /** + * Config. + */ + static config: BaseConfig = { + name: 'ScrollAnimationTimeline', + components: { + ScrollAnimationTarget, + }, + }; + + /** + * Scrolled in view hook. + */ + scrolledInView(props: ScrollInViewProps) { + for (const child of this.$children.ScrollAnimationTarget) { + child.scrolledInView(props); + } + } +} diff --git a/packages/ui/ScrollAnimation/ScrollAnimationWithEase.ts b/packages/ui/ScrollAnimation/ScrollAnimationWithEase.ts index 076672c3..98b5d62b 100644 --- a/packages/ui/ScrollAnimation/ScrollAnimationWithEase.ts +++ b/packages/ui/ScrollAnimation/ScrollAnimationWithEase.ts @@ -1,9 +1,12 @@ import { type BaseConfig } from '@studiometa/js-toolkit'; +import { isDev } from '@studiometa/js-toolkit/utils'; import { ScrollAnimation } from './ScrollAnimation.js'; import { animationScrollWithEase } from './animationScrollWithEase.js'; /** - * ScrollAnimation class. + * ScrollAnimationWithEase class. + * + * @deprecated Use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children instead. */ export class ScrollAnimationWithEase extends animationScrollWithEase(ScrollAnimation) { /** @@ -13,4 +16,16 @@ export class ScrollAnimationWithEase extends animationScrollWithEase(ScrollAnima ...ScrollAnimation.config, name: 'ScrollAnimationWithEase', }; + + /** + * Display deprecation warning. + */ + mounted() { + if (isDev) { + console.warn( + `The ${this.$options.name} component is deprecated.`, + '\nUse `ScrollAnimationTimeline` with `ScrollAnimationTarget` children instead.', + ); + } + } } diff --git a/packages/ui/ScrollAnimation/animationScrollWithEase.ts b/packages/ui/ScrollAnimation/animationScrollWithEase.ts index ba1749c8..3463ca87 100644 --- a/packages/ui/ScrollAnimation/animationScrollWithEase.ts +++ b/packages/ui/ScrollAnimation/animationScrollWithEase.ts @@ -1,5 +1,5 @@ import type { BaseConfig, BaseProps, BaseDecorator, BaseInterface } from '@studiometa/js-toolkit'; -import { ease } from '@studiometa/js-toolkit/utils'; +import { ease, isDev } from '@studiometa/js-toolkit/utils'; import type { AbstractScrollAnimation } from './AbstractScrollAnimation.js'; const regex = /ease([A-Z])/; @@ -20,6 +20,8 @@ export interface AnimationScrollWithEaseInterface extends BaseInterface {} /** * Extend a `ScrollAnimation` component to use easings. + * + * @deprecated This decorator is deprecated. Easing can be applied directly via CSS or animation options. */ export function animationScrollWithEase( ScrollAnimation: typeof AbstractScrollAnimation, @@ -40,6 +42,18 @@ export function animationScrollWithEase( }, }; + /** + * Display a deprecation warning. + */ + mounted() { + if (isDev) { + console.warn( + `The animationScrollWithEase decorator is deprecated.`, + '\nEasing can be applied directly via CSS or animation options.', + ); + } + } + /** * Eases the progress value. */ diff --git a/packages/ui/ScrollAnimation/index.ts b/packages/ui/ScrollAnimation/index.ts index ac1f90bd..4507e87a 100644 --- a/packages/ui/ScrollAnimation/index.ts +++ b/packages/ui/ScrollAnimation/index.ts @@ -1,4 +1,8 @@ export * from './AbstractScrollAnimation.js'; +export * from './ScrollAnimationTimeline.js'; +export * from './ScrollAnimationTarget.js'; + +// Deprecated exports export * from './animationScrollWithEase.js'; export * from './ScrollAnimation.js'; export * from './ScrollAnimationWithEase.js'; From 9b9688bb0ce898da84b45df9d04faef0f56f4c9d Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 12 Jan 2026 21:00:04 +0100 Subject: [PATCH 02/42] Update ScrollAnimation documentation and changelog - Add documentation for ScrollAnimationTimeline and ScrollAnimationTarget - Update examples to use new component names - Add deprecation notices for old components - Update changelog with PR reference Co-authored-by: Claude Sonnet 4.5 --- CHANGELOG.md | 4 + .../components/ScrollAnimation/examples.md | 10 +- .../docs/components/ScrollAnimation/index.md | 60 +++++++++- .../docs/components/ScrollAnimation/js-api.md | 106 ++++++++++++++++++ .../stories/parallax-parent/app.js | 20 ++-- .../stories/parallax-parent/app.twig | 4 +- .../ScrollAnimation/stories/parent/app.js | 16 +-- .../ScrollAnimation/stories/parent/app.twig | 16 +-- 8 files changed, 198 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c494a761..55c022d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- **ScrollAnimation:** refactor exports ([#494](https://github.com/studiometa/ui/pull/494), [a5d0e29](https://github.com/studiometa/ui/commit/a5d0e29)) + ## [v1.7.0](https://github.com/studiometa/ui/compare/1.6.0..1.7.0) (2025-11-11) ### Added diff --git a/packages/docs/components/ScrollAnimation/examples.md b/packages/docs/components/ScrollAnimation/examples.md index 2690f8ef..4673ae7a 100644 --- a/packages/docs/components/ScrollAnimation/examples.md +++ b/packages/docs/components/ScrollAnimation/examples.md @@ -25,7 +25,9 @@ title: ScrollAnimation examples -## Parent driven animation +## Timeline driven animation + +Coordinate multiple animations using `ScrollAnimationTimeline` with `ScrollAnimationTarget` children. -## Parallax with a parent +## Parallax with a timeline -It might be sometimes interesting to use the parent ↔ child logic of the `ScrollAnimation` component to improve performance, as only the parent progression in the viewport is watched. +It might be sometimes interesting to use the timeline ↔ target logic of the `ScrollAnimation` component to improve performance, as only the timeline progression in the viewport is watched. -The resulting effect is different as each child animation is driven by the parent one, but it is still interesting. +The resulting effect is different as each target animation is driven by the timeline, but it is still interesting. @@ -43,6 +43,49 @@ export default createApp(App, document.body); ``` +## Timeline usage + +For more complex animations with multiple targets, use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children: + +```js{2,3,9,10} +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline } from '@studiometa/ui'; +import { ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App, document.body); +``` + +```html +
+
+
First element
+
+
+
Second element
+
+
+``` + ## Features - **Scroll-driven**: Animations progress based on scroll position @@ -51,5 +94,18 @@ export default createApp(App, document.body); - **Easing Control**: Customize animation timing with cubic-bezier easing - **Play Range**: Control when animation starts and ends during scroll - **Performance**: Optimized with intersection observer and RAF +- **Timeline Support**: Coordinate multiple animations with `ScrollAnimationTimeline` and `ScrollAnimationTarget` - **Variants**: Multiple specialized classes for different use cases +## Deprecated components + +:::warning Deprecated +The following components are deprecated and will be removed in a future version. Use `ScrollAnimationTimeline` and `ScrollAnimationTarget` instead: + +- `ScrollAnimationParent` → use `ScrollAnimationTimeline` +- `ScrollAnimationChild` → use `ScrollAnimationTarget` +- `ScrollAnimationChildWithEase` → use `ScrollAnimationTarget` +- `ScrollAnimationWithEase` → use `ScrollAnimation` +- `animationScrollWithEase` → no replacement +::: + diff --git a/packages/docs/components/ScrollAnimation/js-api.md b/packages/docs/components/ScrollAnimation/js-api.md index 1036697e..6be1e8ef 100644 --- a/packages/docs/components/ScrollAnimation/js-api.md +++ b/packages/docs/components/ScrollAnimation/js-api.md @@ -230,3 +230,109 @@ The element being animated (either the `target` ref or the component's root elem - Type: `Animation` The animation instance created from the keyframes and easing options. See [`animate` documentation](https://js-toolkit.studiometa.dev/utils/css/animate.html). + +--- + +## ScrollAnimationTimeline + +A parent component that manages scroll-based animations for its children `ScrollAnimationTarget` components. + +### Usage + +```js +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App, document.body); +``` + +```html +
+
+
Content
+
+
+``` + +### Children Components + +#### `ScrollAnimationTarget` + +- Type: `ScrollAnimationTarget[]` + +Array of child animation targets that will be animated based on the scroll progress of the timeline. + +--- + +## ScrollAnimationTarget + +A component that animates based on scroll progress from a parent `ScrollAnimationTimeline`. Each target can have its own animation keyframes and play range. + +### Usage + +```html +
+
+
First element
+
+
+
Second element
+
+
+``` + +### Options + +`ScrollAnimationTarget` inherits all options from `ScrollAnimation` and adds: + +#### `dampFactor` + +- Type: `number` +- Default: `0.1` + +Damping factor for smooth scroll animations. Lower values create smoother, slower animations. + +#### `dampPrecision` + +- Type: `number` +- Default: `0.001` + +Precision threshold for damping calculations. Lower values increase precision but may impact performance. + +### Properties + +#### `dampedCurrent` + +- Type: `{ x: number, y: number }` + +Current damped scroll position values for both axes. + +#### `dampedProgress` + +- Type: `{ x: number, y: number }` + +Current damped progress values (0-1) for both axes. diff --git a/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.js b/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.js index acbca604..995e4295 100644 --- a/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.js +++ b/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.js @@ -1,10 +1,10 @@ import { Base, createApp } from '@studiometa/js-toolkit'; -import { Figure, ScrollAnimationChild, ScrollAnimationParent } from '@studiometa/ui'; +import { Figure, ScrollAnimationTarget, ScrollAnimationTimeline } from '@studiometa/ui'; -class ParallaxChild extends ScrollAnimationChild { +class ParallaxTarget extends ScrollAnimationTarget { static config = { - ...ScrollAnimationChild.config, - name: 'ParallaxChild', + ...ScrollAnimationTarget.config, + name: 'ParallaxTarget', components: { Figure, }, @@ -15,12 +15,12 @@ class ParallaxChild extends ScrollAnimationChild { } } -class ParallaxParent extends ScrollAnimationParent { +class ParallaxTimeline extends ScrollAnimationTimeline { static config = { - ...ScrollAnimationParent.config, - name: 'ParallaxParent', + ...ScrollAnimationTimeline.config, + name: 'ParallaxTimeline', components: { - ParallaxChild, + ParallaxTarget, }, }; @@ -29,7 +29,7 @@ class ParallaxParent extends ScrollAnimationParent { } scrolledInView(props) { - this.$children.ParallaxChild.forEach((child) => { + this.$children.ParallaxTarget.forEach((child) => { child.scrolledInView(props); }); } @@ -39,7 +39,7 @@ class App extends Base { static config = { name: 'App', components: { - ParallaxParent, + ParallaxTimeline, }, }; } diff --git a/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.twig b/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.twig index 797ec736..1fe6ff61 100644 --- a/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.twig +++ b/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.twig @@ -40,11 +40,11 @@ } ] %} -
+
{% include '@ui/ImageGrid/ImageGrid.twig' with { images: images, image_attr: { - data_component: 'ParallaxChild', + data_component: 'ParallaxTarget', data_option_from: { y: [-20, '%'] }, data_option_to: { y: [20, '%'] }, class: 'h-fit overflow-hidden', diff --git a/packages/docs/components/ScrollAnimation/stories/parent/app.js b/packages/docs/components/ScrollAnimation/stories/parent/app.js index 1e1abeec..c38f8b1b 100644 --- a/packages/docs/components/ScrollAnimation/stories/parent/app.js +++ b/packages/docs/components/ScrollAnimation/stories/parent/app.js @@ -1,23 +1,15 @@ import { Base, createApp } from '@studiometa/js-toolkit'; import { - ScrollAnimationParent as ScrollAnimationParentCore, - ScrollAnimationChild, + ScrollAnimationTimeline, + ScrollAnimationTarget, } from '@studiometa/ui'; -class ScrollAnimationParent extends ScrollAnimationParentCore { - static config = { - name: 'ScrollAnimationParent', - components: { - ScrollAnimationChild, - }, - }; -} - class App extends Base { static config = { name: 'App', components: { - ScrollAnimationParent, + ScrollAnimationTimeline, + ScrollAnimationTarget, }, }; } diff --git a/packages/docs/components/ScrollAnimation/stories/parent/app.twig b/packages/docs/components/ScrollAnimation/stories/parent/app.twig index f14fa81a..6e90a7e3 100644 --- a/packages/docs/components/ScrollAnimation/stories/parent/app.twig +++ b/packages/docs/components/ScrollAnimation/stories/parent/app.twig @@ -7,10 +7,10 @@
Scroll down
-
-
-
-
-
-
@@ -63,13 +63,13 @@ Lorem ipsum dolor sit amet

-
-
Date: Mon, 12 Jan 2026 21:43:18 +0100 Subject: [PATCH 03/42] Fix shared state mutation in ScrollAnimationTarget damping Co-authored-by: Claude --- packages/ui/ScrollAnimation/ScrollAnimationChild.ts | 8 +++++--- packages/ui/ScrollAnimation/ScrollAnimationTarget.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/ui/ScrollAnimation/ScrollAnimationChild.ts b/packages/ui/ScrollAnimation/ScrollAnimationChild.ts index ed217012..7aa2ed49 100644 --- a/packages/ui/ScrollAnimation/ScrollAnimationChild.ts +++ b/packages/ui/ScrollAnimation/ScrollAnimationChild.ts @@ -93,12 +93,14 @@ export class ScrollAnimationChild extends Abstr const { dampFactor, dampPrecision } = this.$options; updateProps(this, props, dampFactor, dampPrecision, 'x'); updateProps(this, props, dampFactor, dampPrecision, 'y'); - props.dampedCurrent = this.dampedCurrent; - props.dampedProgress = this.dampedProgress; }); domScheduler.write(() => { - super.scrolledInView(props); + super.scrolledInView({ + ...props, + dampedCurrent: this.dampedCurrent, + dampedProgress: this.dampedProgress, + }); }); } } diff --git a/packages/ui/ScrollAnimation/ScrollAnimationTarget.ts b/packages/ui/ScrollAnimation/ScrollAnimationTarget.ts index 63e1b91d..15d8837e 100644 --- a/packages/ui/ScrollAnimation/ScrollAnimationTarget.ts +++ b/packages/ui/ScrollAnimation/ScrollAnimationTarget.ts @@ -96,12 +96,14 @@ export class ScrollAnimationTarget extends Abst const { dampFactor, dampPrecision } = this.$options; updateProps(this, props, dampFactor, dampPrecision, 'x'); updateProps(this, props, dampFactor, dampPrecision, 'y'); - props.dampedCurrent = this.dampedCurrent; - props.dampedProgress = this.dampedProgress; }); domScheduler.write(() => { - super.scrolledInView(props); + super.scrolledInView({ + ...props, + dampedCurrent: this.dampedCurrent, + dampedProgress: this.dampedProgress, + }); }); } } From 1e3c6be79c716bce4aae538a97bb8cd0e3fd0b2e Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 12 Jan 2026 21:43:20 +0100 Subject: [PATCH 04/42] Add test case for independent damping in ScrollAnimationTimeline Co-authored-by: Claude --- .../ScrollAnimationTimeline.spec.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/tests/ScrollAnimation/ScrollAnimationTimeline.spec.ts b/packages/tests/ScrollAnimation/ScrollAnimationTimeline.spec.ts index 924d5003..bbbb9a49 100644 --- a/packages/tests/ScrollAnimation/ScrollAnimationTimeline.spec.ts +++ b/packages/tests/ScrollAnimation/ScrollAnimationTimeline.spec.ts @@ -1,7 +1,9 @@ import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest'; import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; +import { domScheduler } from '@studiometa/js-toolkit/utils'; import { h, + destroy, mockIsIntersecting, intersectionObserverBeforeAllCallback, intersectionObserverAfterEachCallback, @@ -90,4 +92,48 @@ describe('ScrollAnimationTimeline', () => { it('should be extended from withScrolledInView(Base)', () => { expect(parent.scrolledInView).toBeDefined(); }); + + it('should not share dampedProgress between children', async () => { + parentElement = h('div'); + childElement1 = h('div', { + 'data-component': 'ScrollAnimationTarget', + 'data-option-damp-factor': '0.1', + }); + childElement2 = h('div', { + 'data-component': 'ScrollAnimationTarget', + 'data-option-damp-factor': '1', + }); + + parentElement.appendChild(childElement1); + parentElement.appendChild(childElement2); + + const timeline = new ScrollAnimationTimeline(parentElement); + await mockIsIntersecting(parentElement, true); + + const child1 = timeline.$children.ScrollAnimationTarget[0]; + const child2 = timeline.$children.ScrollAnimationTarget[1]; + + const child1RenderSpy = vi.spyOn(child1, 'render'); + const child2RenderSpy = vi.spyOn(child2, 'render'); + + const mockProps = { + current: { x: 0, y: 100 }, + start: { x: 0, y: 0 }, + end: { x: 0, y: 1000 }, + progress: { x: 0, y: 0.1 }, + dampedCurrent: { x: 0, y: 0 }, + dampedProgress: { x: 0, y: 0 }, + }; + + timeline.scrolledInView(mockProps as any); + + // Wait for domScheduler + await new Promise((resolve) => domScheduler.read(() => domScheduler.write(resolve))); + + expect(child1RenderSpy).toHaveBeenCalledWith(0.01); + expect(child2RenderSpy).toHaveBeenCalledWith(0.1); + + await mockIsIntersecting(parentElement, false); + await destroy(timeline); + }); }); From f925170c7c41fad4d14d1a0c5e43364df4b2b137 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 12 Jan 2026 21:43:22 +0100 Subject: [PATCH 05/42] Update ScrollAnimation documentation to promote Timeline API Co-authored-by: Claude --- .../docs/components/ScrollAnimation/index.md | 70 +++++++++---------- .../docs/components/ScrollAnimation/js-api.md | 10 +-- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/docs/components/ScrollAnimation/index.md b/packages/docs/components/ScrollAnimation/index.md index 338695f8..f0ebe68a 100644 --- a/packages/docs/components/ScrollAnimation/index.md +++ b/packages/docs/components/ScrollAnimation/index.md @@ -13,17 +13,18 @@ The `ScrollAnimation` component creates scroll-driven animations that respond to ## Usage -Once the [package installed](/guide/installation/), simply include the component in your project: +For the best performance and flexibility, use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children: -```js{2,8} +```js{2,3,9,10} import { Base, createApp } from '@studiometa/js-toolkit'; -import { ScrollAnimation } from '@studiometa/ui'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; class App extends Base { static config = { name: 'App', components: { - ScrollAnimation, + ScrollAnimationTimeline, + ScrollAnimationTarget, }, }; } @@ -32,32 +33,39 @@ export default createApp(App, document.body); ``` ```html -
-
- Content to animate +
+
+ First element +
+
+ Second element
``` -## Timeline usage +## Legacy usage -For more complex animations with multiple targets, use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children: +The `ScrollAnimation` component can be used standalone for simple use cases, but it is **deprecated** in favor of the timeline API. -```js{2,3,9,10} +```js{2,8} import { Base, createApp } from '@studiometa/js-toolkit'; -import { ScrollAnimationTimeline } from '@studiometa/ui'; -import { ScrollAnimationTarget } from '@studiometa/ui'; +import { ScrollAnimation } from '@studiometa/ui'; class App extends Base { static config = { name: 'App', components: { - ScrollAnimationTimeline, - ScrollAnimationTarget, + ScrollAnimation, }, }; } @@ -66,22 +74,13 @@ export default createApp(App, document.body); ``` ```html -
-
-
First element
-
-
-
Second element
+
+
+ Content to animate
``` @@ -102,10 +101,11 @@ export default createApp(App, document.body); :::warning Deprecated The following components are deprecated and will be removed in a future version. Use `ScrollAnimationTimeline` and `ScrollAnimationTarget` instead: +- `ScrollAnimation` → use `ScrollAnimationTimeline` and `ScrollAnimationTarget` - `ScrollAnimationParent` → use `ScrollAnimationTimeline` - `ScrollAnimationChild` → use `ScrollAnimationTarget` - `ScrollAnimationChildWithEase` → use `ScrollAnimationTarget` -- `ScrollAnimationWithEase` → use `ScrollAnimation` +- `ScrollAnimationWithEase` → use `ScrollAnimationTimeline` and `ScrollAnimationTarget` - `animationScrollWithEase` → no replacement ::: diff --git a/packages/docs/components/ScrollAnimation/js-api.md b/packages/docs/components/ScrollAnimation/js-api.md index 6be1e8ef..7c1cb289 100644 --- a/packages/docs/components/ScrollAnimation/js-api.md +++ b/packages/docs/components/ScrollAnimation/js-api.md @@ -223,7 +223,7 @@ Manually render the animation at a specific progress value. - Type: `HTMLElement` -The element being animated (either the `target` ref or the component's root element). +The element being animated. For `ScrollAnimationTarget`, it defaults to the component's root element (`$el`). For the legacy `ScrollAnimation`, it uses the `target` ref if provided. ### `animation` @@ -263,7 +263,7 @@ export default createApp(App, document.body); data-option-from='{"opacity": 0}' data-option-to='{"opacity": 1}' > -
Content
+ Content
``` @@ -292,7 +292,7 @@ A component that animates based on scroll progress from a parent `ScrollAnimatio data-option-to='{"opacity": 1, "translateY": "0px"}' data-option-play-range='[0, 0.5]' > -
First element
+ First element
-
Second element
+ Second element
``` ### Options -`ScrollAnimationTarget` inherits all options from `ScrollAnimation` and adds: +`ScrollAnimationTarget` inherits all options from the base animation class and adds: #### `dampFactor` From 4081dd92a3f99ac9ac8298a1f5034a895a3b70bf Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 11:46:35 +0100 Subject: [PATCH 06/42] Add deprecation notes to ScrollAnimation tests and update exports snapshot --- .../ScrollAnimation/ScrollAnimation.spec.ts | 2 +- .../ScrollAnimationChild.spec.ts | 2 +- .../ScrollAnimationChildWithEase.spec.ts | 53 +++++++++++++++++++ .../ScrollAnimationParent.spec.ts | 2 +- packages/tests/index.spec.ts | 2 + packages/tests/vitest.config.ts | 2 +- 6 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 packages/tests/ScrollAnimation/ScrollAnimationChildWithEase.spec.ts diff --git a/packages/tests/ScrollAnimation/ScrollAnimation.spec.ts b/packages/tests/ScrollAnimation/ScrollAnimation.spec.ts index 0f7d22de..df8652b0 100644 --- a/packages/tests/ScrollAnimation/ScrollAnimation.spec.ts +++ b/packages/tests/ScrollAnimation/ScrollAnimation.spec.ts @@ -7,7 +7,7 @@ import { intersectionObserverAfterEachCallback, } from '#test-utils'; -describe('ScrollAnimation', () => { +describe('ScrollAnimation (deprecated)', () => { let element: HTMLDivElement; let targetElement: HTMLDivElement; let animation: ScrollAnimation; diff --git a/packages/tests/ScrollAnimation/ScrollAnimationChild.spec.ts b/packages/tests/ScrollAnimation/ScrollAnimationChild.spec.ts index ff08fb6c..82078703 100644 --- a/packages/tests/ScrollAnimation/ScrollAnimationChild.spec.ts +++ b/packages/tests/ScrollAnimation/ScrollAnimationChild.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { ScrollAnimationChild } from '@studiometa/ui'; import { h, mount, destroy } from '#test-utils'; -describe('ScrollAnimationChild', () => { +describe('ScrollAnimationChild (deprecated)', () => { let element: HTMLDivElement; let animation: ScrollAnimationChild; diff --git a/packages/tests/ScrollAnimation/ScrollAnimationChildWithEase.spec.ts b/packages/tests/ScrollAnimation/ScrollAnimationChildWithEase.spec.ts new file mode 100644 index 00000000..78b2e465 --- /dev/null +++ b/packages/tests/ScrollAnimation/ScrollAnimationChildWithEase.spec.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { ScrollAnimationChildWithEase } from '@studiometa/ui'; +import { h, mount, destroy } from '#test-utils'; + +describe('ScrollAnimationChildWithEase (deprecated)', () => { + let element: HTMLDivElement; + let animation: ScrollAnimationChildWithEase; + + beforeEach(async () => { + element = h('div'); + animation = new ScrollAnimationChildWithEase(element); + await mount(animation); + }); + + afterEach(async () => { + await destroy(animation); + }); + + it('should have the correct config', () => { + expect(ScrollAnimationChildWithEase.config.name).toBe('ScrollAnimationChildWithEase'); + expect(ScrollAnimationChildWithEase.config.options.dampFactor.default).toBe(0.1); + expect(ScrollAnimationChildWithEase.config.options.dampPrecision.default).toBe(0.001); + }); + + it('should initialize with correct default damped values', () => { + expect(animation.dampedCurrent).toEqual({ x: 0, y: 0 }); + expect(animation.dampedProgress).toEqual({ x: 0, y: 0 }); + }); + + it('should have damping options accessible', () => { + expect(animation.$options.dampFactor).toBe(0.1); + expect(animation.$options.dampPrecision).toBe(0.001); + }); + + it('should override scrolledInView method', () => { + const mockProps = { + current: { x: 0.5, y: 0.8 }, + dampedCurrent: { x: 0.4, y: 0.7 }, + start: { x: 0, y: 0 }, + end: { x: 1, y: 1 }, + dampedProgress: { x: 0.4, y: 0.7 }, + progress: { x: 0.5, y: 0.8 }, + }; + + expect(() => animation.scrolledInView(mockProps)).not.toThrow(); + }); + + it('should inherit from AbstractScrollAnimation', () => { + expect(animation.render).toBeDefined(); + expect(animation.target).toBe(element); + expect(animation.playRange).toEqual([0, 1]); + }); +}); diff --git a/packages/tests/ScrollAnimation/ScrollAnimationParent.spec.ts b/packages/tests/ScrollAnimation/ScrollAnimationParent.spec.ts index 60553096..b8d7df61 100644 --- a/packages/tests/ScrollAnimation/ScrollAnimationParent.spec.ts +++ b/packages/tests/ScrollAnimation/ScrollAnimationParent.spec.ts @@ -7,7 +7,7 @@ import { intersectionObserverAfterEachCallback, } from '#test-utils'; -describe('ScrollAnimationParent', () => { +describe('ScrollAnimationParent (deprecated)', () => { let parentElement: HTMLDivElement; let childElement1: HTMLDivElement; let childElement2: HTMLDivElement; diff --git a/packages/tests/index.spec.ts b/packages/tests/index.spec.ts index 2f426fb0..a86a11a6 100644 --- a/packages/tests/index.spec.ts +++ b/packages/tests/index.spec.ts @@ -49,6 +49,8 @@ test('components exports', () => { "ScrollAnimationChild", "ScrollAnimationChildWithEase", "ScrollAnimationParent", + "ScrollAnimationTarget", + "ScrollAnimationTimeline", "ScrollAnimationWithEase", "ScrollReveal", "Sentinel", diff --git a/packages/tests/vitest.config.ts b/packages/tests/vitest.config.ts index 85200df6..c3a3d4d0 100644 --- a/packages/tests/vitest.config.ts +++ b/packages/tests/vitest.config.ts @@ -14,6 +14,6 @@ export default defineConfig({ include: ['ui/**/*.ts'], exclude: ['**/tests/**/*.ts', '**/ui/**/index.ts'], }, - exclude: ['**/.symfony/vendor/**'], + exclude: ['**/.symfony/vendor/**', '**/api/vendor/**'], }, }); From 82bba35baea3e03f525b782c1c32728a7cc55fc2 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 13:55:22 +0100 Subject: [PATCH 07/42] Fix line highlights in ScrollAnimation documentation --- packages/docs/components/ScrollAnimation/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/components/ScrollAnimation/index.md b/packages/docs/components/ScrollAnimation/index.md index f0ebe68a..60ffa4ce 100644 --- a/packages/docs/components/ScrollAnimation/index.md +++ b/packages/docs/components/ScrollAnimation/index.md @@ -15,7 +15,7 @@ The `ScrollAnimation` component creates scroll-driven animations that respond to For the best performance and flexibility, use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children: -```js{2,3,9,10} +```js{2,8-9} import { Base, createApp } from '@studiometa/js-toolkit'; import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; From 56f3bec619f6dd9a78a5bb0195713e90f6f263ab Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 14:09:09 +0100 Subject: [PATCH 08/42] Improve ScrollAnimation JS API documentation structure - Move ScrollAnimationTimeline to the top as the recommended API - Add clear separation between Timeline and Target components - Document all options under ScrollAnimationTarget section - Move deprecated ScrollAnimation to the bottom with warning - Update all code examples to use the new Timeline/Target pattern --- .../docs/components/ScrollAnimation/js-api.md | 344 +++++++++++------- 1 file changed, 203 insertions(+), 141 deletions(-) diff --git a/packages/docs/components/ScrollAnimation/js-api.md b/packages/docs/components/ScrollAnimation/js-api.md index 7c1cb289..13d79dd1 100644 --- a/packages/docs/components/ScrollAnimation/js-api.md +++ b/packages/docs/components/ScrollAnimation/js-api.md @@ -4,37 +4,103 @@ title: ScrollAnimation JS API # JS API -## Refs +## ScrollAnimationTimeline -### `target` +A parent component that manages scroll-based animations for its children `ScrollAnimationTarget` components. The timeline watches for its position in the viewport and propagates the scroll progress to all its children. -HTMLElement reference for the element to animate. If not provided, the component's root element will be used. +### Usage -## Options +```js +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} -### `playRange` +export default createApp(App, document.body); +``` -- Type: `[number, number] | [number, number, number]` -- Default: `[0, 1]` +```html +
+
+ Content +
+
+``` -Define the scroll progress range when the animation should play. Values between 0 and 1, where 0 is when the element enters the viewport and 1 is when it exits. +### Children Components -This can be useful to create timelines where elements should be animated sequentially. +#### `ScrollAnimationTarget` -```html {3,8} -
-
...
+- Type: `ScrollAnimationTarget[]` + +Array of child animation targets that will be animated based on the scroll progress of the timeline. + +--- + +## ScrollAnimationTarget + +A component that animates based on scroll progress from a parent `ScrollAnimationTimeline`. Each target can have its own animation keyframes, play range, and damping settings. + +### Usage + +```html +
+
+ First element +
+
+ Second element +
-
-
...
+``` + +### Options + +#### `playRange` + +- Type: `[number, number] | [number, number, number]` +- Default: `[0, 1]` + +Define the scroll progress range when the animation should play. Values between 0 and 1, where 0 is when the timeline enters the viewport and 1 is when it exits. + +```html {4,11} +
+
+ First (animates from 0% to 50% scroll) +
+
+ Second (animates from 50% to 100% scroll) +
``` -#### Staggered animation +##### Staggered animation Staggered scroll animation can be created by giving 3 numbers to the `playRange` option: @@ -42,16 +108,18 @@ Staggered scroll animation can be created by giving 3 numbers to the `playRange` - `length`: the length of the staggered items - `step`: the delay to apply between each item in the staggered list -```html {3,8} -
-
...
-
-
-
...
+```html {4,10} +
+
+ ... +
+
+ ... +
``` @@ -74,7 +142,7 @@ The following example uses Twig `loop.index0` and `loop.length` variables to gen -#### Sequentially ordered animation +##### Sequentially ordered animation Use the `[index, length, step]` format for the `data-option-play-range` attribute value with a `step` value set to `1 / length` to make each animation play sequentially. @@ -95,37 +163,41 @@ Use the `[index, length, step]` format for the `data-option-play-range` attribut -### `from` +#### `from` - Type: `object` - Default: `{}` Initial keyframe for the animation. Define CSS properties as key-value pairs. -```html {3} -
-
...
+```html {4} +
+
+ ... +
``` -### `to` +#### `to` - Type: `object` - Default: `{}` Final keyframe for the animation. Define CSS properties as key-value pairs. -```html {3} -
-
...
+```html {4} +
+
+ ... +
``` -### `keyframes` +#### `keyframes` - Type: `keyFrame[]` - Default: `[]` @@ -133,22 +205,21 @@ Final keyframe for the animation. Define CSS properties as key-value pairs. Array of keyframes for complex animations. When provided, `from` and `to` are ignored. ```html -
+
...
``` -See the following types defintions and [the `animate` documentation](https://js-toolkit.studiometa.dev/utils/css/animate.html) for more advanced documentation on keyframes. +See the following types definitions and [the `animate` documentation](https://js-toolkit.studiometa.dev/utils/css/animate.html) for more advanced documentation on keyframes. ```ts twoslash import { TransformProps } from '@studiometa/js-toolkit/utils'; @@ -166,18 +237,24 @@ interface KeyFrame extends TransformProps { } ``` -### `easing` +#### `easing` - Type: `[number, number, number, number]` - Default: `[0, 0, 1, 1]` Cubic-bezier easing values for the animation timing. -```html -
...
+```html {4} +
+
+ ... +
+
``` -#### Common easing values +##### Common easing values | Easing | Cubic bezier | | ---------- | ----------------------------- | @@ -207,9 +284,40 @@ Cubic-bezier easing values for the animation timing. | InOutQuint | `[0.86, 0, 0.07, 1]` | | InOutSine | `[0.445, 0.05, 0.55, 0.95]` | -## Methods +#### `dampFactor` + +- Type: `number` +- Default: `0.1` + +Damping factor for smooth scroll animations. Lower values create smoother, slower animations. Each `ScrollAnimationTarget` can have its own damping factor, allowing for different animation speeds within the same timeline. + +```html {4,9} +
+
+ Slow and smooth +
+
+ Fast and snappy +
+
+``` + +#### `dampPrecision` + +- Type: `number` +- Default: `0.001` -### `render(progress)` +Precision threshold for damping calculations. Lower values increase precision but may impact performance. + +### Methods + +#### `render(progress)` - Parameters: - `progress` (`number`): animation progress between 0 and 1 @@ -217,122 +325,76 @@ Cubic-bezier easing values for the animation timing. Manually render the animation at a specific progress value. -## Properties +### Properties -### `target` +#### `target` - Type: `HTMLElement` -The element being animated. For `ScrollAnimationTarget`, it defaults to the component's root element (`$el`). For the legacy `ScrollAnimation`, it uses the `target` ref if provided. +The element being animated. Defaults to the component's root element (`$el`). -### `animation` +#### `animation` - Type: `Animation` The animation instance created from the keyframes and easing options. See [`animate` documentation](https://js-toolkit.studiometa.dev/utils/css/animate.html). ---- - -## ScrollAnimationTimeline - -A parent component that manages scroll-based animations for its children `ScrollAnimationTarget` components. - -### Usage +#### `dampedCurrent` -```js -import { Base, createApp } from '@studiometa/js-toolkit'; -import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; +- Type: `{ x: number, y: number }` -class App extends Base { - static config = { - name: 'App', - components: { - ScrollAnimationTimeline, - ScrollAnimationTarget, - }, - }; -} +Current damped scroll position values for both axes. -export default createApp(App, document.body); -``` +#### `dampedProgress` -```html -
-
- Content -
-
-``` +- Type: `{ x: number, y: number }` -### Children Components +Current damped progress values (0-1) for both axes. -#### `ScrollAnimationTarget` +#### `playRange` -- Type: `ScrollAnimationTarget[]` +- Type: `[number, number]` -Array of child animation targets that will be animated based on the scroll progress of the timeline. +The computed play range for the animation, taking into account the staggered format if used. --- -## ScrollAnimationTarget +## ScrollAnimation (deprecated) {#scrollanimation-deprecated} -A component that animates based on scroll progress from a parent `ScrollAnimationTimeline`. Each target can have its own animation keyframes and play range. +:::warning Deprecated +The `ScrollAnimation` component is deprecated. Use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children instead. +::: + +A standalone component that watches its own position in the viewport and animates a target element. This component requires a `target` ref. ### Usage ```html -
-
- First element -
-
- Second element +
+
+ Content to animate
``` -### Options - -`ScrollAnimationTarget` inherits all options from the base animation class and adds: - -#### `dampFactor` - -- Type: `number` -- Default: `0.1` +### Refs -Damping factor for smooth scroll animations. Lower values create smoother, slower animations. - -#### `dampPrecision` - -- Type: `number` -- Default: `0.001` +#### `target` -Precision threshold for damping calculations. Lower values increase precision but may impact performance. - -### Properties - -#### `dampedCurrent` - -- Type: `{ x: number, y: number }` +- Type: `HTMLElement` -Current damped scroll position values for both axes. +The element to animate. Required for this component. -#### `dampedProgress` +### Options -- Type: `{ x: number, y: number }` +The `ScrollAnimation` component supports the same options as `ScrollAnimationTarget`: -Current damped progress values (0-1) for both axes. +- [`playRange`](#playrange) +- [`from`](#from) +- [`to`](#to) +- [`keyframes`](#keyframes) +- [`easing`](#easing) From befc0d46424126549052c78de26dc7153a7dccf1 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 14:23:23 +0100 Subject: [PATCH 09/42] Add migration guide from v1.0 to v2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document all breaking changes for ScrollAnimation components: - ScrollAnimation → ScrollAnimationTimeline + ScrollAnimationTarget - ScrollAnimationParent → ScrollAnimationTimeline - ScrollAnimationChild → ScrollAnimationTarget - ScrollAnimationChildWithEase → ScrollAnimationTarget with dampFactor - ScrollAnimationWithEase → ScrollAnimationTimeline + ScrollAnimationTarget - animationScrollWithEase decorator removed --- .../docs/migration-guides/1.0-2.0/index.md | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 packages/docs/migration-guides/1.0-2.0/index.md diff --git a/packages/docs/migration-guides/1.0-2.0/index.md b/packages/docs/migration-guides/1.0-2.0/index.md new file mode 100644 index 00000000..4865221e --- /dev/null +++ b/packages/docs/migration-guides/1.0-2.0/index.md @@ -0,0 +1,248 @@ +# v1.0 → v2.0 + +You will find on this page documentation on all the breaking changes included in the v2.0 of the package. + +[[toc]] + +## ScrollAnimation components have been refactored + +The `ScrollAnimation` family of components has been refactored for better performance and flexibility. The new API uses `ScrollAnimationTimeline` as a parent component that manages scroll progress, with `ScrollAnimationTarget` children that handle individual animations. + +### Summary of component renames + +| v1.0 (deprecated) | v2.0 (new) | +| ----------------------------- | --------------------------------------------------- | +| `ScrollAnimation` | `ScrollAnimationTimeline` + `ScrollAnimationTarget` | +| `ScrollAnimationParent` | `ScrollAnimationTimeline` | +| `ScrollAnimationChild` | `ScrollAnimationTarget` | +| `ScrollAnimationChildWithEase`| `ScrollAnimationTarget` with `dampFactor` option | +| `ScrollAnimationWithEase` | `ScrollAnimationTimeline` + `ScrollAnimationTarget` | +| `animationScrollWithEase` | Extend `ScrollAnimationTarget` instead | + +### Benefits of the new API + +The refactored API provides several benefits: + +1. **Better performance**: The timeline only watches one element for scroll position, reducing the number of intersection observers +2. **Independent damping**: Each target can have its own `dampFactor`, allowing different animation speeds within the same timeline +3. **Simpler markup**: No need for a `target` ref — the component animates itself +4. **Clearer naming**: `Timeline` and `Target` clearly describe the parent-child relationship +5. **More flexible**: Easier to coordinate multiple animations with different play ranges + +### Replace `ScrollAnimation` with `ScrollAnimationTimeline` and `ScrollAnimationTarget` + +The standalone `ScrollAnimation` component has been removed. Use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children instead. + +**Before (v1.0):** + +```html +
+
+ Content to animate +
+
+``` + +```js +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimation } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimation, + }, + }; +} + +export default createApp(App, document.body); +``` + +**After (v2.0):** + +```html +
+
+ Content to animate +
+
+``` + +```js +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App, document.body); +``` + +::: tip Key differences +- The `target` ref is no longer needed — `ScrollAnimationTarget` animates itself +- Animation options (`from`, `to`, `playRange`, etc.) are now on `ScrollAnimationTarget` +- Multiple targets can share the same timeline for coordinated animations +::: + +### Replace `ScrollAnimationParent` with `ScrollAnimationTimeline` + +The `ScrollAnimationParent` component has been renamed to `ScrollAnimationTimeline`. + +**Before (v1.0):** + +```html +
+
+ ... +
+
+``` + +```js +import { ScrollAnimationParent, ScrollAnimationChild } from '@studiometa/ui'; +``` + +**After (v2.0):** + +```html +
+
+ ... +
+
+``` + +```js +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; +``` + +### Replace `ScrollAnimationChild` with `ScrollAnimationTarget` + +The `ScrollAnimationChild` component has been renamed to `ScrollAnimationTarget`. + +```diff +-
++
+ ... +
+``` + +```diff +- import { ScrollAnimationChild } from '@studiometa/ui'; ++ import { ScrollAnimationTarget } from '@studiometa/ui'; +``` + +### Replace `ScrollAnimationChildWithEase` with `ScrollAnimationTarget` + +The `ScrollAnimationChildWithEase` component has been removed. Use `ScrollAnimationTarget` with the `dampFactor` option instead. + +**Before (v1.0):** + +```html +
+
+ ... +
+
+``` + +```js +import { ScrollAnimationChildWithEase } from '@studiometa/ui'; +``` + +**After (v2.0):** + +```html +
+
+ ... +
+
+``` + +```js +import { ScrollAnimationTarget } from '@studiometa/ui'; +``` + +::: tip +Each `ScrollAnimationTarget` can have its own `dampFactor` value, allowing for different animation speeds within the same timeline. +::: + +### Replace `ScrollAnimationWithEase` with `ScrollAnimationTimeline` and `ScrollAnimationTarget` + +The `ScrollAnimationWithEase` component has been removed. Use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children that have the `dampFactor` option. + +**Before (v1.0):** + +```html +
+
...
+
+``` + +**After (v2.0):** + +```html +
+
+ ... +
+
+``` + +### Remove `animationScrollWithEase` decorator + +The `animationScrollWithEase` decorator has been removed without a direct replacement. If you were using it to create custom scroll animation components, extend `ScrollAnimationTarget` instead. + +**Before (v1.0):** + +```js +import { Base } from '@studiometa/js-toolkit'; +import { animationScrollWithEase } from '@studiometa/ui'; + +class MyScrollAnimation extends animationScrollWithEase(Base) { + // ... +} +``` + +**After (v2.0):** + +```js +import { ScrollAnimationTarget } from '@studiometa/ui'; + +class MyScrollAnimation extends ScrollAnimationTarget { + static config = { + ...ScrollAnimationTarget.config, + name: 'MyScrollAnimation', + }; + + // Override methods as needed +} +``` From 82c62222384f5d16a84719d5711d36ebc918bdb2 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 14:25:31 +0100 Subject: [PATCH 10/42] Add migration guides link to version dropdown menu --- packages/docs/.vitepress/config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/docs/.vitepress/config.ts b/packages/docs/.vitepress/config.ts index b6f67cc4..47d27b00 100644 --- a/packages/docs/.vitepress/config.ts +++ b/packages/docs/.vitepress/config.ts @@ -78,7 +78,10 @@ export default defineConfig({ }, { text: `v${pkg.version}`, - items: [{ text: 'Release Notes', link: 'https://github.com/studiometa/ui/releases' }], + items: [ + { text: 'Release Notes', link: 'https://github.com/studiometa/ui/releases' }, + { text: 'Migration guides', link: '/migration-guides/' }, + ], }, ], sidebar: { From 03bbd9741a04fb34be59f73e4c5e5d3ef572b5d4 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 14:42:34 +0100 Subject: [PATCH 11/42] Format MD files --- packages/docs/.vitepress/config.ts | 2 +- packages/docs/components/Action/index.md | 1 - packages/docs/components/Action/js-api.md | 49 ++++++------ .../docs/components/AnchorScrollto/js-api.md | 1 + packages/docs/components/Button/index.md | 1 - .../components/CircularMarquee/examples.md | 5 ++ packages/docs/components/Cursor/index.md | 3 +- .../docs/components/DataComputed/index.md | 1 - packages/docs/components/Draggable/index.md | 4 +- packages/docs/components/Draggable/js-api.md | 16 +--- packages/docs/components/Fetch/index.md | 1 - packages/docs/components/Figure/index.md | 5 +- packages/docs/components/Figure/js-api.md | 1 - packages/docs/components/Figure/twig-api.md | 3 +- .../docs/components/FigureShopify/index.md | 3 +- .../components/FigureTwicpics/examples.md | 1 - .../docs/components/FigureTwicpics/index.md | 3 +- .../components/FigureTwicpics/twig-api.md | 2 +- packages/docs/components/FigureVideo/index.md | 3 +- .../docs/components/FigureVideo/twig-api.md | 1 + .../FigureVideoTwicpics/examples.md | 1 - .../components/FigureVideoTwicpics/index.md | 3 +- packages/docs/components/Frame/examples.md | 2 +- .../Frame/js-api/frame-trigger-loader.md | 1 + .../docs/components/Frame/js-api/frame.md | 2 +- packages/docs/components/Hero/examples.md | 1 - packages/docs/components/Hoverable/index.md | 4 +- .../docs/components/LargeText/examples.md | 1 - .../components/MapboxStaticMap/examples.md | 2 - .../docs/components/MapboxStaticMap/index.md | 2 +- .../components/MapboxStaticMap/twig-api.md | 2 +- packages/docs/components/Menu/index.md | 2 +- packages/docs/components/Panel/twig-api.md | 4 +- .../docs/components/ScrollAnimation/index.md | 18 ++--- .../docs/components/ScrollAnimation/js-api.md | 26 ++++--- .../docs/components/ScrollReveal/index.md | 3 +- packages/docs/components/Slider/index.md | 1 - .../components/Slider/js-api/slider-count.md | 1 + .../components/Slider/js-api/slider-drag.md | 1 + packages/docs/components/Tabs/index.md | 2 +- packages/docs/components/Tabs/js-api.md | 3 + packages/docs/components/Tabs/twig-api.md | 2 + packages/docs/components/Transition/index.md | 3 +- packages/docs/components/Transition/js-api.md | 20 ++++- .../migration-guides/0.1.0-0.2.0/index.md | 10 +-- .../docs/migration-guides/1.0-2.0/index.md | 75 ++++++++----------- 46 files changed, 148 insertions(+), 150 deletions(-) diff --git a/packages/docs/.vitepress/config.ts b/packages/docs/.vitepress/config.ts index 47d27b00..9292d0dc 100644 --- a/packages/docs/.vitepress/config.ts +++ b/packages/docs/.vitepress/config.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'; import { basename, dirname } from 'node:path'; import { defineConfig } from 'vitepress'; import { transformerTwoslash } from '@shikijs/vitepress-twoslash'; -import { withLeadingSlash, withLeadingCharacters } from '@studiometa/js-toolkit/utils'; +import { withLeadingSlash } from '@studiometa/js-toolkit/utils'; import glob from 'fast-glob'; import pkg from '../package.json' with { type: 'json' }; diff --git a/packages/docs/components/Action/index.md b/packages/docs/components/Action/index.md index e48300df..9dcc9698 100644 --- a/packages/docs/components/Action/index.md +++ b/packages/docs/components/Action/index.md @@ -72,7 +72,6 @@ And specify an additional selector to filter the targeted components: - ### Simple usage with the `Target` component The `Target` component is a companion of the `Action` component that can be used to easily target other DOM elements without creating specific component. diff --git a/packages/docs/components/Action/js-api.md b/packages/docs/components/Action/js-api.md index 97ac8138..fda961cd 100644 --- a/packages/docs/components/Action/js-api.md +++ b/packages/docs/components/Action/js-api.md @@ -24,6 +24,7 @@ Use this option to change the event that will trigger the [effect callback](#eff Modifiers can be chained with a `.` as separator: + ```html {3} + ``` + #### Multiple targets + ```html {3} - + ``` + #### Reduce the list of target with a selector In the following example, the effect callback will only be triggered on the `Foo` component with the `foo` id. -```html {3,9} + +```html {3,7} -
- ... -
- -
- ... -
-``` +
...
+
...
+``` + ### `effect` @@ -135,6 +127,7 @@ The `effect` option must be used to define a small piece of JavaScript that will The effect can also define an arrow function which will be executed as well. The following examples are similar: + ```html {3,9}
``` + #### Accessing the current instances mounted on the current element Use the name of the component mounted on the current element to get access to its instance. + ```html {3}
@@ -77,11 +75,8 @@ export default createApp(App, document.body);
-
- Content to animate -
+ data-option-to='{"opacity": 1, "translateY": "0px"}'> +
Content to animate
``` @@ -99,6 +94,7 @@ export default createApp(App, document.body); ## Deprecated components :::warning Deprecated + The following components are deprecated and will be removed in a future version. Use `ScrollAnimationTimeline` and `ScrollAnimationTarget` instead: - `ScrollAnimation` → use `ScrollAnimationTimeline` and `ScrollAnimationTarget` @@ -107,5 +103,5 @@ The following components are deprecated and will be removed in a future version. - `ScrollAnimationChildWithEase` → use `ScrollAnimationTarget` - `ScrollAnimationWithEase` → use `ScrollAnimationTimeline` and `ScrollAnimationTarget` - `animationScrollWithEase` → no replacement -::: +::: diff --git a/packages/docs/components/ScrollAnimation/js-api.md b/packages/docs/components/ScrollAnimation/js-api.md index 13d79dd1..93c63932 100644 --- a/packages/docs/components/ScrollAnimation/js-api.md +++ b/packages/docs/components/ScrollAnimation/js-api.md @@ -32,8 +32,7 @@ export default createApp(App, document.body);
+ data-option-to='{"opacity": 1}'> Content
@@ -61,16 +60,14 @@ A component that animates based on scroll progress from a parent `ScrollAnimatio data-component="ScrollAnimationTarget" data-option-from='{"opacity": 0, "y": 100}' data-option-to='{"opacity": 1, "y": 0}' - data-option-play-range='[0, 0.5]' - > + data-option-play-range="[0, 0.5]"> First element
+ data-option-play-range="[0.5, 1]"> Second element
@@ -85,6 +82,7 @@ A component that animates based on scroll progress from a parent `ScrollAnimatio Define the scroll progress range when the animation should play. Values between 0 and 1, where 0 is when the timeline enters the viewport and 1 is when it exits. + ```html {4,11}
``` + ##### Staggered animation @@ -108,6 +107,7 @@ Staggered scroll animation can be created by giving 3 numbers to the `playRange` - `length`: the length of the staggered items - `step`: the delay to apply between each item in the staggered list + ```html {4,10}
``` + The following example uses Twig `loop.index0` and `loop.length` variables to generate `data-option-play-range` attributes with a staggered effect. @@ -170,6 +171,7 @@ Use the `[index, length, step]` format for the `data-option-play-range` attribut Initial keyframe for the animation. Define CSS properties as key-value pairs. + ```html {4}
``` + #### `to` @@ -187,6 +190,7 @@ Initial keyframe for the animation. Define CSS properties as key-value pairs. Final keyframe for the animation. Define CSS properties as key-value pairs. + ```html {4}
``` + #### `keyframes` @@ -244,6 +249,7 @@ interface KeyFrame extends TransformProps { Cubic-bezier easing values for the animation timing. + ```html {4}
``` + ##### Common easing values @@ -373,11 +380,8 @@ A standalone component that watches its own position in the viewport and animate
-
- Content to animate -
+ data-option-to='{"opacity": 1, "y": 0}'> +
Content to animate
``` diff --git a/packages/docs/components/ScrollReveal/index.md b/packages/docs/components/ScrollReveal/index.md index 13ebc244..051da2a8 100644 --- a/packages/docs/components/ScrollReveal/index.md +++ b/packages/docs/components/ScrollReveal/index.md @@ -35,8 +35,7 @@ export default createApp(App);
+ data-option-enter-active="transition">
...
``` diff --git a/packages/docs/components/Slider/index.md b/packages/docs/components/Slider/index.md index 8803a899..95456af1 100644 --- a/packages/docs/components/Slider/index.md +++ b/packages/docs/components/Slider/index.md @@ -16,7 +16,6 @@ badges: [JS] - [SliderItem](./js-api/slider-item.md) - [SliderProgress](./js-api/slider-progress.md) - ## Usage Use the `Slider` component to display items on a X axis and enable indexed navigation between them. diff --git a/packages/docs/components/Slider/js-api/slider-count.md b/packages/docs/components/Slider/js-api/slider-count.md index b05533d1..f7a9061c 100644 --- a/packages/docs/components/Slider/js-api/slider-count.md +++ b/packages/docs/components/Slider/js-api/slider-count.md @@ -1,4 +1,5 @@ # SliderCount + This component can be used to display the current index of the slider and update it on change. ## Refs diff --git a/packages/docs/components/Slider/js-api/slider-drag.md b/packages/docs/components/Slider/js-api/slider-drag.md index f323afc8..c402c6ff 100644 --- a/packages/docs/components/Slider/js-api/slider-drag.md +++ b/packages/docs/components/Slider/js-api/slider-drag.md @@ -9,6 +9,7 @@ This component can be used to add drag capabilities to the slider. It should wra
...
+
``` This component uses the [`withDrag` decorator](https://js-toolkit.studiometa.dev/api/decorators/withDrag.html) and inherits from its APIs. diff --git a/packages/docs/components/Tabs/index.md b/packages/docs/components/Tabs/index.md index 730ca5b3..01856ab2 100644 --- a/packages/docs/components/Tabs/index.md +++ b/packages/docs/components/Tabs/index.md @@ -39,7 +39,7 @@ export default createApp(App, document.body); content: 'Content for tab 1' }, { - title: 'Tab 2', + title: 'Tab 2', content: 'Content for tab 2' }, { diff --git a/packages/docs/components/Tabs/js-api.md b/packages/docs/components/Tabs/js-api.md index 758e0b59..f824aae7 100644 --- a/packages/docs/components/Tabs/js-api.md +++ b/packages/docs/components/Tabs/js-api.md @@ -23,6 +23,7 @@ HTMLElement references for tab content panels. Each panel corresponds to a tab b Configure the styles for different tab states. Available references are `btn` and `content`, each supporting `open`, `active`, and `closed` style states. + ```html data-option-styles='{ "btn": { @@ -35,6 +36,7 @@ data-option-styles='{ } }' ``` + ## Methods @@ -69,6 +71,7 @@ Emitted when a tab is disabled. The event data contains the disabled tab item. - Type: `TabItem[]` Array of tab items, each containing: + - `btn` (HTMLElement) - The tab button element - `content` (HTMLElement) - The tab content element - `isEnabled` (boolean) - Whether the tab is currently enabled diff --git a/packages/docs/components/Tabs/twig-api.md b/packages/docs/components/Tabs/twig-api.md index 928823c1..aa2015ab 100644 --- a/packages/docs/components/Tabs/twig-api.md +++ b/packages/docs/components/Tabs/twig-api.md @@ -44,6 +44,7 @@ Customize the wrapper around all tab buttons. By default, renders all tab button ### `title` Customize each tab button's content. Defaults to `item.title`. Available variables: + - `item` - The current tab item ### `content_wrapper` @@ -53,4 +54,5 @@ Customize the wrapper around all tab content panels. By default, renders all con ### `content` Customize each tab content panel. Defaults to `item.content`. Available variables: + - `item` - The current tab item diff --git a/packages/docs/components/Transition/index.md b/packages/docs/components/Transition/index.md index 10b7a890..1e04e693 100644 --- a/packages/docs/components/Transition/index.md +++ b/packages/docs/components/Transition/index.md @@ -64,8 +64,7 @@ You can now add a togglable component in your HTML with the needed option to des data-option-leave-active="transition duration-500 ease-out-expo" data-option-leave-to="transform translate-y-20 opacity-0" data-option-leave-keep - class="transform translate-y-4 opacity-0" -> + class="transform translate-y-4 opacity-0"> ...
``` diff --git a/packages/docs/components/Transition/js-api.md b/packages/docs/components/Transition/js-api.md index bbd0e005..4ffcfea9 100644 --- a/packages/docs/components/Transition/js-api.md +++ b/packages/docs/components/Transition/js-api.md @@ -14,12 +14,14 @@ outline: deep Defines the classes that describe the initial state of the enter transition. + ```html {2}
...
``` + ### `enterActive` @@ -28,12 +30,14 @@ Defines the classes that describe the initial state of the enter transition. Defines the classes that describe the transitioning state of the enter transition. + ```html {2}
...
``` + ### `enterTo` @@ -42,12 +46,14 @@ Defines the classes that describe the transitioning state of the enter transitio Defines the classes that describe the end state of the enter transition. + ```html {2}
...
``` + ### `enterKeep` @@ -56,12 +62,14 @@ Defines the classes that describe the end state of the enter transition. Configure wether or not the `enterTo` classes should be kept on the target element at the end of the enter transition. + ```html {2}
...
``` + ### `leaveFrom` @@ -70,12 +78,14 @@ Configure wether or not the `enterTo` classes should be kept on the target eleme Defines the classes that describe the initial state of the leave transition. + ```html {2}
...
``` + ### `leaveActive` @@ -84,12 +94,14 @@ Defines the classes that describe the initial state of the leave transition. Defines the classes that describe the transitioning state of the leave transition. + ```html {2}
...
``` + ### `leaveTo` @@ -98,12 +110,14 @@ Defines the classes that describe the transitioning state of the leave transitio Defines the classes that describe the end state of the leave transition. + ```html {2}
...
``` + ### `leaveKeep` @@ -112,13 +126,14 @@ Defines the classes that describe the end state of the leave transition. Configure wether or not the `leaveTo` classes should be kept on the target element at the end of the leave transition. + ```html {2}
...
``` - + ### `group` @@ -127,6 +142,7 @@ Configure wether or not the `leaveTo` classes should be kept on the target eleme Define a group to sync `enter` and `leave` transition between multiple instances. + ```html {2,7}
@@ -138,7 +154,7 @@ Define a group to sync `enter` and `leave` transition between multiple instances ...
``` - + ## Properties diff --git a/packages/docs/migration-guides/0.1.0-0.2.0/index.md b/packages/docs/migration-guides/0.1.0-0.2.0/index.md index 4c402f59..af55a1a1 100644 --- a/packages/docs/migration-guides/0.1.0-0.2.0/index.md +++ b/packages/docs/migration-guides/0.1.0-0.2.0/index.md @@ -4,11 +4,11 @@ The following components have been updated: -| Component | Previous version | New version | Changed | -|-----------------------------------------------|-----------------------------------|------------------------|-----------------------------------| -| [Button](/components/Button/) | | | • Twig Template API standardization | -| [Cursor](/components/Cursor/) | | | • Twig Template API standardization | -| [Figure](/components/Figure/) | | | • Twig Template API standardization | +| Component | Previous version | New version | Changed | +| ----------------------------------- | --------------------------------- | ---------------------- | ----------------------------------- | +| [Button](/components/Button/) | | | • Twig Template API standardization | +| [Cursor](/components/Cursor/) | | | • Twig Template API standardization | +| [Figure](/components/Figure/) | | | • Twig Template API standardization | | [Accordion](/components/Accordion/) | | | • Twig Template API standardization | | [Modal](/components/Modal/) | | | • Twig Template API standardization | | [Sticky](/components/Sticky/) | | | • Twig Template API standardization | diff --git a/packages/docs/migration-guides/1.0-2.0/index.md b/packages/docs/migration-guides/1.0-2.0/index.md index 4865221e..3549d5bb 100644 --- a/packages/docs/migration-guides/1.0-2.0/index.md +++ b/packages/docs/migration-guides/1.0-2.0/index.md @@ -1,6 +1,6 @@ -# v1.0 → v2.0 +# v1.x → v2.x -You will find on this page documentation on all the breaking changes included in the v2.0 of the package. +You will find on this page documentation on all the breaking changes included in the v2.x of the package. [[toc]] @@ -10,14 +10,14 @@ The `ScrollAnimation` family of components has been refactored for better perfor ### Summary of component renames -| v1.0 (deprecated) | v2.0 (new) | -| ----------------------------- | --------------------------------------------------- | -| `ScrollAnimation` | `ScrollAnimationTimeline` + `ScrollAnimationTarget` | -| `ScrollAnimationParent` | `ScrollAnimationTimeline` | -| `ScrollAnimationChild` | `ScrollAnimationTarget` | -| `ScrollAnimationChildWithEase`| `ScrollAnimationTarget` with `dampFactor` option | -| `ScrollAnimationWithEase` | `ScrollAnimationTimeline` + `ScrollAnimationTarget` | -| `animationScrollWithEase` | Extend `ScrollAnimationTarget` instead | +| v1.x (deprecated) | v2.x (new) | +| ------------------------------ | --------------------------------------------------- | +| `ScrollAnimation` | `ScrollAnimationTimeline` + `ScrollAnimationTarget` | +| `ScrollAnimationParent` | `ScrollAnimationTimeline` | +| `ScrollAnimationChild` | `ScrollAnimationTarget` | +| `ScrollAnimationChildWithEase` | `ScrollAnimationTarget` with `dampFactor` option | +| `ScrollAnimationWithEase` | `ScrollAnimationTimeline` + `ScrollAnimationTarget` | +| `animationScrollWithEase` | Extend `ScrollAnimationTarget` instead | ### Benefits of the new API @@ -33,17 +33,14 @@ The refactored API provides several benefits: The standalone `ScrollAnimation` component has been removed. Use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children instead. -**Before (v1.0):** +**Before (v1.x):** ```html
-
- Content to animate -
+ data-option-to='{"opacity": 1, "y": 0}'> +
Content to animate
``` @@ -63,15 +60,14 @@ class App extends Base { export default createApp(App, document.body); ``` -**After (v2.0):** +**After (v2.x):** ```html
+ data-option-to='{"opacity": 1, "y": 0}'> Content to animate
@@ -95,22 +91,22 @@ export default createApp(App, document.body); ``` ::: tip Key differences + - The `target` ref is no longer needed — `ScrollAnimationTarget` animates itself - Animation options (`from`, `to`, `playRange`, etc.) are now on `ScrollAnimationTarget` - Multiple targets can share the same timeline for coordinated animations + ::: ### Replace `ScrollAnimationParent` with `ScrollAnimationTimeline` The `ScrollAnimationParent` component has been renamed to `ScrollAnimationTimeline`. -**Before (v1.0):** +**Before (v1.x):** ```html
-
- ... -
+
...
``` @@ -118,13 +114,11 @@ The `ScrollAnimationParent` component has been renamed to `ScrollAnimationTimeli import { ScrollAnimationParent, ScrollAnimationChild } from '@studiometa/ui'; ``` -**After (v2.0):** +**After (v2.x):** ```html
-
- ... -
+
...
``` @@ -152,13 +146,11 @@ The `ScrollAnimationChild` component has been renamed to `ScrollAnimationTarget` The `ScrollAnimationChildWithEase` component has been removed. Use `ScrollAnimationTarget` with the `dampFactor` option instead. -**Before (v1.0):** +**Before (v1.x):** ```html
-
- ... -
+
...
``` @@ -166,15 +158,14 @@ The `ScrollAnimationChildWithEase` component has been removed. Use `ScrollAnimat import { ScrollAnimationChildWithEase } from '@studiometa/ui'; ``` -**After (v2.0):** +**After (v2.x):** ```html
+ data-option-from='{"opacity": 0}'> ...
@@ -192,26 +183,22 @@ Each `ScrollAnimationTarget` can have its own `dampFactor` value, allowing for d The `ScrollAnimationWithEase` component has been removed. Use `ScrollAnimationTimeline` with `ScrollAnimationTarget` children that have the `dampFactor` option. -**Before (v1.0):** +**Before (v1.x):** ```html -
+
...
``` -**After (v2.0):** +**After (v2.x):** ```html
+ data-option-from='{"opacity": 0}'> ...
@@ -221,7 +208,7 @@ The `ScrollAnimationWithEase` component has been removed. Use `ScrollAnimationTi The `animationScrollWithEase` decorator has been removed without a direct replacement. If you were using it to create custom scroll animation components, extend `ScrollAnimationTarget` instead. -**Before (v1.0):** +**Before (v1.x):** ```js import { Base } from '@studiometa/js-toolkit'; @@ -232,7 +219,7 @@ class MyScrollAnimation extends animationScrollWithEase(Base) { } ``` -**After (v2.0):** +**After (v2.x):** ```js import { ScrollAnimationTarget } from '@studiometa/ui'; From 596f1010dfff91922ddcf457ec7fe6e8c1307b2b Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 14:46:21 +0100 Subject: [PATCH 12/42] Add withScrolledInView decorator reference to ScrollAnimationTimeline docs --- packages/docs/components/ScrollAnimation/js-api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/docs/components/ScrollAnimation/js-api.md b/packages/docs/components/ScrollAnimation/js-api.md index 93c63932..354c19a0 100644 --- a/packages/docs/components/ScrollAnimation/js-api.md +++ b/packages/docs/components/ScrollAnimation/js-api.md @@ -8,6 +8,8 @@ title: ScrollAnimation JS API A parent component that manages scroll-based animations for its children `ScrollAnimationTarget` components. The timeline watches for its position in the viewport and propagates the scroll progress to all its children. +This component is based on the [`withScrolledInView`](https://js-toolkit.studiometa.dev/api/decorators/withScrolledInView.html) decorator from the `@studiometa/js-toolkit` package and inherits all of its options. + ### Usage ```js From 6b89c26cd427acefe287bd21a876a7f220a1da09 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 14:55:46 +0100 Subject: [PATCH 13/42] Fix playground static files serving in VitePress dev mode Add a custom Vite plugin that serves the public/play directory using sirv middleware. This prevents VitePress from intercepting requests to /play/* and rendering them as pages instead of serving the static files. --- packages/docs/vite.config.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/docs/vite.config.js b/packages/docs/vite.config.js index 0aad506b..b875c356 100644 --- a/packages/docs/vite.config.js +++ b/packages/docs/vite.config.js @@ -1,8 +1,11 @@ +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; import { defineConfig } from 'vite'; import Icons from 'unplugin-icons/vite'; import IconsResolver from 'unplugin-icons/resolver'; import Components from 'unplugin-vue-components/vite'; import llmstxt from 'vitepress-plugin-llms'; +import sirv from 'sirv'; /** * Import match as plain text. @@ -26,8 +29,34 @@ function plainText(match) { }; } +/** + * Serve the playground static files from the public directory. + * This is needed because VitePress intercepts requests to /play/* and renders + * them as pages instead of serving the static files. + * @returns {import('vite').Plugin} + */ +function servePlayground() { + const publicDir = resolve(import.meta.dirname, 'public'); + const playDir = resolve(publicDir, 'play'); + + return { + name: 'serve-playground', + configureServer(server) { + // Only add middleware if the play directory exists + if (!existsSync(playDir)) { + return; + } + + // Serve files from public/play at /play + const serve = sirv(playDir, { dev: true, etag: true }); + server.middlewares.use('/play', serve); + }, + }; +} + const config = defineConfig({ plugins: [ + servePlayground(), llmstxt({ stripHTML: false, }), From a539b8fe03aa85a6886c8f9ecdc2aeee63442bd0 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:06 +0100 Subject: [PATCH 14/42] Update staggered, sequence and parallax examples to use new API Co-authored-by: Claude --- .../components/ScrollAnimation/stories/parallax/app.js | 8 ++++---- .../ScrollAnimation/stories/parallax/app.twig | 10 +++++----- .../components/ScrollAnimation/stories/sequence/app.js | 7 ++++--- .../ScrollAnimation/stories/sequence/app.twig | 4 ++-- .../ScrollAnimation/stories/staggered/app.js | 7 ++++--- .../ScrollAnimation/stories/staggered/app.twig | 4 ++-- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/docs/components/ScrollAnimation/stories/parallax/app.js b/packages/docs/components/ScrollAnimation/stories/parallax/app.js index 7d625366..83400937 100644 --- a/packages/docs/components/ScrollAnimation/stories/parallax/app.js +++ b/packages/docs/components/ScrollAnimation/stories/parallax/app.js @@ -1,9 +1,9 @@ -import { Base, createApp } from '@studiometa/js-toolkit'; -import { Figure, ScrollAnimation } from '@studiometa/ui'; +import { Base, createApp, withScrolledInView } from '@studiometa/js-toolkit'; +import { Figure, ScrollAnimationTarget } from '@studiometa/ui'; -class Parallax extends ScrollAnimation { +class Parallax extends withScrolledInView(ScrollAnimationTarget) { static config = { - ...ScrollAnimation.config, + ...ScrollAnimationTarget.config, name: 'Parallax', components: { Figure, diff --git a/packages/docs/components/ScrollAnimation/stories/parallax/app.twig b/packages/docs/components/ScrollAnimation/stories/parallax/app.twig index 75d74a98..1836ee00 100644 --- a/packages/docs/components/ScrollAnimation/stories/parallax/app.twig +++ b/packages/docs/components/ScrollAnimation/stories/parallax/app.twig @@ -1,5 +1,5 @@ -
- Scroll down +
+ Scroll down ↓
{% set img_attr = { @@ -40,7 +40,7 @@ } ] %} -
+
{% include '@ui/ImageGrid/ImageGrid.twig' with { images: images, image_attr: { @@ -54,6 +54,6 @@ } %}
-
- Scroll up +
+ Scroll up ↑
diff --git a/packages/docs/components/ScrollAnimation/stories/sequence/app.js b/packages/docs/components/ScrollAnimation/stories/sequence/app.js index f785ce82..539bc8b4 100644 --- a/packages/docs/components/ScrollAnimation/stories/sequence/app.js +++ b/packages/docs/components/ScrollAnimation/stories/sequence/app.js @@ -1,12 +1,13 @@ import { Base, createApp } from '@studiometa/js-toolkit'; -import { ScrollAnimationParent } from '@studiometa/ui'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; class App extends Base { static config = { name: 'App', components: { - ScrollAnimationParent, - } + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, }; } diff --git a/packages/docs/components/ScrollAnimation/stories/sequence/app.twig b/packages/docs/components/ScrollAnimation/stories/sequence/app.twig index ffc924f1..b7696bc2 100644 --- a/packages/docs/components/ScrollAnimation/stories/sequence/app.twig +++ b/packages/docs/components/ScrollAnimation/stories/sequence/app.twig @@ -9,13 +9,13 @@
{% for item in items %}
{% for index in 1..total %}
Date: Mon, 26 Jan 2026 18:16:10 +0100 Subject: [PATCH 15/42] Remove old simple, parent and parallax-parent examples Co-authored-by: Claude --- .../stories/parallax-parent/app.js | 47 ------ .../stories/parallax-parent/app.twig | 58 -------- .../ScrollAnimation/stories/parent/app.js | 17 --- .../ScrollAnimation/stories/parent/app.twig | 82 ----------- .../ScrollAnimation/stories/simple/app.js | 14 -- .../ScrollAnimation/stories/simple/app.twig | 137 ------------------ 6 files changed, 355 deletions(-) delete mode 100644 packages/docs/components/ScrollAnimation/stories/parallax-parent/app.js delete mode 100644 packages/docs/components/ScrollAnimation/stories/parallax-parent/app.twig delete mode 100644 packages/docs/components/ScrollAnimation/stories/parent/app.js delete mode 100644 packages/docs/components/ScrollAnimation/stories/parent/app.twig delete mode 100644 packages/docs/components/ScrollAnimation/stories/simple/app.js delete mode 100644 packages/docs/components/ScrollAnimation/stories/simple/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.js b/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.js deleted file mode 100644 index 995e4295..00000000 --- a/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.js +++ /dev/null @@ -1,47 +0,0 @@ -import { Base, createApp } from '@studiometa/js-toolkit'; -import { Figure, ScrollAnimationTarget, ScrollAnimationTimeline } from '@studiometa/ui'; - -class ParallaxTarget extends ScrollAnimationTarget { - static config = { - ...ScrollAnimationTarget.config, - name: 'ParallaxTarget', - components: { - Figure, - }, - }; - - get target() { - return this.$children.Figure[0].$el; - } -} - -class ParallaxTimeline extends ScrollAnimationTimeline { - static config = { - ...ScrollAnimationTimeline.config, - name: 'ParallaxTimeline', - components: { - ParallaxTarget, - }, - }; - - get target() { - return this.$children.Figure[0].$el; - } - - scrolledInView(props) { - this.$children.ParallaxTarget.forEach((child) => { - child.scrolledInView(props); - }); - } -} - -class App extends Base { - static config = { - name: 'App', - components: { - ParallaxTimeline, - }, - }; -} - -export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.twig b/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.twig deleted file mode 100644 index 1fe6ff61..00000000 --- a/packages/docs/components/ScrollAnimation/stories/parallax-parent/app.twig +++ /dev/null @@ -1,58 +0,0 @@ -
- Scroll down -
- -{% set img_attr = { - class: 'object-cover', - style: { top: '-20%', height: '140%' } -} %} - -{% set images = [ - { - src: 'https://picsum.photos/700/600', - width: '700', - height: '600', - img_attr: img_attr - }, - { - src: 'https://picsum.photos/800/600', - width: '800', - height: '600', - img_attr: img_attr - }, - { - src: 'https://picsum.photos/600/800', - width: '600', - height: '800', - img_attr: img_attr - }, - { - src: 'https://picsum.photos/700/600', - width: '700', - height: '600', - img_attr: img_attr - }, - { - src: 'https://picsum.photos/800/600', - width: '800', - height: '600', - img_attr: img_attr - } -] %} - -
- {% include '@ui/ImageGrid/ImageGrid.twig' with { - images: images, - image_attr: { - data_component: 'ParallaxTarget', - data_option_from: { y: [-20, '%'] }, - data_option_to: { y: [20, '%'] }, - class: 'h-fit overflow-hidden', - style: { contain: 'content' } - } - } %} -
- -
- Scroll up -
diff --git a/packages/docs/components/ScrollAnimation/stories/parent/app.js b/packages/docs/components/ScrollAnimation/stories/parent/app.js deleted file mode 100644 index c38f8b1b..00000000 --- a/packages/docs/components/ScrollAnimation/stories/parent/app.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Base, createApp } from '@studiometa/js-toolkit'; -import { - ScrollAnimationTimeline, - ScrollAnimationTarget, -} from '@studiometa/ui'; - -class App extends Base { - static config = { - name: 'App', - components: { - ScrollAnimationTimeline, - ScrollAnimationTarget, - }, - }; -} - -export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/parent/app.twig b/packages/docs/components/ScrollAnimation/stories/parent/app.twig deleted file mode 100644 index 6e90a7e3..00000000 --- a/packages/docs/components/ScrollAnimation/stories/parent/app.twig +++ /dev/null @@ -1,82 +0,0 @@ - - -
- Scroll down -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- Lorem ipsum dolor sit amet -

-
-
-
-
-
-
-
diff --git a/packages/docs/components/ScrollAnimation/stories/simple/app.js b/packages/docs/components/ScrollAnimation/stories/simple/app.js deleted file mode 100644 index 0724ff31..00000000 --- a/packages/docs/components/ScrollAnimation/stories/simple/app.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Base, createApp } from '@studiometa/js-toolkit'; -import { ScrollAnimation, ScrollAnimationWithEase } from '@studiometa/ui'; - -class App extends Base { - static config = { - name: 'App', - components: { - ScrollAnimation, - ScrollAnimationWithEase, - }, - }; -} - -export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/simple/app.twig b/packages/docs/components/ScrollAnimation/stories/simple/app.twig deleted file mode 100644 index e0d32975..00000000 --- a/packages/docs/components/ScrollAnimation/stories/simple/app.twig +++ /dev/null @@ -1,137 +0,0 @@ - - -{% macro animated_image(width, height) %} -
- -
-{% endmacro %} - -
- Scroll down -
-
-
-
-
- {{ _self.animated_image(400, 600) }} - {{ _self.animated_image(400, 500) }} - {{ _self.animated_image(400, 550) }} -
-
-
-
- {{ _self.animated_image(400, 400) }} - {{ _self.animated_image(400, 450) }} - {{ _self.animated_image(400, 580) }} -
-
-
-
- {{ _self.animated_image(400, 440) }} - {{ _self.animated_image(400, 460) }} - {{ _self.animated_image(400, 560) }} - {{ _self.animated_image(400, 610) }} -
-
-
-
- {{ _self.animated_image(400, 530) }} - {{ _self.animated_image(400, 520) }} - {{ _self.animated_image(400, 620) }} -
-
-
-
- {{ _self.animated_image(400, 480) }} - {{ _self.animated_image(400, 540) }} - {{ _self.animated_image(400, 590) }} -
-
-
-
-
-
- - D - - - E - - - M - - - O - -
-
-
-
-

- Lorem ipsum dolor sit amet -

-
-
-
-
-
-
From a78251ce086dd76935af08189baa9fca9051b45f Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:13 +0100 Subject: [PATCH 16/42] Add from-to example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/from-to/app.js | 14 ++++++++++++++ .../ScrollAnimation/stories/from-to/app.twig | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/from-to/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/from-to/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/from-to/app.js b/packages/docs/components/ScrollAnimation/stories/from-to/app.js new file mode 100644 index 00000000..f0e861e5 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/from-to/app.js @@ -0,0 +1,14 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/from-to/app.twig b/packages/docs/components/ScrollAnimation/stories/from-to/app.twig new file mode 100644 index 00000000..36b2c820 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/from-to/app.twig @@ -0,0 +1,17 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+
+ +
+ Scroll up ↑ +
From 3f76d405c11a0775879c52cff752c399094da9cf Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:17 +0100 Subject: [PATCH 17/42] Add keyframes example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/keyframes/app.js | 14 +++++++++++++ .../stories/keyframes/app.twig | 21 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/keyframes/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/keyframes/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/keyframes/app.js b/packages/docs/components/ScrollAnimation/stories/keyframes/app.js new file mode 100644 index 00000000..f0e861e5 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/keyframes/app.js @@ -0,0 +1,14 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/keyframes/app.twig b/packages/docs/components/ScrollAnimation/stories/keyframes/app.twig new file mode 100644 index 00000000..88859555 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/keyframes/app.twig @@ -0,0 +1,21 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+
+ +
+ Scroll up ↑ +
From 56f0d54aab8faa8aa0c2f7f548dd8b85f48a41b7 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:21 +0100 Subject: [PATCH 18/42] Add play-range example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/play-range/app.js | 14 +++++++ .../stories/play-range/app.twig | 39 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/play-range/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/play-range/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/play-range/app.js b/packages/docs/components/ScrollAnimation/stories/play-range/app.js new file mode 100644 index 00000000..f0e861e5 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/play-range/app.js @@ -0,0 +1,14 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/play-range/app.twig b/packages/docs/components/ScrollAnimation/stories/play-range/app.twig new file mode 100644 index 00000000..6c3f8910 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/play-range/app.twig @@ -0,0 +1,39 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+

[0, 0.3]

+
+
+
+

[0.35, 0.65]

+
+
+
+

[0.7, 1]

+
+
+
+ +
+ Scroll up ↑ +
From cd20e0f9cedf79b7c2b3e5b52346e13e0f6f878c Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:25 +0100 Subject: [PATCH 19/42] Add easing example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/easing/app.js | 14 ++++++ .../ScrollAnimation/stories/easing/app.twig | 48 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/easing/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/easing/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/easing/app.js b/packages/docs/components/ScrollAnimation/stories/easing/app.js new file mode 100644 index 00000000..f0e861e5 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/easing/app.js @@ -0,0 +1,14 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/easing/app.twig b/packages/docs/components/ScrollAnimation/stories/easing/app.twig new file mode 100644 index 00000000..ac85d579 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/easing/app.twig @@ -0,0 +1,48 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+

linear

+
+
+
+

ease

+
+
+
+

ease-in

+
+
+
+

ease-out

+
+
+
+ +
+ Scroll up ↑ +
From b252b58b8df84ca8594e493cd7c0d4e82fbe881b Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:28 +0100 Subject: [PATCH 20/42] Add damp-factor example for ScrollAnimation Co-authored-by: Claude --- .../stories/damp-factor/app.js | 14 ++++++ .../stories/damp-factor/app.twig | 48 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/damp-factor/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/damp-factor/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/damp-factor/app.js b/packages/docs/components/ScrollAnimation/stories/damp-factor/app.js new file mode 100644 index 00000000..f0e861e5 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/damp-factor/app.js @@ -0,0 +1,14 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/damp-factor/app.twig b/packages/docs/components/ScrollAnimation/stories/damp-factor/app.twig new file mode 100644 index 00000000..dd6b1a39 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/damp-factor/app.twig @@ -0,0 +1,48 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+

1 (no damping)

+
+
+
+

0.5

+
+
+
+

0.1 (default)

+
+
+
+

0.02 (slow)

+
+
+
+ +
+ Scroll up ↑ +
From 1c053756cf612b966a0141063a895d409666597e Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:34 +0100 Subject: [PATCH 21/42] Add cards example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/cards/app.js | 14 +++++ .../ScrollAnimation/stories/cards/app.twig | 51 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/cards/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/cards/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/cards/app.js b/packages/docs/components/ScrollAnimation/stories/cards/app.js new file mode 100644 index 00000000..f0e861e5 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/cards/app.js @@ -0,0 +1,14 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/cards/app.twig b/packages/docs/components/ScrollAnimation/stories/cards/app.twig new file mode 100644 index 00000000..699d0823 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/cards/app.twig @@ -0,0 +1,51 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ Scroll up ↑ +
From bb7bf7e0ae14215c906033562ab60d7ab5c54907 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:37 +0100 Subject: [PATCH 22/42] Add text example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/text/app.js | 14 ++++++++ .../ScrollAnimation/stories/text/app.twig | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/text/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/text/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/text/app.js b/packages/docs/components/ScrollAnimation/stories/text/app.js new file mode 100644 index 00000000..f0e861e5 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/text/app.js @@ -0,0 +1,14 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/text/app.twig b/packages/docs/components/ScrollAnimation/stories/text/app.twig new file mode 100644 index 00000000..67d7880c --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/text/app.twig @@ -0,0 +1,36 @@ + + +
+ Scroll down ↓ +
+ +
+
+ + D + + + E + + + M + + + O + +
+
+ +
+ Scroll up ↑ +
From 2289ede5c265658e2eb6fe4c6cf7075d73496472 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:40 +0100 Subject: [PATCH 23/42] Add sticky example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/sticky/app.js | 14 +++++++++++ .../ScrollAnimation/stories/sticky/app.twig | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/sticky/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/sticky/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/sticky/app.js b/packages/docs/components/ScrollAnimation/stories/sticky/app.js new file mode 100644 index 00000000..f0e861e5 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/sticky/app.js @@ -0,0 +1,14 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { ScrollAnimationTimeline, ScrollAnimationTarget } from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline, + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/sticky/app.twig b/packages/docs/components/ScrollAnimation/stories/sticky/app.twig new file mode 100644 index 00000000..7053af3a --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/sticky/app.twig @@ -0,0 +1,24 @@ +
+ Scroll down ↓ +
+ +
+
+

+ Lorem ipsum dolor sit amet +

+
+
+
+
+
+ +
+ Scroll up ↑ +
From 3a28413d934b6c97fa339d705af9c5235a182f02 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:44 +0100 Subject: [PATCH 24/42] Add offset example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/offset/app.js | 18 +++++ .../ScrollAnimation/stories/offset/app.twig | 65 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/offset/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/offset/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/offset/app.js b/packages/docs/components/ScrollAnimation/stories/offset/app.js new file mode 100644 index 00000000..95f8b216 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset/app.js @@ -0,0 +1,18 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/offset/app.twig b/packages/docs/components/ScrollAnimation/stories/offset/app.twig new file mode 100644 index 00000000..ceda2214 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset/app.twig @@ -0,0 +1,65 @@ +
+ Scroll down ↓ +
+ +{# Default offset: "start end / end start" #} +{# Animation starts when top of element reaches bottom of viewport #} +{# Animation ends when bottom of element reaches top of viewport #} +
+
+
+
+

Default offset

+

"start end / end start"

+
+
+
+ +{# Center offset: "center center / center center" #} +{# Animation plays only when element center is at viewport center #} +
+
+
+
+

Centered offset

+

"start center / end center"

+
+
+
+ +{# Top section offset: "start start / end start" #} +{# Animation starts when top of element reaches top of viewport #} +{# Animation ends when bottom of element reaches top of viewport #} +
+
+
+
+

Top section offset

+

"start start / end start"

+
+
+
+ +
+ Scroll up ↑ +
From 442c6d2213169878ac3dd762f0c9e62884f81890 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:49 +0100 Subject: [PATCH 25/42] Add withScrollAnimationDebug decorator The decorator adds debug capabilities to ScrollAnimationTimeline. It is exported separately to allow tree-shaking from production bundles. Co-authored-by: Claude --- packages/tests/index.spec.ts | 1 + packages/ui/ScrollAnimation/index.ts | 1 + .../withScrollAnimationDebug.ts | 439 ++++++++++++++++++ 3 files changed, 441 insertions(+) create mode 100644 packages/ui/ScrollAnimation/withScrollAnimationDebug.ts diff --git a/packages/tests/index.spec.ts b/packages/tests/index.spec.ts index a86a11a6..151da8b6 100644 --- a/packages/tests/index.spec.ts +++ b/packages/tests/index.spec.ts @@ -67,6 +67,7 @@ test('components exports', () => { "Transition", "animationScrollWithEase", "withDeprecation", + "withScrollAnimationDebug", "withTransition", ] `); diff --git a/packages/ui/ScrollAnimation/index.ts b/packages/ui/ScrollAnimation/index.ts index 4507e87a..2ead62d0 100644 --- a/packages/ui/ScrollAnimation/index.ts +++ b/packages/ui/ScrollAnimation/index.ts @@ -1,6 +1,7 @@ export * from './AbstractScrollAnimation.js'; export * from './ScrollAnimationTimeline.js'; export * from './ScrollAnimationTarget.js'; +export * from './withScrollAnimationDebug.js'; // Deprecated exports export * from './animationScrollWithEase.js'; diff --git a/packages/ui/ScrollAnimation/withScrollAnimationDebug.ts b/packages/ui/ScrollAnimation/withScrollAnimationDebug.ts new file mode 100644 index 00000000..5dca158c --- /dev/null +++ b/packages/ui/ScrollAnimation/withScrollAnimationDebug.ts @@ -0,0 +1,439 @@ +import type { Base, BaseProps, BaseConfig, ScrollInViewProps } from '@studiometa/js-toolkit'; +import { createElement } from '@studiometa/js-toolkit/utils'; + +/** + * Debug marker element interface. + */ +interface DebugElements { + wrapper: HTMLElement; + startMarker: HTMLElement; + endMarker: HTMLElement; + progress: HTMLElement; + progressBar: HTMLElement; + progressText: HTMLElement; +} + +/** + * Debug colors for different timelines. + */ +const debugColors = [ + '#8b5cf6', // violet + '#3b82f6', // blue + '#10b981', // emerald + '#f59e0b', // amber + '#ef4444', // red + '#ec4899', // pink + '#06b6d4', // cyan + '#84cc16', // lime +]; + +/** + * Get debug color for a given index. + */ +function getDebugColor(index: number): string { + return debugColors[(index - 1) % debugColors.length]; +} + +/** + * Debug styles. + */ +const debugStyles = ` + .scroll-animation-debug { + --debug-color: #8b5cf6; + position: fixed; + right: 0; + top: 0; + bottom: 0; + width: 80px; + pointer-events: none; + z-index: 99999; + font-family: ui-monospace, monospace; + font-size: 10px; + } + .scroll-animation-debug__marker { + position: absolute; + right: 0; + display: flex; + align-items: center; + gap: 4px; + padding: 2px 6px; + color: white; + white-space: nowrap; + transform: translateY(-50%); + } + .scroll-animation-debug__marker::before { + content: ''; + position: absolute; + top: 50%; + right: 100%; + width: 100vw; + height: 1px; + background: var(--debug-color); + } + .scroll-animation-debug__marker--start { + background: var(--debug-color); + } + .scroll-animation-debug__marker--end { + background: var(--debug-color); + opacity: 0.7; + } + .scroll-animation-debug__outline { + position: absolute; + inset: 0; + border: 2px dashed var(--debug-color); + pointer-events: none; + z-index: 99998; + } + .scroll-animation-debug-progress-container { + position: fixed; + right: 8px; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + pointer-events: none; + z-index: 99999; + font-family: ui-monospace, monospace; + font-size: 10px; + } + .scroll-animation-debug__progress { + --debug-color: #8b5cf6; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + } + .scroll-animation-debug__progress-bar { + width: 4px; + height: 60px; + background: rgba(139, 92, 246, 0.2); + border-radius: 2px; + overflow: hidden; + } + .scroll-animation-debug__progress-fill { + width: 100%; + background: var(--debug-color); + border-radius: 2px; + transition: height 0.1s ease-out; + } + .scroll-animation-debug__progress-text { + color: var(--debug-color); + font-weight: bold; + width: 3ch; + text-align: center; + } +`; + +/** + * Shared style element. + */ +let styleElement: HTMLStyleElement | null = null; + +/** + * Shared progress container. + */ +let progressContainer: HTMLElement | null = null; + +/** + * Debug instance counter. + */ +let debugInstanceCount = 0; + +/** + * Debug ID counter. + */ +let debugIdCounter = 0; + +/** + * Inject debug styles if not already injected. + */ +function injectDebugStyles() { + if (!styleElement) { + styleElement = createElement('style', [debugStyles]) as HTMLStyleElement; + document.head.appendChild(styleElement); + } +} + +/** + * Remove debug styles if no more instances. + */ +function removeDebugStyles() { + if (styleElement && debugInstanceCount === 0) { + styleElement.remove(); + styleElement = null; + } +} + +/** + * Get or create the shared progress container. + */ +function getProgressContainer(): HTMLElement { + if (!progressContainer) { + progressContainer = createElement('div', { + class: 'scroll-animation-debug-progress-container', + }); + document.body.appendChild(progressContainer); + } + return progressContainer; +} + +/** + * Remove progress container if empty. + */ +function cleanupProgressContainer() { + if (progressContainer && progressContainer.children.length === 0) { + progressContainer.remove(); + progressContainer = null; + } +} + +/** + * Named offset values mapping. + */ +const namedOffsets: Record = { + start: 0, + center: 0.5, + end: 1, +}; + +/** + * Parse an offset value to a ratio (0-1). + */ +function parseOffsetValue(value: string): number { + if (namedOffsets[value] !== undefined) { + return namedOffsets[value]; + } + if (value.endsWith('%')) { + return Number.parseFloat(value) / 100; + } + return Number.parseFloat(value) || 0; +} + +/** + * Parse the offset option to get viewport positions. + * Format: " / " + */ +function parseOffset(offset: string): { viewportStart: number; viewportEnd: number } { + const parts = offset.split('/').map((part) => part.trim().split(' ')); + const viewportStart = parseOffsetValue(parts[0]?.[1] || 'end'); + const viewportEnd = parseOffsetValue(parts[1]?.[1] || 'start'); + return { viewportStart, viewportEnd }; +} + +/** + * Create debug marker elements. + */ +function createDebugElements(color: string): DebugElements { + const startMarker = createElement( + 'div', + { class: 'scroll-animation-debug__marker scroll-animation-debug__marker--start' }, + [createElement('span', ['start'])], + ); + + const endMarker = createElement( + 'div', + { class: 'scroll-animation-debug__marker scroll-animation-debug__marker--end' }, + [createElement('span', ['end'])], + ); + + const wrapper = createElement('div', { class: 'scroll-animation-debug' }, [ + startMarker, + endMarker, + ]) as HTMLElement; + wrapper.style.setProperty('--debug-color', color); + + const progressBar = createElement('div', { class: 'scroll-animation-debug__progress-fill' }); + const progressText = createElement('span', { class: 'scroll-animation-debug__progress-text' }, [ + '0%', + ]); + + const progress = createElement('div', { class: 'scroll-animation-debug__progress' }, [ + createElement('div', { class: 'scroll-animation-debug__progress-bar' }, [progressBar]), + progressText, + ]) as HTMLElement; + progress.style.setProperty('--debug-color', color); + + return { + wrapper, + startMarker, + endMarker, + progress, + progressBar, + progressText, + }; +} + +/** + * Create outline element for the timeline. + */ +function createOutlineElement(color: string): HTMLElement { + const outline = createElement('div', { + class: 'scroll-animation-debug__outline', + }) as HTMLElement; + outline.style.setProperty('--debug-color', color); + return outline; +} + +export interface WithScrollAnimationDebugProps extends BaseProps { + $options: { + debug: boolean; + offset: string; + }; +} + +/** + * Add debug capabilities to a ScrollAnimationTimeline component. + * + * @example + * ```js + * import { ScrollAnimationTimeline } from '@studiometa/ui'; + * import { withScrollAnimationDebug } from '@studiometa/ui/ScrollAnimation/withScrollAnimationDebug'; + * + * class App extends Base { + * static config = { + * name: 'App', + * components: { + * ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + * }, + * }; + * } + * ``` + */ +export function withScrollAnimationDebug< + S extends Base, + T extends typeof Base, +>(BaseClass: T): T { + // @ts-expect-error - Dynamic class extension + return class extends BaseClass { + static config: BaseConfig = { + ...BaseClass.config, + name: BaseClass.config.name, + options: { + ...BaseClass.config.options, + debug: Boolean, + }, + }; + + /** + * Debug elements. + */ + private __debugElements: DebugElements | null = null; + + /** + * Debug outline element. + */ + private __debugOutline: HTMLElement | null = null; + + /** + * Debug ID. + */ + private __debugId: string = ''; + + /** + * Mounted hook. + */ + mounted() { + // @ts-expect-error - Calling parent method + if (super.mounted) super.mounted(); + if (this.$options.debug) { + this.__initDebug(); + } + } + + /** + * Destroyed hook. + */ + destroyed() { + this.__destroyDebug(); + // @ts-expect-error - Calling parent method + if (super.destroyed) super.destroyed(); + } + + /** + * Initialize debug elements. + */ + private __initDebug() { + debugIdCounter += 1; + debugInstanceCount += 1; + this.__debugId = `timeline-${debugIdCounter}`; + + const color = getDebugColor(debugIdCounter); + + // Inject styles + injectDebugStyles(); + + // Create and append debug markers + this.__debugElements = createDebugElements(color); + document.body.appendChild(this.__debugElements.wrapper); + + // Append progress to shared container + const container = getProgressContainer(); + container.appendChild(this.__debugElements.progress); + + // Position markers based on offset option + const { viewportStart, viewportEnd } = parseOffset(this.$options.offset); + const viewportHeight = window.innerHeight; + + this.__debugElements.startMarker.style.top = `${viewportStart * viewportHeight}px`; + this.__debugElements.endMarker.style.top = `${viewportEnd * viewportHeight}px`; + + // Create and append outline + this.__debugOutline = createOutlineElement(color); + const position = getComputedStyle(this.$el).position; + if (position === 'static') { + (this.$el as HTMLElement).style.position = 'relative'; + } + this.$el.appendChild(this.__debugOutline); + } + + /** + * Destroy debug elements. + */ + private __destroyDebug() { + if (this.__debugElements) { + this.__debugElements.wrapper.remove(); + this.__debugElements.progress.remove(); + this.__debugElements = null; + } + + if (this.__debugOutline) { + this.__debugOutline.remove(); + this.__debugOutline = null; + } + + if (this.$options.debug) { + debugInstanceCount -= 1; + cleanupProgressContainer(); + removeDebugStyles(); + } + } + + /** + * Update debug progress. + */ + private __updateDebug(props: ScrollInViewProps) { + if (!this.__debugElements) return; + + const { progressBar, progressText } = this.__debugElements; + + // Update progress + const progress = Math.round(props.dampedProgress.y * 100); + progressBar.style.height = `${progress}%`; + progressText.textContent = `${progress}%`; + } + + /** + * Scrolled in view hook. + */ + scrolledInView(props: ScrollInViewProps) { + if (this.$options.debug) { + this.__updateDebug(props); + } + + // @ts-expect-error - Calling parent method + super.scrolledInView(props); + } + }; +} From d31893ac9dfd9fce475601aec4bce4ddd550fda2 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:52 +0100 Subject: [PATCH 26/42] Add debug example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/debug/app.js | 18 ++++++++++++++++++ .../ScrollAnimation/stories/debug/app.twig | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/debug/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/debug/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/debug/app.js b/packages/docs/components/ScrollAnimation/stories/debug/app.js new file mode 100644 index 00000000..95f8b216 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/debug/app.js @@ -0,0 +1,18 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/debug/app.twig b/packages/docs/components/ScrollAnimation/stories/debug/app.twig new file mode 100644 index 00000000..c2be3958 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/debug/app.twig @@ -0,0 +1,19 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+
+ +
+ Scroll up ↑ +
From dd1644cd3e54c1b1bd5ca041eb3ff981979f38d7 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:16:57 +0100 Subject: [PATCH 27/42] Update ScrollAnimation examples documentation - Reorganize examples with simple option examples first - Add scroll indicators to all examples - Add debug and offset examples Co-authored-by: Claude --- .../components/ScrollAnimation/examples.md | 237 +++++++++++++++--- 1 file changed, 200 insertions(+), 37 deletions(-) diff --git a/packages/docs/components/ScrollAnimation/examples.md b/packages/docs/components/ScrollAnimation/examples.md index 4673ae7a..db232dc5 100644 --- a/packages/docs/components/ScrollAnimation/examples.md +++ b/packages/docs/components/ScrollAnimation/examples.md @@ -4,133 +4,296 @@ title: ScrollAnimation examples # Examples -## Simple animation +## From / To + +Use the `from` and `to` options to define the start and end states of the animation. :::code-group -<<< ./stories/simple/app.twig -<<< ./stories/simple/app.js +<<< ./stories/from-to/app.twig +<<< ./stories/from-to/app.js ::: -## Timeline driven animation +## Keyframes -Coordinate multiple animations using `ScrollAnimationTimeline` with `ScrollAnimationTarget` children. +Use the `keyframes` option to define multiple animation steps. Each keyframe can have an optional `offset` (0-1) to control when it occurs and an `easing` to control the interpolation to the next keyframe. :::code-group -<<< ./stories/parent/app.twig -<<< ./stories/parent/app.js +<<< ./stories/keyframes/app.twig +<<< ./stories/keyframes/app.js ::: -## Staggered animation +## Play range -Use the `data-option-play-range` attribute with a value following the pattern `[index, length, step]` to add a staggered effect on multiple components. +Use the `playRange` option to control when the animation starts and ends relative to the scroll progress. The value is an array of two numbers between 0 and 1. :::code-group -<<< ./stories/staggered/app.twig -<<< ./stories/staggered/app.js +<<< ./stories/play-range/app.twig +<<< ./stories/play-range/app.js ::: -## Sequentially played animation +## Easing -Like the [staggered animation](#staggered-animation), use the pattern `[index, length, step]` for the `data-option-play-range` attribute, but set the `step` value to be `1 / length` to make each animation in the staggered list play one after another. +Use the `easing` option to control the animation curve. The value is a cubic-bezier array `[x1, y1, x2, y2]`. :::code-group -<<< ./stories/sequence/app.twig -<<< ./stories/sequence/app.js +<<< ./stories/easing/app.twig +<<< ./stories/easing/app.js ::: -## Parallax +## Damp factor -You can easily implement a parallax effect with images by combining the `ScrollAnimation` class with the [Figure component](/components/Figure/). +Use the `dampFactor` option to add smoothing to the animation. Lower values create smoother, slower animations. The default value is `0.1`. -Here, we even use the [ImageGrid organism](/components/ImageGrid/) to quickly have a nice listing layout. We are able to configure it to wrap each image in a `Parallax` component. + + + + + +:::code-group + +<<< ./stories/damp-factor/app.twig +<<< ./stories/damp-factor/app.js + +::: + + + +## Debug + +Use the `withScrollAnimationDebug` decorator and the `debug` option to display visual markers showing the start/end positions and current progress. This is useful during development to understand how the scroll animation is triggered. The decorator allows the debug code to be tree-shaken from production bundles. :::code-group -<<< ./stories/parallax/app.twig -<<< ./stories/parallax/app.js +<<< ./stories/debug/app.twig +<<< ./stories/debug/app.js ::: -## Parallax with a timeline +## Offset -It might be sometimes interesting to use the timeline ↔ target logic of the `ScrollAnimation` component to improve performance, as only the timeline progression in the viewport is watched. +Use the `offset` option on `ScrollAnimationTimeline` to control when the animation starts and ends relative to the viewport. This is useful when a section is at the top, center, or bottom of a page. -The resulting effect is different as each target animation is driven by the timeline, but it is still interesting. + + + + + +:::code-group + +<<< ./stories/offset/app.twig +<<< ./stories/offset/app.js + +::: + + + +## Cards + +Animate multiple elements within a single timeline. + + + + + + +:::code-group + +<<< ./stories/cards/app.twig +<<< ./stories/cards/app.js + +::: + + + +## Text + +Animate text characters individually. :::code-group -<<< ./stories/parallax-parent/app.twig -<<< ./stories/parallax-parent/app.js +<<< ./stories/text/app.twig +<<< ./stories/text/app.js + +::: + + + +## Sticky animation + +Use the `sticky` CSS property to create a sticky element that animates as you scroll. + + + + + + +:::code-group + +<<< ./stories/sticky/app.twig +<<< ./stories/sticky/app.js + +::: + + + +## Staggered animation + +Use the `playRange` option with a value following the pattern `[index, length, step]` to add a staggered effect on multiple components. + + + + + + +:::code-group + +<<< ./stories/staggered/app.twig +<<< ./stories/staggered/app.js + +::: + + + +## Sequentially played animation + +Like the [staggered animation](#staggered-animation), use the pattern `[index, length, step]` for the `playRange` option, but set the `step` value to be `1 / length` to make each animation in the staggered list play one after another. + + + + + + +:::code-group + +<<< ./stories/sequence/app.twig +<<< ./stories/sequence/app.js + +::: + + + +## Parallax + +You can easily implement a parallax effect with images by combining the `ScrollAnimationTarget` class with the [Figure component](/components/Figure/). + +Here, we even use the [ImageGrid organism](/components/ImageGrid/) to quickly have a nice listing layout. We are able to configure it to wrap each image in a `Parallax` component. + + + + + + +:::code-group + +<<< ./stories/parallax/app.twig +<<< ./stories/parallax/app.js ::: From 1a18297a09b827516bc80f99f26895e1ee03c4a0 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:17:00 +0100 Subject: [PATCH 28/42] Update ScrollAnimation JS API documentation - Add debug option documentation - Add offset option documentation - Add withScrollAnimationDebug decorator documentation Co-authored-by: Claude --- .../docs/components/ScrollAnimation/js-api.md | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/packages/docs/components/ScrollAnimation/js-api.md b/packages/docs/components/ScrollAnimation/js-api.md index 354c19a0..38bd879a 100644 --- a/packages/docs/components/ScrollAnimation/js-api.md +++ b/packages/docs/components/ScrollAnimation/js-api.md @@ -40,6 +40,132 @@ export default createApp(App, document.body);
``` +### Options + +#### `debug` + +- Type: `boolean` +- Default: `false` + +Enable debug mode to display visual markers showing the scroll animation's start/end positions and current progress. This is useful during development to understand how the scroll animation is triggered. + +:::warning +To use the debug option, you must wrap your `ScrollAnimationTimeline` with the `withScrollAnimationDebug` decorator. This allows the debug code to be tree-shaken from production bundles. +::: + +When enabled, the following elements are displayed: +- A dashed outline around the timeline element +- Start and end markers on the right side of the viewport +- A progress bar and percentage indicator + +```js {5,12} +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} +``` + + +```html {2} +
+
+ ... +
+
+``` + + + + + + + +:::code-group + +<<< ./stories/debug/app.twig +<<< ./stories/debug/app.js + +::: + + + +#### `offset` + +- Type: `string` +- Default: `"start end / end start"` + +Defines the limits used to calculate the progress of the scroll. The value is a string composed of two parts separated by a slash (`/`). Each part defines the point on which the progress calculation should be based. + +``` + / +``` + +The default value `start end / end start` could be read as: calculate the progress of the target from when the **start** of the target crosses the **end** of the viewport to when the **end** of the target crosses the **start** of the viewport. + +Each point accepts the following values: + +- A **number** between `0` and `1` +- A **named string**, either `start`, `end` or `center` which will be mapped to values between `0` and `1` +- A **string** representing a CSS value with one of the following unit: `%`, `px`, `vw`, `vh`, `vmin`, `vmax` + +**Common offset patterns:** + +| Offset | Description | +| ------ | ----------- | +| `"start end / end start"` | Default. Animation plays while element is visible in viewport | +| `"start center / end center"` | Animation plays while element crosses the center of viewport | +| `"start start / end start"` | Animation plays while element is at the top of viewport | +| `"start end / end end"` | Animation plays while element is at the bottom of viewport | +| `"start start / end end"` | Animation plays from when element enters until it completely leaves | + + +```html {2} +
+
+ Animates as element crosses viewport center +
+
+``` + + + + + + + +:::code-group + +<<< ./stories/offset/app.twig +<<< ./stories/offset/app.js + +::: + + + ### Children Components #### `ScrollAnimationTarget` @@ -404,3 +530,61 @@ The `ScrollAnimation` component supports the same options as `ScrollAnimationTar - [`to`](#to) - [`keyframes`](#keyframes) - [`easing`](#easing) + +--- + +## withScrollAnimationDebug + +A decorator that adds debug capabilities to `ScrollAnimationTimeline`. When the `debug` option is enabled, it displays visual markers to help understand how the scroll animation is triggered. + +This decorator is exported separately to allow tree-shaking the debug code from production bundles. + +### Usage + +```js +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); +``` + +```html +
+
+ ... +
+
+``` + +### Debug features + +When the `debug` option is enabled on a `ScrollAnimationTimeline` component wrapped with this decorator: + +- **Outline**: A dashed border around the timeline element +- **Start marker**: A horizontal line showing where in the viewport the animation starts +- **End marker**: A horizontal line showing where in the viewport the animation ends +- **Progress indicator**: A progress bar and percentage showing the current scroll progress + +Each timeline gets a different color for easy identification when multiple timelines are on the same page. + +### Parameters + +- `BaseClass` (`typeof ScrollAnimationTimeline`): The `ScrollAnimationTimeline` class to decorate + +### Return value + +- `typeof ScrollAnimationTimeline`: The decorated class with debug capabilities From de418d01f0a35545fb1c56bf20e5f590572bc2a2 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:18:04 +0100 Subject: [PATCH 29/42] Add scroll down sections between offset examples Co-authored-by: Claude --- .../components/ScrollAnimation/stories/offset/app.twig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/docs/components/ScrollAnimation/stories/offset/app.twig b/packages/docs/components/ScrollAnimation/stories/offset/app.twig index ceda2214..549a2e2d 100644 --- a/packages/docs/components/ScrollAnimation/stories/offset/app.twig +++ b/packages/docs/components/ScrollAnimation/stories/offset/app.twig @@ -21,6 +21,10 @@
+
+ Scroll down ↓ +
+ {# Center offset: "center center / center center" #} {# Animation plays only when element center is at viewport center #}
+
+ Scroll down ↓ +
+ {# Top section offset: "start start / end start" #} {# Animation starts when top of element reaches top of viewport #} {# Animation ends when bottom of element reaches top of viewport #} From 9bac0a09b6819ef0f5779e86e333ed57e81e940a Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:19:01 +0100 Subject: [PATCH 30/42] Add numbered and percentage offset examples Co-authored-by: Claude --- .../ScrollAnimation/stories/offset/app.twig | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/docs/components/ScrollAnimation/stories/offset/app.twig b/packages/docs/components/ScrollAnimation/stories/offset/app.twig index 549a2e2d..4e370646 100644 --- a/packages/docs/components/ScrollAnimation/stories/offset/app.twig +++ b/packages/docs/components/ScrollAnimation/stories/offset/app.twig @@ -68,6 +68,54 @@
+
+ Scroll down ↓ +
+ +{# Numbered offset: "0 0.8 / 1 0.2" #} +{# Animation starts when top of element reaches 80% of viewport #} +{# Animation ends when bottom of element reaches 20% of viewport #} +
+
+
+
+

Numbered offset

+

"0 0.8 / 1 0.2"

+
+
+
+ +
+ Scroll down ↓ +
+ +{# Percentage offset: "0 75% / 100% 25%" #} +{# Animation starts when top of element reaches 75% of viewport #} +{# Animation ends when bottom of element reaches 25% of viewport #} +
+
+
+
+

Percentage offset

+

"0 75% / 100% 25%"

+
+
+
+
Scroll up ↑
From eb654ac38ffa296afdd0a8e186768b4f4ef5e2da Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:22:43 +0100 Subject: [PATCH 31/42] Remove combined offset example Co-authored-by: Claude --- .../ScrollAnimation/stories/offset/app.js | 18 --- .../ScrollAnimation/stories/offset/app.twig | 121 ------------------ 2 files changed, 139 deletions(-) delete mode 100644 packages/docs/components/ScrollAnimation/stories/offset/app.js delete mode 100644 packages/docs/components/ScrollAnimation/stories/offset/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/offset/app.js b/packages/docs/components/ScrollAnimation/stories/offset/app.js deleted file mode 100644 index 95f8b216..00000000 --- a/packages/docs/components/ScrollAnimation/stories/offset/app.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Base, createApp } from '@studiometa/js-toolkit'; -import { - ScrollAnimationTimeline, - ScrollAnimationTarget, - withScrollAnimationDebug, -} from '@studiometa/ui'; - -class App extends Base { - static config = { - name: 'App', - components: { - ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), - ScrollAnimationTarget, - }, - }; -} - -export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/offset/app.twig b/packages/docs/components/ScrollAnimation/stories/offset/app.twig deleted file mode 100644 index 4e370646..00000000 --- a/packages/docs/components/ScrollAnimation/stories/offset/app.twig +++ /dev/null @@ -1,121 +0,0 @@ -
- Scroll down ↓ -
- -{# Default offset: "start end / end start" #} -{# Animation starts when top of element reaches bottom of viewport #} -{# Animation ends when bottom of element reaches top of viewport #} -
-
-
-
-

Default offset

-

"start end / end start"

-
-
-
- -
- Scroll down ↓ -
- -{# Center offset: "center center / center center" #} -{# Animation plays only when element center is at viewport center #} -
-
-
-
-

Centered offset

-

"start center / end center"

-
-
-
- -
- Scroll down ↓ -
- -{# Top section offset: "start start / end start" #} -{# Animation starts when top of element reaches top of viewport #} -{# Animation ends when bottom of element reaches top of viewport #} -
-
-
-
-

Top section offset

-

"start start / end start"

-
-
-
- -
- Scroll down ↓ -
- -{# Numbered offset: "0 0.8 / 1 0.2" #} -{# Animation starts when top of element reaches 80% of viewport #} -{# Animation ends when bottom of element reaches 20% of viewport #} -
-
-
-
-

Numbered offset

-

"0 0.8 / 1 0.2"

-
-
-
- -
- Scroll down ↓ -
- -{# Percentage offset: "0 75% / 100% 25%" #} -{# Animation starts when top of element reaches 75% of viewport #} -{# Animation ends when bottom of element reaches 25% of viewport #} -
-
-
-
-

Percentage offset

-

"0 75% / 100% 25%"

-
-
-
- -
- Scroll up ↑ -
From 500b13852074ea10f5cf0ef501b3eebbfff1f9bd Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:22:47 +0100 Subject: [PATCH 32/42] Add offset-default example for ScrollAnimation Co-authored-by: Claude --- .../stories/offset-default/app.js | 18 +++++++++++++++ .../stories/offset-default/app.twig | 23 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-default/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-default/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/offset-default/app.js b/packages/docs/components/ScrollAnimation/stories/offset-default/app.js new file mode 100644 index 00000000..95f8b216 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-default/app.js @@ -0,0 +1,18 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/offset-default/app.twig b/packages/docs/components/ScrollAnimation/stories/offset-default/app.twig new file mode 100644 index 00000000..49e066ab --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-default/app.twig @@ -0,0 +1,23 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+

Default offset

+

"start end / end start"

+
+
+
+ +
+ Scroll up ↑ +
From 32620eadce78ffd8c3014bd427f5d7bc9f59ca40 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:22:50 +0100 Subject: [PATCH 33/42] Add offset-center example for ScrollAnimation Co-authored-by: Claude --- .../stories/offset-center/app.js | 18 ++++++++++++++ .../stories/offset-center/app.twig | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-center/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-center/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/offset-center/app.js b/packages/docs/components/ScrollAnimation/stories/offset-center/app.js new file mode 100644 index 00000000..95f8b216 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-center/app.js @@ -0,0 +1,18 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/offset-center/app.twig b/packages/docs/components/ScrollAnimation/stories/offset-center/app.twig new file mode 100644 index 00000000..804ea56f --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-center/app.twig @@ -0,0 +1,24 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+

Centered offset

+

"start center / end center"

+
+
+
+ +
+ Scroll up ↑ +
From 4ae128d958a3c0b7f0b2388b7d2c7e35a9f9d588 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:22:53 +0100 Subject: [PATCH 34/42] Add offset-top example for ScrollAnimation Co-authored-by: Claude --- .../ScrollAnimation/stories/offset-top/app.js | 18 ++++++++++++++ .../stories/offset-top/app.twig | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-top/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-top/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/offset-top/app.js b/packages/docs/components/ScrollAnimation/stories/offset-top/app.js new file mode 100644 index 00000000..95f8b216 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-top/app.js @@ -0,0 +1,18 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/offset-top/app.twig b/packages/docs/components/ScrollAnimation/stories/offset-top/app.twig new file mode 100644 index 00000000..c478ec74 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-top/app.twig @@ -0,0 +1,24 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+

Top section offset

+

"start start / end start"

+
+
+
+ +
+ Scroll up ↑ +
From 90801e2ba2b240b58a9bb89c523638890d10c0da Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:22:57 +0100 Subject: [PATCH 35/42] Add offset-numbered example for ScrollAnimation Co-authored-by: Claude --- .../stories/offset-numbered/app.js | 18 ++++++++++++++ .../stories/offset-numbered/app.twig | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-numbered/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-numbered/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/offset-numbered/app.js b/packages/docs/components/ScrollAnimation/stories/offset-numbered/app.js new file mode 100644 index 00000000..95f8b216 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-numbered/app.js @@ -0,0 +1,18 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/offset-numbered/app.twig b/packages/docs/components/ScrollAnimation/stories/offset-numbered/app.twig new file mode 100644 index 00000000..f3d44161 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-numbered/app.twig @@ -0,0 +1,24 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+

Numbered offset

+

"0 0.8 / 1 0.2"

+
+
+
+ +
+ Scroll up ↑ +
From 8b5948f5bf66de0a76016816584a31eba860b55e Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:23:01 +0100 Subject: [PATCH 36/42] Add offset-percentage example for ScrollAnimation Co-authored-by: Claude --- .../stories/offset-percentage/app.js | 18 ++++++++++++++ .../stories/offset-percentage/app.twig | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-percentage/app.js create mode 100644 packages/docs/components/ScrollAnimation/stories/offset-percentage/app.twig diff --git a/packages/docs/components/ScrollAnimation/stories/offset-percentage/app.js b/packages/docs/components/ScrollAnimation/stories/offset-percentage/app.js new file mode 100644 index 00000000..95f8b216 --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-percentage/app.js @@ -0,0 +1,18 @@ +import { Base, createApp } from '@studiometa/js-toolkit'; +import { + ScrollAnimationTimeline, + ScrollAnimationTarget, + withScrollAnimationDebug, +} from '@studiometa/ui'; + +class App extends Base { + static config = { + name: 'App', + components: { + ScrollAnimationTimeline: withScrollAnimationDebug(ScrollAnimationTimeline), + ScrollAnimationTarget, + }, + }; +} + +export default createApp(App); diff --git a/packages/docs/components/ScrollAnimation/stories/offset-percentage/app.twig b/packages/docs/components/ScrollAnimation/stories/offset-percentage/app.twig new file mode 100644 index 00000000..64c7308a --- /dev/null +++ b/packages/docs/components/ScrollAnimation/stories/offset-percentage/app.twig @@ -0,0 +1,24 @@ +
+ Scroll down ↓ +
+ +
+
+
+
+

Percentage offset

+

"0 75% / 100% 25%"

+
+
+
+ +
+ Scroll up ↑ +
From c3849ba40eb7b013424c5e7aef42d2109ade6faf Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:23:05 +0100 Subject: [PATCH 37/42] Update offset examples documentation with detailed explanations - Separate each offset example into its own playground - Add plain English explanations for each offset value - Explain the offset format and its components Co-authored-by: Claude --- .../components/ScrollAnimation/examples.md | 137 +++++++++++++++++- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/packages/docs/components/ScrollAnimation/examples.md b/packages/docs/components/ScrollAnimation/examples.md index db232dc5..430bda53 100644 --- a/packages/docs/components/ScrollAnimation/examples.md +++ b/packages/docs/components/ScrollAnimation/examples.md @@ -144,22 +144,149 @@ Use the `withScrollAnimationDebug` decorator and the `debug` option to display v ## Offset -Use the `offset` option on `ScrollAnimationTimeline` to control when the animation starts and ends relative to the viewport. This is useful when a section is at the top, center, or bottom of a page. +Use the `offset` option on `ScrollAnimationTimeline` to control when the animation starts and ends relative to the viewport. + +The offset format is `" / "` where: +- `targetStart` / `targetEnd`: position on the timeline element (`start` = top, `center` = middle, `end` = bottom) +- `viewportStart` / `viewportEnd`: position on the viewport (`start` = top, `center` = middle, `end` = bottom) + +You can use named values (`start`, `center`, `end`), numbers between 0 and 1, or percentages. + +### Default offset + +**Value:** `"start end / end start"` (default) + +The animation **starts** when the **start (top) of the timeline** reaches the **end (bottom) of the viewport**. +The animation **ends** when the **end (bottom) of the timeline** reaches the **start (top) of the viewport**. + +This means the animation plays while any part of the timeline is visible in the viewport. + + + + + + +:::code-group + +<<< ./stories/offset-default/app.twig +<<< ./stories/offset-default/app.js + +::: + + + +### Centered offset + +**Value:** `"start center / end center"` + +The animation **starts** when the **start (top) of the timeline** reaches the **center of the viewport**. +The animation **ends** when the **end (bottom) of the timeline** reaches the **center of the viewport**. + +This means the animation plays while the timeline crosses the middle of the screen. + + + + + + +:::code-group + +<<< ./stories/offset-center/app.twig +<<< ./stories/offset-center/app.js + +::: + + + +### Top section offset + +**Value:** `"start start / end start"` + +The animation **starts** when the **start (top) of the timeline** reaches the **start (top) of the viewport**. +The animation **ends** when the **end (bottom) of the timeline** reaches the **start (top) of the viewport**. + +This means the animation plays while the timeline is pinned at the top of the viewport. + + + + + + +:::code-group + +<<< ./stories/offset-top/app.twig +<<< ./stories/offset-top/app.js + +::: + + + +### Numbered offset + +**Value:** `"0 0.8 / 1 0.2"` + +The animation **starts** when the **start (0 = top) of the timeline** reaches **80% (0.8) from the top of the viewport**. +The animation **ends** when the **end (1 = bottom) of the timeline** reaches **20% (0.2) from the top of the viewport**. + +Using numbers between 0 and 1 gives you precise control over the trigger positions. + + + + + + +:::code-group + +<<< ./stories/offset-numbered/app.twig +<<< ./stories/offset-numbered/app.js + +::: + + + +### Percentage offset + +**Value:** `"0 75% / 100% 25%"` + +The animation **starts** when the **start (0 = top) of the timeline** reaches **75% from the top of the viewport**. +The animation **ends** when the **end (100% = bottom) of the timeline** reaches **25% from the top of the viewport**. + +Percentages work the same as numbers but can be more readable. :::code-group -<<< ./stories/offset/app.twig -<<< ./stories/offset/app.js +<<< ./stories/offset-percentage/app.twig +<<< ./stories/offset-percentage/app.js ::: From 47f1606206ccf0d6ecbb3aa3a04052d5b7874f91 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 18:23:14 +0100 Subject: [PATCH 38/42] Add link to offset option in JS API from examples Co-authored-by: Claude --- packages/docs/components/ScrollAnimation/examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/components/ScrollAnimation/examples.md b/packages/docs/components/ScrollAnimation/examples.md index 430bda53..119d580e 100644 --- a/packages/docs/components/ScrollAnimation/examples.md +++ b/packages/docs/components/ScrollAnimation/examples.md @@ -144,7 +144,7 @@ Use the `withScrollAnimationDebug` decorator and the `debug` option to display v ## Offset -Use the `offset` option on `ScrollAnimationTimeline` to control when the animation starts and ends relative to the viewport. +Use the [`offset` option](/components/ScrollAnimation/js-api.html#offset) on `ScrollAnimationTimeline` to control when the animation starts and ends relative to the viewport. The offset format is `" / "` where: - `targetStart` / `targetEnd`: position on the timeline element (`start` = top, `center` = middle, `end` = bottom) From 479db13b07e574494bcbe3f1cfce8a59955027ba Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 21:31:31 +0000 Subject: [PATCH 39/42] Fix missing offset story reference in ScrollAnimation docs --- packages/docs/components/ScrollAnimation/js-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docs/components/ScrollAnimation/js-api.md b/packages/docs/components/ScrollAnimation/js-api.md index 38bd879a..dce2157e 100644 --- a/packages/docs/components/ScrollAnimation/js-api.md +++ b/packages/docs/components/ScrollAnimation/js-api.md @@ -149,18 +149,18 @@ Each point accepts the following values: :::code-group -<<< ./stories/offset/app.twig -<<< ./stories/offset/app.js +<<< ./stories/offset-default/app.twig +<<< ./stories/offset-default/app.js ::: From f3b316c557bae583188a07ac8d498d19f8185e03 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Mon, 26 Jan 2026 21:38:17 +0000 Subject: [PATCH 40/42] Add Codecov configuration to fix missing coverage issue --- codecov.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..0b50296a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,26 @@ +coverage: + status: + project: + default: + target: auto + threshold: 1% + informational: true + patch: + default: + target: auto + threshold: 1% + informational: true + +comment: + layout: "diff, flags, files" + behavior: default + require_changes: false + require_base: false + require_head: true + +ignore: + - "packages/docs/**" + - "packages/tests/**" + - "**/node_modules/**" + - "**/*.spec.ts" + - "**/*.test.ts" From f25c55ee8e6e59a53f6f42807dd4e0dcbd69e2fb Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Tue, 27 Jan 2026 09:35:43 +0100 Subject: [PATCH 41/42] Improve types --- .../withScrollAnimationDebug.ts | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/ui/ScrollAnimation/withScrollAnimationDebug.ts b/packages/ui/ScrollAnimation/withScrollAnimationDebug.ts index 5dca158c..2cc7ecab 100644 --- a/packages/ui/ScrollAnimation/withScrollAnimationDebug.ts +++ b/packages/ui/ScrollAnimation/withScrollAnimationDebug.ts @@ -1,4 +1,11 @@ -import type { Base, BaseProps, BaseConfig, ScrollInViewProps } from '@studiometa/js-toolkit'; +import type { + Base, + BaseProps, + BaseConfig, + BaseInterface, + BaseDecorator, + ScrollInViewProps, +} from '@studiometa/js-toolkit'; import { createElement } from '@studiometa/js-toolkit/utils'; /** @@ -283,6 +290,9 @@ export interface WithScrollAnimationDebugProps extends BaseProps { }; } +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WithScrollAnimationDebugInterface extends BaseInterface {} + /** * Add debug capabilities to a ScrollAnimationTimeline component. * @@ -301,12 +311,15 @@ export interface WithScrollAnimationDebugProps extends BaseProps { * } * ``` */ -export function withScrollAnimationDebug< - S extends Base, - T extends typeof Base, ->(BaseClass: T): T { - // @ts-expect-error - Dynamic class extension - return class extends BaseClass { +export function withScrollAnimationDebug( + BaseClass: typeof Base, +): BaseDecorator { + /** + * Class. + */ + class WithScrollAnimationDebug extends BaseClass< + T & WithScrollAnimationDebugProps + > { static config: BaseConfig = { ...BaseClass.config, name: BaseClass.config.name, @@ -354,7 +367,7 @@ export function withScrollAnimationDebug< /** * Initialize debug elements. */ - private __initDebug() { + __initDebug() { debugIdCounter += 1; debugInstanceCount += 1; this.__debugId = `timeline-${debugIdCounter}`; @@ -391,7 +404,7 @@ export function withScrollAnimationDebug< /** * Destroy debug elements. */ - private __destroyDebug() { + __destroyDebug() { if (this.__debugElements) { this.__debugElements.wrapper.remove(); this.__debugElements.progress.remove(); @@ -413,7 +426,7 @@ export function withScrollAnimationDebug< /** * Update debug progress. */ - private __updateDebug(props: ScrollInViewProps) { + __updateDebug(props: ScrollInViewProps) { if (!this.__debugElements) return; const { progressBar, progressText } = this.__debugElements; @@ -435,5 +448,8 @@ export function withScrollAnimationDebug< // @ts-expect-error - Calling parent method super.scrolledInView(props); } - }; + } + + // @ts-ignore + return WithScrollAnimationDebug; } From d1d1a288f41c1c9f857f0db13a2617b29b3a46f3 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Tue, 27 Jan 2026 09:45:47 +0100 Subject: [PATCH 42/42] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55c022d9..5464cf4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- **ScrollAnimation:** add a `withScrollAnimationDebug` decorator ([#494](https://github.com/studiometa/ui/pull/494)) + ### Changed -- **ScrollAnimation:** refactor exports ([#494](https://github.com/studiometa/ui/pull/494), [a5d0e29](https://github.com/studiometa/ui/commit/a5d0e29)) +- **ScrollAnimation:** refactor components into `ScrollAnimationTimeline` and `ScrollAnimationTarget` ([#441](https://github.com/studiometa/ui/issues/441) [#494](https://github.com/studiometa/ui/pull/494), [a5d0e29](https://github.com/studiometa/ui/commit/a5d0e29)) ## [v1.7.0](https://github.com/studiometa/ui/compare/1.6.0..1.7.0) (2025-11-11)