@supermousejs/core 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @supermousejs/core
2
2
 
3
+ ## 2.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 1048a35: Fixed repository metadata pointing to @supermousejs/core instead of their respective directory in package.json
8
+ - be2d65d: Fix `hideOnLeave` bug where stage didn't fade out on window leave in gecko-based browsers
9
+
3
10
  ## 2.4.0
4
11
 
5
12
  ### Minor Changes
package/README.md CHANGED
@@ -1,30 +1,334 @@
1
- # @supermousejs/core
1
+ # Supermouse.js - TypeScript cursor engine
2
2
 
3
- The high-performance runtime engine for **Supermouse v2**.
3
+ [![npm version](https://img.shields.io/npm/v/@supermousejs/core.svg?style=flat-square)](https://www.npmjs.com/package/@supermousejs/core)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@supermousejs/core.svg?style=flat-square)](https://www.npmjs.com/package/@supermousejs/core)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
6
+ ![npm bundle size](https://img.shields.io/bundlephobia/minzip/%40supermousejs%2Fcore?label=minzip)
4
7
 
5
- It separates cursor **intent** (input, physics, logic) from **rendering** (visuals), exposing a deterministic plugin pipeline.
8
+ <div align="center">
9
+ <img src="https://raw.githubusercontent.com/Whitestar14/supermouse-js/main/docs/public/favicon.svg" width="180" height="180">
10
+ </div>
11
+
12
+ Supermouse.js is a physics-based custom cursor engine written in TypeScript for the web that allows you to install composable cursors and cursor effects, or write them yourself, in your code. It is highly performant, written in TypeScript from scratch, has zero dependencies, and uses a plugin system so the core remains lean.
13
+
14
+ [Documentation](https://supermouse.js.org) | [Examples](https://supermouse.js.org/docs/guide/cookbook) | [License](#license)
6
15
 
7
16
  ## Installation
8
17
 
9
18
  ```bash
10
19
  pnpm add @supermousejs/core
20
+
21
+ npm install @supermousejs/core
22
+ ```
23
+
24
+ Install only the plugins you want:
25
+
26
+ ```bash
27
+ pnpm add @supermousejs/dot @supermousejs/ring
28
+
29
+ npm install @supermousejs/dot @supermousejs/ring
11
30
  ```
12
31
 
13
- ## Basic Usage
32
+ ## Usage
33
+
34
+ Supermouse itself does not render a cursor, as it is not a component system. Nor does it render a default stub.
35
+
36
+ The core is essentially a headless system that provides pointer tracking, state, physics, and a plugin lifecycle. Plugins automatically use the internal Supermouse engine to render to the DOM, but you can write a renderer yourself like in the example below:
14
37
 
15
38
  ```ts
16
39
  import { Supermouse } from "@supermousejs/core";
17
- // Import visuals separately to keep bundle size low
40
+
41
+ const mouse = new Supermouse({
42
+ smoothness: 0.15
43
+ });
44
+
45
+ const dot = document.createElement("div");
46
+
47
+ Object.assign(dot.style, {
48
+ width: "8px",
49
+ height: "8px",
50
+ borderRadius: "50%",
51
+ background: "red",
52
+ position: "absolute",
53
+ transform: "translate(-50%, -50%)",
54
+ pointerEvents: "none"
55
+ });
56
+
57
+ mouse.container.appendChild(dot);
58
+
59
+ function render() {
60
+ const { smooth } = mouse.state;
61
+
62
+ dot.style.transform = `translate3d(${smooth.x}px, ${smooth.y}px, 0) translate(-50%, -50%)`;
63
+
64
+ requestAnimationFrame(render);
65
+ }
66
+
67
+ render();
68
+ ```
69
+
70
+ It is best to avoid writing this way though, as defining another `requestAnimationFrame` besides Supermouse's internal one will leave you with two rAF loops. Use Supermouse's plugin interface instead:
71
+
72
+ ```ts
73
+ import { Supermouse } from "@supermousejs/core";
74
+
75
+ const dot = document.createElement("div");
76
+
77
+ Object.assign(dot.style, {
78
+ width: "8px",
79
+ height: "8px",
80
+ borderRadius: "50%",
81
+ background: "red",
82
+ position: "absolute",
83
+ transform: "translate(-50%, -50%)",
84
+ pointerEvents: "none"
85
+ });
86
+
87
+ const redDot = {
88
+ name: "red-dot",
89
+ install(mouse) {
90
+ mouse.container.appendChild(dot);
91
+ },
92
+ update(mouse) {
93
+ const { smooth } = mouse.state;
94
+
95
+ dot.style.transform = `translate3d(${smooth.x}px, ${smooth.y}px, 0) translate(-50%, -50%)`;
96
+ },
97
+ destroy() {
98
+ dot.remove();
99
+ }
100
+ };
101
+
102
+ const mouse = new Supermouse({
103
+ plugins: [redDot]
104
+ });
105
+ ```
106
+
107
+ Now your dot has all the capabilities of Supermouse (the native cursor automatically hidden, the input normalized, hover detection and accessibility baked in, and the red dot part of the plugin lifecycle). However, if your goal is a simple red dot on your webpage, Supermouse might be overkill, even though the core is designed to be lightweight. For simple effects, plain HTML/CSS and JavaScript may suffice.
108
+
109
+ Supermouse's advantages become apparent when you need to handle increasingly complex effects with granularity. For example, adding a `Ring` to the cursor can be done declaratively:
110
+
111
+ ```ts
18
112
  import { Dot } from "@supermousejs/dot";
113
+ import { Ring } from "@supermousejs/ring";
114
+
115
+ const app = new Supermouse({
116
+ plugins: [Dot({ size: 8 }), Ring({ size: 24 })]
117
+ });
118
+ ```
119
+
120
+ You can also chain plugin instantiation imperatively with `.use`:
121
+
122
+ ```ts
123
+ app.use(Dot({ size: 8 }))
124
+
125
+ if (someEffect) {
126
+ app.use(Effect1({...})).use(Effect2({...}))
127
+ }
128
+ ```
129
+
130
+ Read more about plugins and how to write them [here](https://github.com/Whitestar14/supermouse-js/blob/main/PLUGINS.md).
131
+
132
+ ## Options
133
+
134
+ | Option | Default | Description |
135
+ | --------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
136
+ | `smoothness` | `0.15` | Lower values for a smoother follow |
137
+ | `hoverSelectors` | `["a", "button", "input", "textarea", "[data-hover]", "[data-cursor]"]` | Selectors that set `state.isHover` to `true`. |
138
+ | `enableTouch` | `false` | Whether touch events move the cursor. |
139
+ | `autoDisableOnMobile` | `true` | Disables the custom cursor on devices with a coarse pointer unless `enableTouch` is `true`. |
140
+ | `cursor` | `"auto"` | Cursor mode: `'auto'`, `'custom'`, `'native'`, or `'both'`. Replaces the old `hideCursor` and `ignoreOnNative`. |
141
+ | `hideOnLeave` | `true` | Hides the cursor when the pointer leaves the browser window. |
142
+ | `container` | `document.body` | The area where the instance is active. |
143
+ | `zIndex` | `9999` | The stack order of the cursor stage. Increase this if overlays cover the cursor. |
144
+ | `dataPrefix` | `"supermouse"` | A prefix for `data-*` attributes. Prevents conflicts between instances. |
145
+ | `rules` | — | A map of selectors to interaction data. The core adds this data to `state.interaction` on hover. |
146
+ | `plugins` | — | Plugins to install when you create the instance. |
147
+ | `autoStart` | `true` | Set to `false` to prevent automatic start. Call `.start()` when ready. |
148
+
149
+ ### Cursor Modes
150
+
151
+ Prior to v2.4, earlier versions used two separate options, `hideCursor` and `ignoreOnNative`, to control when the native cursor should be hidden or shown. In v2.4, these have been combined into a single **`cursor`** option that decides both behaviors at once.
152
+
153
+ ```ts
154
+ const app = new Supermouse({
155
+ cursor: "auto"
156
+ });
157
+ ```
158
+
159
+ There are four modes:
160
+
161
+ - **`"auto"`** (default) – Supermouse looks at whatever is under the pointer and makes a reasonable choice. Over buttons, links, inputs, and other native controls, the native OS cursor appears; everywhere else, your custom cursor takes over. This is the best starting point and for most projects, it's what you're going to use without extra configuration.
162
+
163
+ - **`"custom"`** – The custom cursor is always shown, and the native cursor is always hidden. This is the mode you'd use when you want complete visual control and are confident your custom cursor handles all interactions gracefully.
164
+
165
+ - **`"native"`** – The native cursor is always visible, and the custom cursor stays hidden. Useful when you temporarily want to give full control back to the OS.
166
+
167
+ - **`"both"`** – Both the native cursor and the custom cursor are visible at the same time. This is particularly useful for effects that surround the native pointer such as a ring that highlights the native arrow, or a trail that follows the real cursor.
168
+
169
+ You can switch modes at any time with:
170
+
171
+ ```ts
172
+ mouse.setCursor("both");
173
+ ```
174
+
175
+ If you’re coming from an older version and relied on those options, the following table might help you translate:
176
+
177
+ | Old behaviour | New `cursor` value |
178
+ | -------------------------------------------- | ---------------------------------------------------------- |
179
+ | `hideCursor: true`, `ignoreOnNative: "auto"` | `"auto"` |
180
+ | `hideCursor: true`, `ignoreOnNative: null` | `"custom"` |
181
+ | `hideCursor: false` | `"both"` or `"native"` depending on the rest of your setup |
182
+
183
+ The rest of the documentation uses `cursor` consistently. If you have questions about a specific migration case, the [options documentation](https://supermouse.js.org/advanced/api) covers the details.
184
+
185
+ ### Containers
186
+
187
+ By default, Supermouse appends the cursor stage to `document.body` and uses global event listeners. You can scope an instance to a specific container:
188
+
189
+ ```ts
190
+ const modal = document.getElementById("my-modal");
19
191
 
20
192
  const app = new Supermouse({
21
- smoothness: 0.15, // 0-1 (Physics damping)
22
- hideCursor: true // Auto-hide native cursor
193
+ container: modal,
194
+ cursor: "custom"
195
+ });
196
+ ```
197
+
198
+ This allows multiple independent instances on the same page. CSS and cursor‑hiding rules are scoped to avoid leakage. For example:
199
+
200
+ ```ts
201
+ // Main page cursor
202
+ const app1 = new Supermouse({
203
+ container: document.body,
204
+ cursor: "custom"
23
205
  });
206
+ app1.use(Dot({ color: "red" }));
24
207
 
25
- app.use(Dot({ size: 8, color: "#f59e0b" }));
208
+ // Modal cursor
209
+ const app2 = new Supermouse({
210
+ container: document.getElementById("modal"),
211
+ cursor: "both"
212
+ });
213
+ app2.use(Dot({ color: "blue" }));
214
+ ```
215
+
216
+ ### Forcing Native Cursor on an Element
217
+
218
+ When `cursor` is `"auto"`, Supermouse automatically detects when to show the native OS cursor. To force native cursor on an element and its descendants, add the attribute `data-supermouse-ignore`:
219
+
220
+ ```html
221
+ <div data-supermouse-ignore>This area will show the native cursor</div>
222
+ ```
223
+
224
+ This only works when `cursor` is `"auto"`.
225
+
226
+ > **Note:** Custom cursors often break usability on native controls (inputs, text selection, drag handles). The `"auto"` cursor mode briefly yields control back to the OS cursor for these interactions, ensuring accessibility.
227
+
228
+ ### Data Attributes
229
+
230
+ Some plugins can be reactive to HTML data attributes. For example, `Dot` watches for `data-supermouse-color`:
231
+
232
+ ```html
233
+ <div data-supermouse-color="#fff000">...</div>
234
+ ```
235
+
236
+ The `dataPrefix` option can customize this:
237
+
238
+ ```ts
239
+ const app = new Supermouse({ dataPrefix: "sm" });
26
240
  ```
27
241
 
28
- ## Documentation
242
+ ```html
243
+ <div data-sm-stick>Sticky Zone</div>
244
+ ```
245
+
246
+ `data-supermouse-ignore` becomes `data-sm-ignore`.
247
+
248
+ ### Rules
249
+
250
+ Use `rules` to define interaction data without writing many data attributes:
251
+
252
+ ```ts
253
+ const app = new Supermouse({
254
+ rules: {
255
+ ".primary-action": {
256
+ magnetic: true,
257
+ distance: 200,
258
+ color: "red",
259
+ text: "Click!",
260
+ scale: 1.5
261
+ }
262
+ }
263
+ });
264
+ ```
265
+
266
+ When both rules and data attributes apply to the same element, the HTML `data-supermouse-*` attribute wins for that property.
267
+
268
+ ## API
269
+
270
+ ```ts
271
+ const mouse = new Supermouse(options?);
272
+
273
+ // Mouse state read inside plugins
274
+ mouse.state
275
+ // All options with defaults applied
276
+ mouse.options
277
+ // The DOM element the plugins render into
278
+ mouse.stage
279
+
280
+ // Whether the instance is currently processing input
281
+ mouse.isEnabled
282
+
283
+ // Start processing input and re-apply cursor state
284
+ mouse.enable()
285
+ // Stop processing input, restore native cursor, reset state
286
+ mouse.disable()
287
+ // Pause input and hide the stage without destroying the instance
288
+ mouse.suspend()
289
+ // Resume input and show the stage
290
+ mouse.resume()
291
+
292
+ // Set cursor mode: "auto" | "custom" | "native" | "both"
293
+ mouse.setCursor(mode)
294
+
295
+ // Install a plugin
296
+ mouse.use(plugin)
297
+ // Return a plugin by name
298
+ mouse.getPlugin(name)
299
+ // Enable a disabled plugin
300
+ mouse.enablePlugin(name)
301
+ // Disable a plugin
302
+ mouse.disablePlugin(name)
303
+ // Toggle a plugin's enabled state
304
+ mouse.togglePlugin(name)
305
+
306
+ // Add a selector at runtime. Whenever the cursor hovers over this selector, Supermouse detects it and hides the native cursor for it
307
+ mouse.registerHoverTarget(selector)
308
+
309
+ // Start the animation loop if stopped
310
+ mouse.start()
311
+ // Advance one frame manually
312
+ mouse.step(time)
313
+
314
+ // Destroy Supermouse and plugin instances
315
+ mouse.destroy()
316
+ ```
317
+
318
+ ## Browser support
319
+
320
+ Supermouse.js supports all modern browsers.
321
+
322
+ ## Contributing
323
+
324
+ Any bug fixes, performance / docs improvements are welcome. Before adding new effects or features to core, please read [how to contribute](https://github.com/Whitestar14/supermouse-js/blob/main/CONTRIBUTING.md).
325
+
326
+ ## Credits
327
+
328
+ This project was heavily inspired by [Pointer.js](https://github.com/mageowl/pointer.js) and the cursor effects gallery, [Curzr](https://github.com/fuzionix/curzr).
329
+
330
+ ## License
331
+
332
+ MIT
29
333
 
30
- Full documentation and interactive playground available at [supermouse](https://supermouse.js.org) or [check out the repo](https://github.com/Whitestar14/supermouse-js).
334
+ Built and maintained by [Whitestar14](https://github.com/Whitestar14)
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- const c = "2.4.0";
1
+ const c = "2.4.1";
2
2
  function p(o, t, e) {
3
3
  return o + (t - o) * e;
4
4
  }
@@ -170,6 +170,13 @@ class v {
170
170
  const e = t.target, s = t.relatedTarget;
171
171
  this.isOutsideContainer(e) || ((e === this.state.hoverTarget || e.contains(this.state.hoverTarget)) && (!s || !this.state.hoverTarget?.contains(s)) && (this.state.isHover = !1, this.state.hoverTarget = null), this.nativeTarget && (e === this.nativeTarget || e.contains(this.nativeTarget)) && (!s || !this.nativeTarget.contains(s)) && (this.state.isNative = !1, this.nativeTarget = null), e === this.currentTarget && (this.currentTarget = null, this.lastParsedTarget = null, this.matchedRules = []));
172
172
  };
173
+ /**
174
+ * Handles the case where the pointer leaves the entire document.
175
+ * This is more reliable than the `mouseleave` event on `document`.
176
+ */
177
+ handleDocumentMouseOut = (t) => {
178
+ this.isEnabled && t.relatedTarget === null && this.options.hideOnLeave && this.handleWindowLeave();
179
+ };
173
180
  handleWindowLeave = () => {
174
181
  this.options.hideOnLeave && (this.state.hasReceivedInput = !1, this.state.pointer = { ...h });
175
182
  };
@@ -184,7 +191,7 @@ class v {
184
191
  const { signal: t } = this.abortController;
185
192
  window.addEventListener("pointermove", this.handleMove, { passive: !0, signal: t }), window.addEventListener("pointerdown", this.handleDown, { passive: !0, signal: t }), window.addEventListener("pointerup", this.handleUp, { signal: t });
186
193
  const s = !this.options.container || this.options.container === document.body ? document : this.options.container;
187
- s.addEventListener("mouseover", this.handleMouseOver, { signal: t }), s.addEventListener("mouseout", this.handleMouseOut, { signal: t }), document.addEventListener("mouseleave", this.handleWindowLeave, { signal: t });
194
+ s.addEventListener("mouseover", this.handleMouseOver, { signal: t }), s.addEventListener("mouseout", this.handleMouseOut, { signal: t }), document.addEventListener("mouseout", this.handleDocumentMouseOut, { signal: t });
188
195
  }
189
196
  destroy() {
190
197
  this.abortController.abort(), this.resizeObserver?.disconnect();
package/dist/index.umd.js CHANGED
@@ -1,4 +1,4 @@
1
- (function(r,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(r=typeof globalThis<"u"?globalThis:r||self,l(r.SupermouseCore={}))})(this,(function(r){"use strict";const l="2.4.0";function v(a,t,e){return a+(t-a)*e}function c(a,t,e,s){return v(a,t,1-Math.exp(-e*s))}const u={x:-100,y:-100},b=new Set(["input","textarea","select"]),y=new Set(["default","auto","pointer","none","inherit","grab","grabbing"]),d=["a","button","input","textarea","[data-hover]","[data-cursor]"];class p{constructor(t,e,s,i){this.state=t,this.options=e,this.getHoverSelector=s,this.onEnableChange=i,this.dataPrefix=this.options.dataPrefix??"supermouse",this.normalizedDataPrefix=this.dataPrefix.toLowerCase(),this.ignoreAttribute=`data-${this.dataPrefix}-ignore`,this.ruleEntries=this.options.rules?Object.entries(this.options.rules):[],this.checkDeviceCapability(),this.checkMotionPreference(),this.setupContainerRectTracking(),this.bindEvents()}state;options;getHoverSelector;onEnableChange;mediaQueryList;motionQuery;dataPrefix;normalizedDataPrefix;ignoreAttribute;abortController=new AbortController;nativeTarget=null;hasSeenPointer=!1;isEnabled=!0;containerRect=null;resizeObserver;matchedRules=[];lastParsedTarget=null;ruleEntries;currentTarget=null;checkDeviceCapability(){this.options.autoDisableOnMobile&&(this.mediaQueryList=window.matchMedia("(pointer: fine)"),this.updateEnabledState(this.mediaQueryList.matches),this.mediaQueryList.addEventListener("change",t=>this.updateEnabledState(t.matches),{signal:this.abortController.signal}))}checkMotionPreference(){this.motionQuery=window.matchMedia("(prefers-reduced-motion: reduce)"),this.state.reducedMotion=this.motionQuery.matches,this.motionQuery.addEventListener("change",t=>{this.state.reducedMotion=t.matches},{signal:this.abortController.signal})}updateEnabledState(t){this.isEnabled=t,this.onEnableChange(t)}setupContainerRectTracking(){const t=this.options.container;if(!t||t===document.body)return;const e=()=>{this.containerRect=t.getBoundingClientRect()};e(),window.addEventListener("resize",e,{signal:this.abortController.signal}),window.addEventListener("scroll",e,{passive:!0,signal:this.abortController.signal}),typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(e),this.resizeObserver.observe(t))}parseDOMInteraction(t){if(t!==this.lastParsedTarget){this.lastParsedTarget=t,this.matchedRules=[];for(const[i,n]of this.ruleEntries)this.matchesSelector(t,i)&&this.matchedRules.push({selector:i,rules:n})}const e={};for(const{rules:i}of this.matchedRules)try{const n=typeof i=="function"?i(t):i;if(!n||typeof n!="object")continue;for(const[o,h]of Object.entries(n))e[o]=typeof h=="function"?h(t):h}catch(n){console.error("[Supermouse] Rule threw during evaluation:",n)}const s=this.normalizedDataPrefix;for(const i in t.dataset){if(!i.toLowerCase().startsWith(s))continue;const n=i.slice(s.length);if(!n)continue;const o=t.dataset[i];e[n[0].toLowerCase()+n.slice(1)]=o===""?!0:o}this.state.interaction=e}matchesSelector(t,e){try{if(!/\s/.test(e.trim()))return t.matches(e)}catch{}const s=e.trim().split(/\s+/);if(s.length<2)return!1;const i=s.pop(),n=s.join(" ");if(!i||!n)return!1;try{return t.matches(i)&&!!t.closest(n)}catch{return!1}}isOutsideContainer(t){const{container:e}=this.options;return!!e&&e!==document.body&&!e.contains(t)}resolveComputedCursor(t){return window.getComputedStyle(t).cursor}handleMove=t=>{if(this.options.autoDisableOnMobile&&t.pointerType==="touch"&&!this.options.enableTouch)return;let e=t.clientX,s=t.clientY;const i=this.options.container;i&&this.containerRect&&i!==document.body&&(e-=this.containerRect.left,s-=this.containerRect.top),this.state.pointer.x=e,this.state.pointer.y=s,this.hasSeenPointer=!0,this.isEnabled&&(this.state.hasReceivedInput||(this.state.hasReceivedInput=!0,this.state.target.x=this.state.smooth.x=e,this.state.target.y=this.state.smooth.y=s))};handleDown=()=>{this.isEnabled&&(this.state.isDown=!0)};handleUp=()=>{this.isEnabled&&(this.state.isDown=!1)};handleMouseOver=t=>{if(!this.isEnabled)return;const e=t.target;if(this.isOutsideContainer(e))return;if(this.state.cursorMode==="auto"&&e.closest(`[${this.ignoreAttribute}]`)){this.state.isHover=!1,this.state.hoverTarget=null,this.state.interaction={},this.currentTarget=null,this.lastParsedTarget=null,this.matchedRules=[],this.state.isNative=!0,this.nativeTarget=e;return}if(this.state.isNative=!1,this.nativeTarget=null,this.currentTarget=e,this.parseDOMInteraction(e),this.state.cursorMode!=="auto"){const o=e.closest(this.getHoverSelector());o&&(this.state.isHover=!0,this.state.hoverTarget=o);return}const s=e.closest(this.getHoverSelector());s&&(this.state.isHover=!0,this.state.hoverTarget=s);const i=b.has(e.localName)||e.isContentEditable,n=!y.has(this.resolveComputedCursor(e));(i||n)&&(this.state.isNative=!0,this.nativeTarget=e)};handleMouseOut=t=>{if(!this.isEnabled)return;const e=t.target,s=t.relatedTarget;this.isOutsideContainer(e)||((e===this.state.hoverTarget||e.contains(this.state.hoverTarget))&&(!s||!this.state.hoverTarget?.contains(s))&&(this.state.isHover=!1,this.state.hoverTarget=null),this.nativeTarget&&(e===this.nativeTarget||e.contains(this.nativeTarget))&&(!s||!this.nativeTarget.contains(s))&&(this.state.isNative=!1,this.nativeTarget=null),e===this.currentTarget&&(this.currentTarget=null,this.lastParsedTarget=null,this.matchedRules=[]))};handleWindowLeave=()=>{this.options.hideOnLeave&&(this.state.hasReceivedInput=!1,this.state.pointer={...u})};clearHover(){this.state.isHover=!1,this.state.hoverTarget=null,this.state.isNative=!1,this.nativeTarget=null,this.state.interaction={},this.currentTarget=null,this.lastParsedTarget=null,this.matchedRules=[]}getCurrentTarget(){return this.currentTarget}bindEvents(){const{signal:t}=this.abortController;window.addEventListener("pointermove",this.handleMove,{passive:!0,signal:t}),window.addEventListener("pointerdown",this.handleDown,{passive:!0,signal:t}),window.addEventListener("pointerup",this.handleUp,{signal:t});const s=!this.options.container||this.options.container===document.body?document:this.options.container;s.addEventListener("mouseover",this.handleMouseOver,{signal:t}),s.addEventListener("mouseout",this.handleMouseOut,{signal:t}),document.addEventListener("mouseleave",this.handleWindowLeave,{signal:t})}destroy(){this.abortController.abort(),this.resizeObserver?.disconnect()}}let C=0;class g{constructor(t=document.body,e=9999){if(this.container=t,this.zIndex=e,!t||!(t instanceof HTMLElement))throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);t.isConnected||console.warn("[Supermouse] container is not attached to the document — stage sizing/positioning will be wrong until it is.");const s=C++;this.id=`supermouse-style-${s}`,this.scopeClass=`supermouse-scope-${s}`,this.hideClass=`supermouse-hide-${s}`;const i=t===document.body;if(this.element=document.createElement("div"),Object.assign(this.element.style,{position:i?"fixed":"absolute",inset:"0px",pointerEvents:"none",zIndex:String(this.zIndex),opacity:"1",transition:"opacity 0.15s ease"}),!i){const n=window.getComputedStyle(t);this.originalContainerPosition=n.position,n.position==="static"&&(t.style.position="relative")}this.originalContainerCursor=t.style.cursor,t.appendChild(this.element),this.styleTag=document.createElement("style"),this.styleTag.id=this.id,document.head.appendChild(this.styleTag),this.container.classList.add("supermouse-scope",this.scopeClass),this.updateCursorCSS()}container;zIndex;element;styleTag;id;scopeClass;hideClass;currentCursorState=null;originalContainerPosition="";originalContainerCursor="";selectors=new Set(["a","button","input","textarea","select",'[role="button"]',"[tabindex]"]);addSelectors(t){let e=!1;for(const s of t)s.split(",").forEach(i=>{const n=i.trim();n&&!this.selectors.has(n)&&(this.selectors.add(n),e=!0)});e&&this.updateCursorCSS()}addSelector(t){this.addSelectors([t])}setVisibility(t){this.element.style.opacity=t?"1":"0"}setNativeCursor(t){t!==this.currentCursorState&&(this.currentCursorState=t,this.container.classList.toggle(this.hideClass,t==="none"),this.container.style.cursor=t==="none"?"none":this.originalContainerCursor)}updateCursorCSS(){const t=Array.from(this.selectors);if(t.length===0){this.styleTag.innerText="";return}const e=`:not(.${this.scopeClass} .supermouse-scope):not(.${this.scopeClass} .supermouse-scope *)`,s=h=>`.${this.scopeClass}.${this.hideClass} ${h}${e} { cursor: none !important; }`,i=t.map(s).join(`
1
+ (function(r,l){typeof exports=="object"&&typeof module<"u"?l(exports):typeof define=="function"&&define.amd?define(["exports"],l):(r=typeof globalThis<"u"?globalThis:r||self,l(r.SupermouseCore={}))})(this,(function(r){"use strict";const l="2.4.1";function v(a,t,e){return a+(t-a)*e}function c(a,t,e,s){return v(a,t,1-Math.exp(-e*s))}const u={x:-100,y:-100},b=new Set(["input","textarea","select"]),y=new Set(["default","auto","pointer","none","inherit","grab","grabbing"]),d=["a","button","input","textarea","[data-hover]","[data-cursor]"];class p{constructor(t,e,s,i){this.state=t,this.options=e,this.getHoverSelector=s,this.onEnableChange=i,this.dataPrefix=this.options.dataPrefix??"supermouse",this.normalizedDataPrefix=this.dataPrefix.toLowerCase(),this.ignoreAttribute=`data-${this.dataPrefix}-ignore`,this.ruleEntries=this.options.rules?Object.entries(this.options.rules):[],this.checkDeviceCapability(),this.checkMotionPreference(),this.setupContainerRectTracking(),this.bindEvents()}state;options;getHoverSelector;onEnableChange;mediaQueryList;motionQuery;dataPrefix;normalizedDataPrefix;ignoreAttribute;abortController=new AbortController;nativeTarget=null;hasSeenPointer=!1;isEnabled=!0;containerRect=null;resizeObserver;matchedRules=[];lastParsedTarget=null;ruleEntries;currentTarget=null;checkDeviceCapability(){this.options.autoDisableOnMobile&&(this.mediaQueryList=window.matchMedia("(pointer: fine)"),this.updateEnabledState(this.mediaQueryList.matches),this.mediaQueryList.addEventListener("change",t=>this.updateEnabledState(t.matches),{signal:this.abortController.signal}))}checkMotionPreference(){this.motionQuery=window.matchMedia("(prefers-reduced-motion: reduce)"),this.state.reducedMotion=this.motionQuery.matches,this.motionQuery.addEventListener("change",t=>{this.state.reducedMotion=t.matches},{signal:this.abortController.signal})}updateEnabledState(t){this.isEnabled=t,this.onEnableChange(t)}setupContainerRectTracking(){const t=this.options.container;if(!t||t===document.body)return;const e=()=>{this.containerRect=t.getBoundingClientRect()};e(),window.addEventListener("resize",e,{signal:this.abortController.signal}),window.addEventListener("scroll",e,{passive:!0,signal:this.abortController.signal}),typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(e),this.resizeObserver.observe(t))}parseDOMInteraction(t){if(t!==this.lastParsedTarget){this.lastParsedTarget=t,this.matchedRules=[];for(const[i,n]of this.ruleEntries)this.matchesSelector(t,i)&&this.matchedRules.push({selector:i,rules:n})}const e={};for(const{rules:i}of this.matchedRules)try{const n=typeof i=="function"?i(t):i;if(!n||typeof n!="object")continue;for(const[o,h]of Object.entries(n))e[o]=typeof h=="function"?h(t):h}catch(n){console.error("[Supermouse] Rule threw during evaluation:",n)}const s=this.normalizedDataPrefix;for(const i in t.dataset){if(!i.toLowerCase().startsWith(s))continue;const n=i.slice(s.length);if(!n)continue;const o=t.dataset[i];e[n[0].toLowerCase()+n.slice(1)]=o===""?!0:o}this.state.interaction=e}matchesSelector(t,e){try{if(!/\s/.test(e.trim()))return t.matches(e)}catch{}const s=e.trim().split(/\s+/);if(s.length<2)return!1;const i=s.pop(),n=s.join(" ");if(!i||!n)return!1;try{return t.matches(i)&&!!t.closest(n)}catch{return!1}}isOutsideContainer(t){const{container:e}=this.options;return!!e&&e!==document.body&&!e.contains(t)}resolveComputedCursor(t){return window.getComputedStyle(t).cursor}handleMove=t=>{if(this.options.autoDisableOnMobile&&t.pointerType==="touch"&&!this.options.enableTouch)return;let e=t.clientX,s=t.clientY;const i=this.options.container;i&&this.containerRect&&i!==document.body&&(e-=this.containerRect.left,s-=this.containerRect.top),this.state.pointer.x=e,this.state.pointer.y=s,this.hasSeenPointer=!0,this.isEnabled&&(this.state.hasReceivedInput||(this.state.hasReceivedInput=!0,this.state.target.x=this.state.smooth.x=e,this.state.target.y=this.state.smooth.y=s))};handleDown=()=>{this.isEnabled&&(this.state.isDown=!0)};handleUp=()=>{this.isEnabled&&(this.state.isDown=!1)};handleMouseOver=t=>{if(!this.isEnabled)return;const e=t.target;if(this.isOutsideContainer(e))return;if(this.state.cursorMode==="auto"&&e.closest(`[${this.ignoreAttribute}]`)){this.state.isHover=!1,this.state.hoverTarget=null,this.state.interaction={},this.currentTarget=null,this.lastParsedTarget=null,this.matchedRules=[],this.state.isNative=!0,this.nativeTarget=e;return}if(this.state.isNative=!1,this.nativeTarget=null,this.currentTarget=e,this.parseDOMInteraction(e),this.state.cursorMode!=="auto"){const o=e.closest(this.getHoverSelector());o&&(this.state.isHover=!0,this.state.hoverTarget=o);return}const s=e.closest(this.getHoverSelector());s&&(this.state.isHover=!0,this.state.hoverTarget=s);const i=b.has(e.localName)||e.isContentEditable,n=!y.has(this.resolveComputedCursor(e));(i||n)&&(this.state.isNative=!0,this.nativeTarget=e)};handleMouseOut=t=>{if(!this.isEnabled)return;const e=t.target,s=t.relatedTarget;this.isOutsideContainer(e)||((e===this.state.hoverTarget||e.contains(this.state.hoverTarget))&&(!s||!this.state.hoverTarget?.contains(s))&&(this.state.isHover=!1,this.state.hoverTarget=null),this.nativeTarget&&(e===this.nativeTarget||e.contains(this.nativeTarget))&&(!s||!this.nativeTarget.contains(s))&&(this.state.isNative=!1,this.nativeTarget=null),e===this.currentTarget&&(this.currentTarget=null,this.lastParsedTarget=null,this.matchedRules=[]))};handleDocumentMouseOut=t=>{this.isEnabled&&t.relatedTarget===null&&this.options.hideOnLeave&&this.handleWindowLeave()};handleWindowLeave=()=>{this.options.hideOnLeave&&(this.state.hasReceivedInput=!1,this.state.pointer={...u})};clearHover(){this.state.isHover=!1,this.state.hoverTarget=null,this.state.isNative=!1,this.nativeTarget=null,this.state.interaction={},this.currentTarget=null,this.lastParsedTarget=null,this.matchedRules=[]}getCurrentTarget(){return this.currentTarget}bindEvents(){const{signal:t}=this.abortController;window.addEventListener("pointermove",this.handleMove,{passive:!0,signal:t}),window.addEventListener("pointerdown",this.handleDown,{passive:!0,signal:t}),window.addEventListener("pointerup",this.handleUp,{signal:t});const s=!this.options.container||this.options.container===document.body?document:this.options.container;s.addEventListener("mouseover",this.handleMouseOver,{signal:t}),s.addEventListener("mouseout",this.handleMouseOut,{signal:t}),document.addEventListener("mouseout",this.handleDocumentMouseOut,{signal:t})}destroy(){this.abortController.abort(),this.resizeObserver?.disconnect()}}let C=0;class g{constructor(t=document.body,e=9999){if(this.container=t,this.zIndex=e,!t||!(t instanceof HTMLElement))throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);t.isConnected||console.warn("[Supermouse] container is not attached to the document — stage sizing/positioning will be wrong until it is.");const s=C++;this.id=`supermouse-style-${s}`,this.scopeClass=`supermouse-scope-${s}`,this.hideClass=`supermouse-hide-${s}`;const i=t===document.body;if(this.element=document.createElement("div"),Object.assign(this.element.style,{position:i?"fixed":"absolute",inset:"0px",pointerEvents:"none",zIndex:String(this.zIndex),opacity:"1",transition:"opacity 0.15s ease"}),!i){const n=window.getComputedStyle(t);this.originalContainerPosition=n.position,n.position==="static"&&(t.style.position="relative")}this.originalContainerCursor=t.style.cursor,t.appendChild(this.element),this.styleTag=document.createElement("style"),this.styleTag.id=this.id,document.head.appendChild(this.styleTag),this.container.classList.add("supermouse-scope",this.scopeClass),this.updateCursorCSS()}container;zIndex;element;styleTag;id;scopeClass;hideClass;currentCursorState=null;originalContainerPosition="";originalContainerCursor="";selectors=new Set(["a","button","input","textarea","select",'[role="button"]',"[tabindex]"]);addSelectors(t){let e=!1;for(const s of t)s.split(",").forEach(i=>{const n=i.trim();n&&!this.selectors.has(n)&&(this.selectors.add(n),e=!0)});e&&this.updateCursorCSS()}addSelector(t){this.addSelectors([t])}setVisibility(t){this.element.style.opacity=t?"1":"0"}setNativeCursor(t){t!==this.currentCursorState&&(this.currentCursorState=t,this.container.classList.toggle(this.hideClass,t==="none"),this.container.style.cursor=t==="none"?"none":this.originalContainerCursor)}updateCursorCSS(){const t=Array.from(this.selectors);if(t.length===0){this.styleTag.innerText="";return}const e=`:not(.${this.scopeClass} .supermouse-scope):not(.${this.scopeClass} .supermouse-scope *)`,s=h=>`.${this.scopeClass}.${this.hideClass} ${h}${e} { cursor: none !important; }`,i=t.map(s).join(`
2
2
  `),n=`.${this.scopeClass}.${this.hideClass} *${e} { cursor: none !important; }`,o=`.${this.scopeClass}.${this.hideClass} { cursor: none !important; }`;this.styleTag.innerText=`
3
3
  ${o}
4
4
  ${n}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supermousejs/core",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "The core physics engine and state manager for Supermouse",
5
5
  "main": "dist/index.umd.js",
6
6
  "module": "dist/index.mjs",
package/src/Supermouse.ts CHANGED
@@ -330,7 +330,6 @@ export class Input {
330
330
  }
331
331
  }
332
332
 
333
- // Clear current target when pointer leaves it
334
333
  if (target === this.currentTarget) {
335
334
  this.currentTarget = null;
336
335
  this.lastParsedTarget = null;
@@ -338,6 +337,17 @@ export class Input {
338
337
  }
339
338
  };
340
339
 
340
+ /**
341
+ * Handles the case where the pointer leaves the entire document.
342
+ * This is more reliable than the `mouseleave` event on `document`.
343
+ */
344
+ private handleDocumentMouseOut = (e: MouseEvent): void => {
345
+ if (!this.isEnabled) return;
346
+ if (e.relatedTarget === null && this.options.hideOnLeave) {
347
+ this.handleWindowLeave();
348
+ }
349
+ };
350
+
341
351
  private handleWindowLeave = (): void => {
342
352
  if (this.options.hideOnLeave) {
343
353
  this.state.hasReceivedInput = false;
@@ -371,7 +381,9 @@ export class Input {
371
381
  const hoverRoot = isBody ? document : this.options.container!;
372
382
  hoverRoot.addEventListener("mouseover", this.handleMouseOver, { signal });
373
383
  hoverRoot.addEventListener("mouseout", this.handleMouseOut, { signal });
374
- document.addEventListener("mouseleave", this.handleWindowLeave, { signal });
384
+
385
+ // Reliable window-leave detection
386
+ document.addEventListener("mouseout", this.handleDocumentMouseOut, { signal });
375
387
  }
376
388
 
377
389
  public destroy(): void {
@@ -734,11 +746,6 @@ export class Supermouse {
734
746
  this.isSuspended = true;
735
747
  this.input.isEnabled = false;
736
748
  this.input.clearHover();
737
- /** Current limitations with multi-scoped containers identified.
738
- * setting native cursor here suppresses cursor css so aggressively
739
- * that cursor: "both" will not work on scoped instances.
740
- * proposed fix by v2.5+ */
741
- // this._stage.setNativeCursor("auto");
742
749
  this._stage.setVisibility(false);
743
750
  }
744
751
 
@@ -230,7 +230,7 @@ describe("Supermouse core", () => {
230
230
  );
231
231
  expect(app.state.hasReceivedInput).toBe(true);
232
232
 
233
- document.dispatchEvent(new MouseEvent("mouseleave"));
233
+ document.dispatchEvent(new MouseEvent("mouseout", { relatedTarget: null }));
234
234
  expect(app.state.hasReceivedInput).toBe(false);
235
235
  expect(app.state.pointer).toEqual({ x: -100, y: -100 });
236
236
  });
@@ -178,7 +178,7 @@ describe("Supermouse input handling", () => {
178
178
  dispatchPointerMove(100, 100);
179
179
  expect(app.state.pointer).toEqual({ x: 100, y: 100 });
180
180
 
181
- const leaveEvent = new MouseEvent("mouseleave", { bubbles: false });
181
+ const leaveEvent = new MouseEvent("mouseout", { relatedTarget: null });
182
182
  document.dispatchEvent(leaveEvent);
183
183
 
184
184
  expect(app.state.hasReceivedInput).toBe(false);
@@ -190,7 +190,7 @@ describe("Supermouse input handling", () => {
190
190
  app = new Supermouse({ container, autoStart: false, hideOnLeave: false });
191
191
 
192
192
  dispatchPointerMove(100, 100);
193
- const leaveEvent = new MouseEvent("mouseleave", { bubbles: false });
193
+ const leaveEvent = new MouseEvent("mouseout", { relatedTarget: null });
194
194
  document.dispatchEvent(leaveEvent);
195
195
 
196
196
  expect(app.state.hasReceivedInput).toBe(true);
@@ -14,27 +14,34 @@ describe("Nested instances cursor inheritance", () => {
14
14
  document.head.innerHTML = "";
15
15
  });
16
16
 
17
- it("preview container should set inline cursor to auto when in 'both' mode to override inherited none", () => {
18
- body = document.body;
19
- globalApp = new Supermouse({ container: body, cursor: "auto", autoStart: false });
20
-
21
- // simulate global instance hiding native cursor
22
- globalApp.setCursor("custom"); // force custom -> native suppressed
23
- globalApp.step(performance.now() + 16);
24
- expect(body.style.cursor).toBe("none");
25
-
26
- previewContainer = document.createElement("div");
27
- body.appendChild(previewContainer);
28
-
29
- previewApp = new Supermouse({ container: previewContainer, cursor: "both", autoStart: false });
30
- previewApp.step(performance.now() + 16);
31
-
32
- // The preview container should have its own inline cursor set to 'auto'
33
- // to override the inherited 'none' from body.
34
- expect(previewContainer.style.cursor).toBe("auto");
35
- });
36
-
37
- it("nested both mode should show native cursor (KNOWN ISSUE)", () => {
17
+ it.fails(
18
+ "preview container should set inline cursor to auto when in 'both' mode to override inherited none",
19
+ () => {
20
+ body = document.body;
21
+ globalApp = new Supermouse({ container: body, cursor: "auto", autoStart: false });
22
+
23
+ // Force global instance to hide native cursor
24
+ globalApp.setCursor("custom");
25
+ globalApp.step(performance.now() + 16);
26
+ expect(body.style.cursor).toBe("none");
27
+
28
+ previewContainer = document.createElement("div");
29
+ body.appendChild(previewContainer);
30
+
31
+ previewApp = new Supermouse({
32
+ container: previewContainer,
33
+ cursor: "both",
34
+ autoStart: false
35
+ });
36
+ previewApp.step(performance.now() + 16);
37
+
38
+ // The preview container should have its own inline cursor set to 'auto'
39
+ // to override the inherited 'none' from body.
40
+ expect(previewContainer.style.cursor).toBe("auto");
41
+ }
42
+ );
43
+
44
+ it.fails("nested both mode should show native cursor", () => {
38
45
  const global = new Supermouse({ container: document.body, cursor: "auto", autoStart: false });
39
46
  global.setCursor("custom");
40
47
  global.step(performance.now() + 16);
@@ -45,7 +52,7 @@ describe("Nested instances cursor inheritance", () => {
45
52
  const local = new Supermouse({ container: preview, cursor: "both", autoStart: false });
46
53
  local.step(performance.now() + 16);
47
54
 
48
- // This will fail with current stable code – intentional.
55
+ // The preview container should have inline cursor 'auto', showing native cursor
49
56
  expect(preview.style.cursor).toBe("auto");
50
57
  });
51
58
  });