From 2eba526be05975ab0c3b8fa5befe9415b3dcc4f7 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Tue, 21 Apr 2026 16:57:29 +0200 Subject: [PATCH 1/8] Add shape constraints to Hoverable --- packages/tests/Hoverable/Hoverable.spec.ts | 44 ++++++++++++++++ packages/ui/Hoverable/Hoverable.ts | 58 +++++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/packages/tests/Hoverable/Hoverable.spec.ts b/packages/tests/Hoverable/Hoverable.spec.ts index 1ffa509d..67764e8d 100644 --- a/packages/tests/Hoverable/Hoverable.spec.ts +++ b/packages/tests/Hoverable/Hoverable.spec.ts @@ -73,6 +73,50 @@ describe('The Hoverable component', () => { expect(hoverable.props.y).toBe(0); }); + it('should constrain the target position to a circle when the circle shape is used', async () => { + const target = h('div', { dataRef: 'target' }); + const div = h('div', { dataOptionShape: 'circle' }, [target]); + const hoverable = new Hoverable(div); + const spy = vi.spyOn(hoverable, 'bounds', 'get'); + spy.mockImplementation(() => ({ + xMin: 0, + xMax: 100, + yMin: 0, + yMax: 100, + })); + await mount(hoverable); + + hoverable.movedrelative(pointerProgress(0, 0)); + expect(hoverable.props.x).toBeCloseTo(14.64466094067263); + expect(hoverable.props.y).toBeCloseTo(14.64466094067263); + + hoverable.movedrelative(pointerProgress(0.5, 0.5)); + expect(hoverable.props.x).toBe(50); + expect(hoverable.props.y).toBe(50); + }); + + it('should constrain the target position to an ellipse when the ellipse shape is used', async () => { + const target = h('div', { dataRef: 'target' }); + const div = h('div', { dataOptionShape: 'ellipse' }, [target]); + const hoverable = new Hoverable(div); + const spy = vi.spyOn(hoverable, 'bounds', 'get'); + spy.mockImplementation(() => ({ + xMin: 0, + xMax: 200, + yMin: 0, + yMax: 100, + })); + await mount(hoverable); + + hoverable.movedrelative(pointerProgress(0, 0)); + expect(hoverable.props.x).toBeCloseTo(29.28932188134526); + expect(hoverable.props.y).toBeCloseTo(14.64466094067263); + + hoverable.movedrelative(pointerProgress(0.5, 0.5)); + expect(hoverable.props.x).toBe(100); + expect(hoverable.props.y).toBe(50); + }); + it('should stop update x and y position when contained option is used and mouse position is out of bounds', async () => { const target = h('div', { dataRef: 'target' }); const div = h('div', { dataOptionContained: true }, [target]); diff --git a/packages/ui/Hoverable/Hoverable.ts b/packages/ui/Hoverable/Hoverable.ts index 4ba6e6ea..efdb2050 100644 --- a/packages/ui/Hoverable/Hoverable.ts +++ b/packages/ui/Hoverable/Hoverable.ts @@ -22,6 +22,10 @@ export interface HoverableProps extends BaseProps { * Wether to stop moving the target when the mouse is not over the root element or not. */ contained: boolean; + /** + * The bounding shape used to constrain the target movement. + */ + shape: 'rect' | 'circle' | 'ellipse'; }; } @@ -45,6 +49,10 @@ export class Hoverable extends withRelativePoin }, reversed: Boolean, contained: Boolean, + shape: { + type: String, + default: 'rect', + }, }, }; @@ -88,6 +96,47 @@ export class Hoverable extends withRelativePoin }; } + /** + * Constrain a position to the configured bounding shape. + */ + constrainPosition(x: number, y: number, bounds = this.bounds) { + const { shape } = this.$options; + + if (shape === 'circle' || shape === 'ellipse') { + const { xMin, xMax, yMin, yMax } = bounds; + const centerX = (xMin + xMax) / 2; + const centerY = (yMin + yMax) / 2; + const deltaX = x - centerX; + const deltaY = y - centerY; + const radiusX = (xMax - xMin) / 2; + const radiusY = (yMax - yMin) / 2; + const minRadius = Math.min(radiusX, radiusY); + const constrainedRadiusX = shape === 'circle' ? minRadius : radiusX; + const constrainedRadiusY = shape === 'circle' ? minRadius : radiusY; + + if (constrainedRadiusX <= 0 || constrainedRadiusY <= 0) { + return { x: centerX, y: centerY }; + } + + const ratio = + (deltaX * deltaX) / (constrainedRadiusX * constrainedRadiusX) + + (deltaY * deltaY) / (constrainedRadiusY * constrainedRadiusY); + + if (ratio <= 1) { + return { x, y }; + } + + const scale = 1 / Math.sqrt(ratio); + + return { + x: centerX + deltaX * scale, + y: centerY + deltaY * scale, + }; + } + + return { x, y }; + } + /** * Update props when the mouse moves. */ @@ -103,9 +152,14 @@ export class Hoverable extends withRelativePoin const from = reversed ? 1 : 0; const to = reversed ? 0 : 1; + const position = this.constrainPosition( + map(clamp01(x), from, to, bounds.xMin, bounds.xMax), + map(clamp01(y), from, to, bounds.yMin, bounds.yMax), + bounds, + ); - props.y = map(clamp01(y), from, to, bounds.yMin, bounds.yMax); - props.x = map(clamp01(x), from, to, bounds.xMin, bounds.xMax); + props.x = position.x; + props.y = position.y; } /** From 71fe012d3db6cc0acab1d8c92a06feb00fe7fbc4 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Tue, 21 Apr 2026 16:57:36 +0200 Subject: [PATCH 2/8] Document Hoverable shape options --- .../docs/components/Hoverable/examples.md | 2 +- packages/docs/components/Hoverable/js-api.md | 11 +- .../components/Hoverable/stories/app.twig | 111 +++++++++++++++--- 3 files changed, 108 insertions(+), 16 deletions(-) diff --git a/packages/docs/components/Hoverable/examples.md b/packages/docs/components/Hoverable/examples.md index 03a41479..6dcf2d42 100644 --- a/packages/docs/components/Hoverable/examples.md +++ b/packages/docs/components/Hoverable/examples.md @@ -6,7 +6,7 @@ title: Hoverable examples ## Default -In this example, we showcase the default behaviour as well as [the `reversed`](./js-api.md#reversed) and [`contained` options](./js-api.md#contained). +In this example, we showcase the default behaviour as well as [the `reversed`](./js-api.md#reversed), [`contained`](./js-api.md#contained) and [`shape`](./js-api.md#shape) options. -
+
-

Default

-
+ Default +

+
-
{% include '@ui/Figure/Figure.twig' with { src: 'https://picsum.photos/600/600', @@ -28,12 +33,16 @@
-

Reversed

-
+ Reversed +

+
-
{% include '@ui/Figure/Figure.twig' with { src: 'https://picsum.photos/600/600', @@ -55,14 +64,19 @@
-
+
-

Contained

-
+ Contained +

+
-
{% include '@ui/Figure/Figure.twig' with { src: 'https://picsum.photos/600/600', @@ -84,13 +98,17 @@
-

Reversed & Contained

-
+ Reversed & Contained +

+
-
{% include '@ui/Figure/Figure.twig' with { src: 'https://picsum.photos/600/600', @@ -112,4 +130,69 @@
+
+
+

+ Circle +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg rounded-full overflow-hidden' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
+
+

+ Ellipse +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg rounded-[9999px] overflow-hidden' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
+
From 1edf1e2b702b97b0d0d52c2ef1286e2eb16eaf58 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Tue, 21 Apr 2026 17:02:40 +0200 Subject: [PATCH 3/8] Fix Hoverable shape constraints for oversized targets --- packages/tests/Hoverable/Hoverable.spec.ts | 44 ++++++++++++++++++++++ packages/ui/Hoverable/Hoverable.ts | 13 ++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/tests/Hoverable/Hoverable.spec.ts b/packages/tests/Hoverable/Hoverable.spec.ts index 67764e8d..1b9f349b 100644 --- a/packages/tests/Hoverable/Hoverable.spec.ts +++ b/packages/tests/Hoverable/Hoverable.spec.ts @@ -95,6 +95,28 @@ describe('The Hoverable component', () => { expect(hoverable.props.y).toBe(50); }); + it('should constrain the target position to a circle when bounds are inverted', async () => { + const target = h('div', { dataRef: 'target' }); + const div = h('div', { dataOptionShape: 'circle' }, [target]); + const hoverable = new Hoverable(div); + const spy = vi.spyOn(hoverable, 'bounds', 'get'); + spy.mockImplementation(() => ({ + xMin: 20, + xMax: -20, + yMin: 20, + yMax: -20, + })); + await mount(hoverable); + + hoverable.movedrelative(pointerProgress(0, 0)); + expect(hoverable.props.x).toBeCloseTo(14.14213562373095); + expect(hoverable.props.y).toBeCloseTo(14.14213562373095); + + hoverable.movedrelative(pointerProgress(0.5, 0.5)); + expect(hoverable.props.x).toBe(0); + expect(hoverable.props.y).toBe(0); + }); + it('should constrain the target position to an ellipse when the ellipse shape is used', async () => { const target = h('div', { dataRef: 'target' }); const div = h('div', { dataOptionShape: 'ellipse' }, [target]); @@ -117,6 +139,28 @@ describe('The Hoverable component', () => { expect(hoverable.props.y).toBe(50); }); + it('should constrain the target position to an ellipse when bounds are inverted', async () => { + const target = h('div', { dataRef: 'target' }); + const div = h('div', { dataOptionShape: 'ellipse' }, [target]); + const hoverable = new Hoverable(div); + const spy = vi.spyOn(hoverable, 'bounds', 'get'); + spy.mockImplementation(() => ({ + xMin: 40, + xMax: -40, + yMin: 20, + yMax: -20, + })); + await mount(hoverable); + + hoverable.movedrelative(pointerProgress(0, 0)); + expect(hoverable.props.x).toBeCloseTo(28.2842712474619); + expect(hoverable.props.y).toBeCloseTo(14.14213562373095); + + hoverable.movedrelative(pointerProgress(0.5, 0.5)); + expect(hoverable.props.x).toBe(0); + expect(hoverable.props.y).toBe(0); + }); + it('should stop update x and y position when contained option is used and mouse position is out of bounds', async () => { const target = h('div', { dataRef: 'target' }); const div = h('div', { dataOptionContained: true }, [target]); diff --git a/packages/ui/Hoverable/Hoverable.ts b/packages/ui/Hoverable/Hoverable.ts index efdb2050..a69a0244 100644 --- a/packages/ui/Hoverable/Hoverable.ts +++ b/packages/ui/Hoverable/Hoverable.ts @@ -103,13 +103,16 @@ export class Hoverable extends withRelativePoin const { shape } = this.$options; if (shape === 'circle' || shape === 'ellipse') { - const { xMin, xMax, yMin, yMax } = bounds; - const centerX = (xMin + xMax) / 2; - const centerY = (yMin + yMax) / 2; + const minX = Math.min(bounds.xMin, bounds.xMax); + const maxX = Math.max(bounds.xMin, bounds.xMax); + const minY = Math.min(bounds.yMin, bounds.yMax); + const maxY = Math.max(bounds.yMin, bounds.yMax); + const centerX = (minX + maxX) / 2; + const centerY = (minY + maxY) / 2; const deltaX = x - centerX; const deltaY = y - centerY; - const radiusX = (xMax - xMin) / 2; - const radiusY = (yMax - yMin) / 2; + const radiusX = (maxX - minX) / 2; + const radiusY = (maxY - minY) / 2; const minRadius = Math.min(radiusX, radiusY); const constrainedRadiusX = shape === 'circle' ? minRadius : radiusX; const constrainedRadiusY = shape === 'circle' ? minRadius : radiusY; From 9f45a7bcc609ff12c766041ecbad451409f0c63d Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Tue, 21 Apr 2026 17:14:13 +0200 Subject: [PATCH 4/8] Split Hoverable examples into dedicated playgrounds --- .../docs/components/Hoverable/examples.md | 99 ++++++++++++++++++- .../components/Hoverable/stories/circle.twig | 26 +++++ .../Hoverable/stories/contained.twig | 26 +++++ .../components/Hoverable/stories/default.twig | 25 +++++ .../components/Hoverable/stories/ellipse.twig | 26 +++++ .../Hoverable/stories/reversed-contained.twig | 27 +++++ .../Hoverable/stories/reversed.twig | 26 +++++ 7 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 packages/docs/components/Hoverable/stories/circle.twig create mode 100644 packages/docs/components/Hoverable/stories/contained.twig create mode 100644 packages/docs/components/Hoverable/stories/default.twig create mode 100644 packages/docs/components/Hoverable/stories/ellipse.twig create mode 100644 packages/docs/components/Hoverable/stories/reversed-contained.twig create mode 100644 packages/docs/components/Hoverable/stories/reversed.twig diff --git a/packages/docs/components/Hoverable/examples.md b/packages/docs/components/Hoverable/examples.md index 6dcf2d42..22897b6a 100644 --- a/packages/docs/components/Hoverable/examples.md +++ b/packages/docs/components/Hoverable/examples.md @@ -6,11 +6,104 @@ title: Hoverable examples ## Default -In this example, we showcase the default behaviour as well as [the `reversed`](./js-api.md#reversed), [`contained`](./js-api.md#contained) and [`shape`](./js-api.md#shape) options. + + + + + +:::code-group + +<<< ./stories/default.twig +<<< ./stories/app.js + +::: + + + +## Reversed + + + + + + +:::code-group + +<<< ./stories/reversed.twig +<<< ./stories/app.js + +::: + + + +## Contained + + + + + + +:::code-group + +<<< ./stories/contained.twig +<<< ./stories/app.js + +::: + + + +## Reversed and contained + + + + + + +:::code-group + +<<< ./stories/reversed-contained.twig +<<< ./stories/app.js + +::: + + + +## Circle shape + + + + + + +:::code-group + +<<< ./stories/circle.twig +<<< ./stories/app.js + +::: + + + +## Ellipse shape @@ -18,7 +111,7 @@ In this example, we showcase the default behaviour as well as [the `reversed`](. :::code-group -<<< ./stories/app.twig +<<< ./stories/ellipse.twig <<< ./stories/app.js ::: diff --git a/packages/docs/components/Hoverable/stories/circle.twig b/packages/docs/components/Hoverable/stories/circle.twig new file mode 100644 index 00000000..d16ad6e3 --- /dev/null +++ b/packages/docs/components/Hoverable/stories/circle.twig @@ -0,0 +1,26 @@ +
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg rounded-full overflow-hidden' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
diff --git a/packages/docs/components/Hoverable/stories/contained.twig b/packages/docs/components/Hoverable/stories/contained.twig new file mode 100644 index 00000000..a8d0f3df --- /dev/null +++ b/packages/docs/components/Hoverable/stories/contained.twig @@ -0,0 +1,26 @@ +
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
diff --git a/packages/docs/components/Hoverable/stories/default.twig b/packages/docs/components/Hoverable/stories/default.twig new file mode 100644 index 00000000..56be7fb9 --- /dev/null +++ b/packages/docs/components/Hoverable/stories/default.twig @@ -0,0 +1,25 @@ +
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
diff --git a/packages/docs/components/Hoverable/stories/ellipse.twig b/packages/docs/components/Hoverable/stories/ellipse.twig new file mode 100644 index 00000000..5afd8ca5 --- /dev/null +++ b/packages/docs/components/Hoverable/stories/ellipse.twig @@ -0,0 +1,26 @@ +
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg rounded-[9999px] overflow-hidden' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
diff --git a/packages/docs/components/Hoverable/stories/reversed-contained.twig b/packages/docs/components/Hoverable/stories/reversed-contained.twig new file mode 100644 index 00000000..896bb384 --- /dev/null +++ b/packages/docs/components/Hoverable/stories/reversed-contained.twig @@ -0,0 +1,27 @@ +
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
diff --git a/packages/docs/components/Hoverable/stories/reversed.twig b/packages/docs/components/Hoverable/stories/reversed.twig new file mode 100644 index 00000000..68a06a1d --- /dev/null +++ b/packages/docs/components/Hoverable/stories/reversed.twig @@ -0,0 +1,26 @@ +
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
From a8fe7201cbbd5dbeb79310897baf2e2b3df90b26 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Tue, 21 Apr 2026 17:25:37 +0200 Subject: [PATCH 5/8] Improve Hoverable example demos --- .../docs/components/Hoverable/examples.md | 12 +++ .../components/Hoverable/stories/circle.twig | 89 +++++++++++++----- .../Hoverable/stories/contained.twig | 89 +++++++++++++----- .../components/Hoverable/stories/default.twig | 86 ++++++++++++----- .../components/Hoverable/stories/ellipse.twig | 89 +++++++++++++----- .../Hoverable/stories/reversed-contained.twig | 92 ++++++++++++++----- .../Hoverable/stories/reversed.twig | 89 +++++++++++++----- 7 files changed, 402 insertions(+), 144 deletions(-) diff --git a/packages/docs/components/Hoverable/examples.md b/packages/docs/components/Hoverable/examples.md index 22897b6a..fad744b5 100644 --- a/packages/docs/components/Hoverable/examples.md +++ b/packages/docs/components/Hoverable/examples.md @@ -6,6 +6,8 @@ title: Hoverable examples ## Default +Moves the target relative to the pointer inside rectangular bounds. This playground compares an oversized target with an undersized one. + -
-
- {% include '@ui/Figure/Figure.twig' with { - src: 'https://picsum.photos/600/600', - width: 600, - height: 600, - fit: 'cover', - absolute: true, - attr: { - data_option_enter_from: 'opacity-0' - }, - inner_attr: { - class: 'bg-vp-bg rounded-full overflow-hidden' - }, - img_attr: { - class: 'opacity-0 transform' - } - } only %} +
+
+
+

+ Oversized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg rounded-full overflow-hidden' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
+ +
+

+ Undersized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg rounded-full overflow-hidden' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
diff --git a/packages/docs/components/Hoverable/stories/contained.twig b/packages/docs/components/Hoverable/stories/contained.twig index a8d0f3df..cc24e3c6 100644 --- a/packages/docs/components/Hoverable/stories/contained.twig +++ b/packages/docs/components/Hoverable/stories/contained.twig @@ -1,26 +1,67 @@ -
-
-
- {% include '@ui/Figure/Figure.twig' with { - src: 'https://picsum.photos/600/600', - width: 600, - height: 600, - fit: 'cover', - absolute: true, - attr: { - data_option_enter_from: 'opacity-0' - }, - inner_attr: { - class: 'bg-vp-bg' - }, - img_attr: { - class: 'opacity-0 transform' - } - } only %} +
+
+
+

+ Oversized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
+ +
+

+ Undersized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
diff --git a/packages/docs/components/Hoverable/stories/default.twig b/packages/docs/components/Hoverable/stories/default.twig index 56be7fb9..e2d0df88 100644 --- a/packages/docs/components/Hoverable/stories/default.twig +++ b/packages/docs/components/Hoverable/stories/default.twig @@ -1,25 +1,65 @@ -
-
-
- {% include '@ui/Figure/Figure.twig' with { - src: 'https://picsum.photos/600/600', - width: 600, - height: 600, - fit: 'cover', - absolute: true, - attr: { - data_option_enter_from: 'opacity-0' - }, - inner_attr: { - class: 'bg-vp-bg' - }, - img_attr: { - class: 'opacity-0 transform' - } - } only %} +
+
+
+

+ Oversized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
+ +
+

+ Undersized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
diff --git a/packages/docs/components/Hoverable/stories/ellipse.twig b/packages/docs/components/Hoverable/stories/ellipse.twig index 5afd8ca5..56f98f57 100644 --- a/packages/docs/components/Hoverable/stories/ellipse.twig +++ b/packages/docs/components/Hoverable/stories/ellipse.twig @@ -1,26 +1,67 @@ -
-
-
- {% include '@ui/Figure/Figure.twig' with { - src: 'https://picsum.photos/600/600', - width: 600, - height: 600, - fit: 'cover', - absolute: true, - attr: { - data_option_enter_from: 'opacity-0' - }, - inner_attr: { - class: 'bg-vp-bg rounded-[9999px] overflow-hidden' - }, - img_attr: { - class: 'opacity-0 transform' - } - } only %} +
+
+
+

+ Oversized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg rounded-[9999px] overflow-hidden' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
+ +
+

+ Undersized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg rounded-[9999px] overflow-hidden' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
diff --git a/packages/docs/components/Hoverable/stories/reversed-contained.twig b/packages/docs/components/Hoverable/stories/reversed-contained.twig index 896bb384..c89a393d 100644 --- a/packages/docs/components/Hoverable/stories/reversed-contained.twig +++ b/packages/docs/components/Hoverable/stories/reversed-contained.twig @@ -1,27 +1,69 @@ -
-
-
- {% include '@ui/Figure/Figure.twig' with { - src: 'https://picsum.photos/600/600', - width: 600, - height: 600, - fit: 'cover', - absolute: true, - attr: { - data_option_enter_from: 'opacity-0' - }, - inner_attr: { - class: 'bg-vp-bg' - }, - img_attr: { - class: 'opacity-0 transform' - } - } only %} +
+
+
+

+ Oversized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
+ +
+

+ Undersized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
diff --git a/packages/docs/components/Hoverable/stories/reversed.twig b/packages/docs/components/Hoverable/stories/reversed.twig index 68a06a1d..b38edb97 100644 --- a/packages/docs/components/Hoverable/stories/reversed.twig +++ b/packages/docs/components/Hoverable/stories/reversed.twig @@ -1,26 +1,67 @@ -
-
-
- {% include '@ui/Figure/Figure.twig' with { - src: 'https://picsum.photos/600/600', - width: 600, - height: 600, - fit: 'cover', - absolute: true, - attr: { - data_option_enter_from: 'opacity-0' - }, - inner_attr: { - class: 'bg-vp-bg' - }, - img_attr: { - class: 'opacity-0 transform' - } - } only %} +
+
+
+

+ Oversized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
+ +
+

+ Undersized target +

+
+
+
+ {% include '@ui/Figure/Figure.twig' with { + src: 'https://picsum.photos/600/600', + width: 600, + height: 600, + fit: 'cover', + absolute: true, + attr: { + data_option_enter_from: 'opacity-0' + }, + inner_attr: { + class: 'bg-vp-bg' + }, + img_attr: { + class: 'opacity-0 transform' + } + } only %} +
+
+
From 7b0b53d2c84c823503d2346b538a27ead76329aa Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Tue, 21 Apr 2026 17:29:10 +0200 Subject: [PATCH 6/8] Refine Hoverable example descriptions --- packages/docs/components/Hoverable/examples.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/docs/components/Hoverable/examples.md b/packages/docs/components/Hoverable/examples.md index fad744b5..9b67ece2 100644 --- a/packages/docs/components/Hoverable/examples.md +++ b/packages/docs/components/Hoverable/examples.md @@ -6,7 +6,7 @@ title: Hoverable examples ## Default -Moves the target relative to the pointer inside rectangular bounds. This playground compares an oversized target with an undersized one. +Moves the target relative to the pointer inside rectangular bounds. Date: Tue, 21 Apr 2026 17:30:44 +0200 Subject: [PATCH 7/8] Document Hoverable constrainPosition method --- packages/docs/components/Hoverable/js-api.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/docs/components/Hoverable/js-api.md b/packages/docs/components/Hoverable/js-api.md index 9e7d9bd2..b515ec00 100644 --- a/packages/docs/components/Hoverable/js-api.md +++ b/packages/docs/components/Hoverable/js-api.md @@ -44,6 +44,16 @@ Use this option to constrain the target movement to an inscribed shape instead o The values used to calculate and render the position of the target element. +## Methods + +### `constrainPosition` + +- Signature: `(x: number, y: number, bounds = this.bounds) => { x: number, y: number }` + +Constrains the given position to the configured bounding shape. + +By default, this method supports the built-in `rect`, `circle` and `ellipse` shapes. You can override it in a custom component to implement more advanced constraints. + ## Getters ### `target` From 469eebbb12a3697203b07526481234f969ad3aaf Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Wed, 22 Apr 2026 14:54:27 +0200 Subject: [PATCH 8/8] Remove regex plugin from oxlint config --- .oxlintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 7678e9d0..da72c4e4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,6 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["vitest", "import", "promise", "regex", "jsdoc"], + "plugins": ["vitest", "import", "promise", "jsdoc"], "rules": { "func-style": ["warn", "declaration"], "no-floating-promises": "allow",