@supermousejs/core 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,44 +1,57 @@
1
- const d = "2.3.0";
2
- function p(n, t, e) {
3
- return n + (t - n) * e;
1
+ const c = "2.4.0";
2
+ function p(o, t, e) {
3
+ return o + (t - o) * e;
4
4
  }
5
- function c(n, t, e, i) {
6
- return p(n, t, 1 - Math.exp(-e * i));
5
+ function d(o, t, e, s) {
6
+ return p(o, t, 1 - Math.exp(-e * s));
7
7
  }
8
- class f {
9
- constructor(t, e, i, a) {
10
- this.state = t, this.options = e, this.getHoverSelector = i, this.onEnableChange = a, this.dataPrefix = this.options.dataPrefix ?? "supermouse", this.normalizedDataPrefix = this.dataPrefix.toLowerCase(), this.ignoreAttribute = `data-${this.dataPrefix}-ignore`, this.checkDeviceCapability(), this.checkMotionPreference(), this.bindEvents();
8
+ const h = { x: -100, y: -100 }, g = /* @__PURE__ */ new Set(["input", "textarea", "select"]), f = /* @__PURE__ */ new Set([
9
+ "default",
10
+ "auto",
11
+ "pointer",
12
+ "none",
13
+ "inherit",
14
+ "grab",
15
+ "grabbing"
16
+ ]), m = [
17
+ "a",
18
+ "button",
19
+ "input",
20
+ "textarea",
21
+ "[data-hover]",
22
+ "[data-cursor]"
23
+ ];
24
+ class v {
25
+ constructor(t, e, s, i) {
26
+ 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();
11
27
  }
12
28
  state;
13
29
  options;
14
30
  getHoverSelector;
15
31
  onEnableChange;
16
32
  mediaQueryList;
17
- mediaQueryHandler;
18
33
  motionQuery;
19
34
  dataPrefix;
20
35
  normalizedDataPrefix;
21
36
  ignoreAttribute;
22
- /**
23
- * Master switch for input processing.
24
- * Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
25
- */
26
- isEnabled = !0;
27
37
  abortController = new AbortController();
28
- /**
29
- * Automatically disables the custom cursor on devices without fine pointer control.
30
- */
38
+ nativeTarget = null;
39
+ hasSeenPointer = !1;
40
+ isEnabled = !0;
41
+ containerRect = null;
42
+ resizeObserver;
43
+ /** Cached matched rules for the current interaction element. */
44
+ matchedRules = [];
45
+ lastParsedTarget = null;
46
+ /** Precomputed list of rule entries for faster iteration. */
47
+ ruleEntries;
48
+ /** The actual element currently under the pointer (regardless of hover selectors). */
49
+ currentTarget = null;
31
50
  checkDeviceCapability() {
32
- this.options.autoDisableOnMobile && (this.mediaQueryList = window.matchMedia("(pointer: fine)"), this.updateEnabledState(this.mediaQueryList.matches), this.mediaQueryHandler = (t) => {
33
- this.updateEnabledState(t.matches);
34
- }, this.mediaQueryList.addEventListener("change", this.mediaQueryHandler, {
51
+ this.options.autoDisableOnMobile && (this.mediaQueryList = window.matchMedia("(pointer: fine)"), this.updateEnabledState(this.mediaQueryList.matches), this.mediaQueryList.addEventListener("change", (t) => this.updateEnabledState(t.matches), {
35
52
  signal: this.abortController.signal
36
53
  }));
37
54
  }
38
- /**
39
- * Checks for `prefers-reduced-motion`.
40
- * If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
41
- */
42
55
  checkMotionPreference() {
43
56
  this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"), this.state.reducedMotion = this.motionQuery.matches, this.motionQuery.addEventListener(
44
57
  "change",
@@ -51,115 +64,167 @@ class f {
51
64
  updateEnabledState(t) {
52
65
  this.isEnabled = t, this.onEnableChange(t);
53
66
  }
67
+ /** Caches container rect; updates on resize/scroll/ResizeObserver. */
68
+ setupContainerRectTracking() {
69
+ const t = this.options.container;
70
+ if (!t || t === document.body) return;
71
+ const e = () => {
72
+ this.containerRect = t.getBoundingClientRect();
73
+ };
74
+ e(), window.addEventListener("resize", e, { signal: this.abortController.signal }), window.addEventListener("scroll", e, {
75
+ passive: !0,
76
+ signal: this.abortController.signal
77
+ }), typeof ResizeObserver < "u" && (this.resizeObserver = new ResizeObserver(e), this.resizeObserver.observe(t));
78
+ }
79
+ /**
80
+ * Evaluates rules against the given element.
81
+ * Selector matching is cached per element; only function values re-evaluated each frame.
82
+ */
54
83
  parseDOMInteraction(t) {
55
- if (this.options.resolveInteraction) {
56
- this.state.interaction = this.options.resolveInteraction(t) || {};
57
- return;
84
+ if (t !== this.lastParsedTarget) {
85
+ this.lastParsedTarget = t, this.matchedRules = [];
86
+ for (const [i, n] of this.ruleEntries)
87
+ this.matchesSelector(t, i) && this.matchedRules.push({ selector: i, rules: n });
58
88
  }
59
89
  const e = {};
60
- if (this.options.rules)
61
- for (const [s, o] of Object.entries(this.options.rules))
62
- t.matches(s) && Object.assign(e, o);
63
- const i = this.normalizedDataPrefix, a = this.dataPrefix.length;
64
- for (const s in t.dataset) {
65
- if (!s.toLowerCase().startsWith(i)) continue;
66
- const o = s.slice(a);
67
- if (!o) continue;
68
- const r = t.dataset[s];
69
- e[o[0].toLowerCase() + o.slice(1)] = r === "" ? !0 : r;
90
+ for (const { rules: i } of this.matchedRules)
91
+ try {
92
+ const n = typeof i == "function" ? i(t) : i;
93
+ if (!n || typeof n != "object") continue;
94
+ for (const [r, a] of Object.entries(n))
95
+ e[r] = typeof a == "function" ? a(t) : a;
96
+ } catch (n) {
97
+ console.error("[Supermouse] Rule threw during evaluation:", n);
98
+ }
99
+ const s = this.normalizedDataPrefix;
100
+ for (const i in t.dataset) {
101
+ if (!i.toLowerCase().startsWith(s)) continue;
102
+ const n = i.slice(s.length);
103
+ if (!n) continue;
104
+ const r = t.dataset[i];
105
+ e[n[0].toLowerCase() + n.slice(1)] = r === "" ? !0 : r;
70
106
  }
71
107
  this.state.interaction = e;
72
108
  }
73
- handleMove(t) {
74
- if (!this.isEnabled || this.options.autoDisableOnMobile && t.pointerType === "touch" && !this.options.enableTouch)
75
- return;
76
- let e = t.clientX, i = t.clientY;
77
- if (this.options.container && this.options.container !== document.body) {
78
- const a = this.options.container.getBoundingClientRect();
79
- e -= a.left, i -= a.top;
109
+ /**
110
+ * Optimized selector matching:
111
+ * - For simple selectors (no spaces), just call `element.matches`.
112
+ * - For complex selectors with descendant combinators, fallback to splitting.
113
+ */
114
+ matchesSelector(t, e) {
115
+ try {
116
+ if (!/\s/.test(e.trim()))
117
+ return t.matches(e);
118
+ } catch {
119
+ }
120
+ const s = e.trim().split(/\s+/);
121
+ if (s.length < 2) return !1;
122
+ const i = s.pop(), n = s.join(" ");
123
+ if (!i || !n) return !1;
124
+ try {
125
+ return t.matches(i) && !!t.closest(n);
126
+ } catch {
127
+ return !1;
80
128
  }
81
- this.state.pointer.x = e, this.state.pointer.y = i, this.state.hasReceivedInput || (this.state.hasReceivedInput = !0, this.state.target.x = this.state.smooth.x = e, this.state.target.y = this.state.smooth.y = i);
82
129
  }
83
- handleDown() {
84
- this.isEnabled && (this.state.isDown = !0);
130
+ isOutsideContainer(t) {
131
+ const { container: e } = this.options;
132
+ return !!e && e !== document.body && !e.contains(t);
85
133
  }
86
- handleUp() {
87
- this.isEnabled && (this.state.isDown = !1);
134
+ resolveComputedCursor(t) {
135
+ return window.getComputedStyle(t).cursor;
88
136
  }
89
- handleMouseOver(t) {
137
+ handleMove = (t) => {
138
+ if (this.options.autoDisableOnMobile && t.pointerType === "touch" && !this.options.enableTouch)
139
+ return;
140
+ let e = t.clientX, s = t.clientY;
141
+ const i = this.options.container;
142
+ 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));
143
+ };
144
+ handleDown = () => {
145
+ this.isEnabled && (this.state.isDown = !0);
146
+ };
147
+ handleUp = () => {
148
+ this.isEnabled && (this.state.isDown = !1);
149
+ };
150
+ handleMouseOver = (t) => {
90
151
  if (!this.isEnabled) return;
91
152
  const e = t.target;
92
- if (e.closest(`[${this.ignoreAttribute}]`)) {
93
- this.state.isNative = !0;
153
+ if (this.isOutsideContainer(e)) return;
154
+ if (this.state.cursorMode === "auto" && e.closest(`[${this.ignoreAttribute}]`)) {
155
+ 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;
94
156
  return;
95
157
  }
96
- const i = this.getHoverSelector(), a = e.closest(i);
97
- a && (this.state.isHover = !0, this.state.hoverTarget = a, this.parseDOMInteraction(this.state.hoverTarget));
98
- const s = this.options.ignoreOnNative;
99
- if (s && s !== null) {
100
- const o = s === "auto" || s === "tag", r = s === "auto" || s === "css";
101
- let u = !1;
102
- if (o) {
103
- const h = e.localName;
104
- (h === "input" || h === "textarea" || h === "select" || e.isContentEditable) && (u = !0);
105
- }
106
- if (!u && r) {
107
- const h = window.getComputedStyle(e).cursor;
108
- ["default", "auto", "pointer", "none", "inherit"].includes(h) || (u = !0);
109
- }
110
- u && (this.state.isNative = !0);
158
+ if (this.state.isNative = !1, this.nativeTarget = null, this.currentTarget = e, this.parseDOMInteraction(e), this.state.cursorMode !== "auto") {
159
+ const r = e.closest(this.getHoverSelector());
160
+ r && (this.state.isHover = !0, this.state.hoverTarget = r);
161
+ return;
111
162
  }
112
- }
113
- handleMouseOut(t) {
163
+ const s = e.closest(this.getHoverSelector());
164
+ s && (this.state.isHover = !0, this.state.hoverTarget = s);
165
+ const i = g.has(e.localName) || e.isContentEditable, n = !f.has(this.resolveComputedCursor(e));
166
+ (i || n) && (this.state.isNative = !0, this.nativeTarget = e);
167
+ };
168
+ handleMouseOut = (t) => {
114
169
  if (!this.isEnabled) return;
115
- const e = t.target;
116
- (e === this.state.hoverTarget || e.contains(this.state.hoverTarget)) && (!t.relatedTarget || !this.state.hoverTarget?.contains(t.relatedTarget)) && (this.state.isHover = !1, this.state.hoverTarget = null, this.state.interaction = {}), this.state.isNative && (this.state.isNative = !1);
117
- }
118
- handleWindowLeave() {
119
- this.options.hideOnLeave && (this.state.hasReceivedInput = !1, this.state.pointer = { ...l });
120
- }
170
+ const e = t.target, s = t.relatedTarget;
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
+ };
173
+ handleWindowLeave = () => {
174
+ this.options.hideOnLeave && (this.state.hasReceivedInput = !1, this.state.pointer = { ...h });
175
+ };
121
176
  clearHover() {
122
- this.state.isHover = !1, this.state.hoverTarget = null, this.state.isNative = !1, this.state.interaction = {};
177
+ 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 = [];
178
+ }
179
+ /** Returns the raw element currently under the pointer. */
180
+ getCurrentTarget() {
181
+ return this.currentTarget;
123
182
  }
124
183
  bindEvents() {
125
184
  const { signal: t } = this.abortController;
126
- window.addEventListener("pointermove", this.handleMove.bind(this), { passive: !0, signal: t }), window.addEventListener("pointerdown", this.handleDown.bind(this), { passive: !0, signal: t }), window.addEventListener("pointerup", this.handleUp.bind(this), { signal: t }), document.addEventListener("mouseover", this.handleMouseOver.bind(this), { signal: t }), document.addEventListener("mouseout", this.handleMouseOut.bind(this), { signal: t }), document.addEventListener("mouseleave", this.handleWindowLeave.bind(this), { signal: t });
185
+ 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
+ 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 });
127
188
  }
128
189
  destroy() {
129
- this.abortController.abort();
190
+ this.abortController.abort(), this.resizeObserver?.disconnect();
130
191
  }
131
192
  }
132
- let g = 0;
133
- class v {
134
- constructor(t = document.body, e) {
135
- if (this.container = t, this.hideNativeCursor = e, !t || !(t instanceof HTMLElement))
193
+ let b = 0;
194
+ class y {
195
+ constructor(t = document.body, e = 9999) {
196
+ if (this.container = t, this.zIndex = e, !t || !(t instanceof HTMLElement))
136
197
  throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);
137
- const i = g++;
138
- this.id = `supermouse-style-${i}`, this.scopeClass = `supermouse-scope-${i}`;
139
- const a = t === document.body;
198
+ t.isConnected || console.warn(
199
+ "[Supermouse] container is not attached to the document — stage sizing/positioning will be wrong until it is."
200
+ );
201
+ const s = b++;
202
+ this.id = `supermouse-style-${s}`, this.scopeClass = `supermouse-scope-${s}`, this.hideClass = `supermouse-hide-${s}`;
203
+ const i = t === document.body;
140
204
  if (this.element = document.createElement("div"), Object.assign(this.element.style, {
141
- position: a ? "fixed" : "absolute",
205
+ position: i ? "fixed" : "absolute",
142
206
  inset: "0px",
143
207
  pointerEvents: "none",
144
- zIndex: "9999",
208
+ zIndex: String(this.zIndex),
145
209
  opacity: "1",
146
210
  transition: "opacity 0.15s ease"
147
- }), !a) {
148
- const s = window.getComputedStyle(t);
149
- this.originalContainerPosition = s.position, s.position === "static" && (t.style.position = "relative");
211
+ }), !i) {
212
+ const n = window.getComputedStyle(t);
213
+ this.originalContainerPosition = n.position, n.position === "static" && (t.style.position = "relative");
150
214
  }
151
- 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(this.scopeClass), this.hideNativeCursor && this.setNativeCursor("none");
215
+ 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();
152
216
  }
153
217
  container;
154
- hideNativeCursor;
155
- /** The container element appended to the document. */
218
+ zIndex;
156
219
  element;
157
220
  styleTag;
158
221
  id;
159
222
  scopeClass;
223
+ hideClass;
160
224
  currentCursorState = null;
161
225
  originalContainerPosition = "";
162
226
  originalContainerCursor = "";
227
+ /** Selectors that need explicit `cursor: none !important` to override UA styles. */
163
228
  selectors = /* @__PURE__ */ new Set([
164
229
  "a",
165
230
  "button",
@@ -169,23 +234,26 @@ class v {
169
234
  '[role="button"]',
170
235
  "[tabindex]"
171
236
  ]);
172
- /**
173
- * Adds a new CSS selector to the `selectors` set.
174
- * Called by `Supermouse` and subsequently plugins during install to ensure
175
- * the native cursor is hidden on their specific interactive targets.
176
- */
237
+ /** Batch add selectors. Comma‑separated groups are split and scoped individually. */
238
+ addSelectors(t) {
239
+ let e = !1;
240
+ for (const s of t)
241
+ s.split(",").forEach((i) => {
242
+ const n = i.trim();
243
+ n && !this.selectors.has(n) && (this.selectors.add(n), e = !0);
244
+ });
245
+ e && this.updateCursorCSS();
246
+ }
247
+ /** Add a single selector (or comma‑separated group). */
177
248
  addSelector(t) {
178
- this.selectors.add(t), this.hideNativeCursor && this.updateCursorCSS();
249
+ this.addSelectors([t]);
179
250
  }
180
251
  setVisibility(t) {
181
252
  this.element.style.opacity = t ? "1" : "0";
182
253
  }
183
- /**
184
- * Toggles the visibility of the native cursor via CSS injection.
185
- * @param type 'none' to hide, 'auto' to show.
186
- */
254
+ /** Toggle native cursor visibility. */
187
255
  setNativeCursor(t) {
188
- !this.hideNativeCursor && t === "none" || t !== this.currentCursorState && (this.currentCursorState = t, t === "none" ? (this.container.style.cursor = "none", this.updateCursorCSS()) : (this.container.style.cursor = "", this.styleTag.innerText = ""));
256
+ t !== this.currentCursorState && (this.currentCursorState = t, this.container.classList.toggle(this.hideClass, t === "none"), this.container.style.cursor = t === "none" ? "none" : this.originalContainerCursor);
189
257
  }
190
258
  updateCursorCSS() {
191
259
  const t = Array.from(this.selectors);
@@ -193,227 +261,241 @@ class v {
193
261
  this.styleTag.innerText = "";
194
262
  return;
195
263
  }
196
- const e = t.map((i) => `.${this.scopeClass} ${i}`).join(", ");
264
+ const e = `:not(.${this.scopeClass} .supermouse-scope):not(.${this.scopeClass} .supermouse-scope *)`, s = (a) => `.${this.scopeClass}.${this.hideClass} ${a}${e} { cursor: none !important; }`, i = t.map(s).join(`
265
+ `), n = `.${this.scopeClass}.${this.hideClass} *${e} { cursor: none !important; }`, r = `.${this.scopeClass}.${this.hideClass} { cursor: none !important; }`;
197
266
  this.styleTag.innerText = `
198
- ${e} {
199
- cursor: none !important;
200
- }
201
- `;
267
+ ${r}
268
+ ${n}
269
+ ${i}
270
+ ${s("label")}
271
+ ${s("select")}
272
+ ${s('input[type="range"]::-webkit-slider-thumb')}
273
+ ${s('input[type="range"]::-moz-range-thumb')}
274
+ `;
202
275
  }
203
276
  destroy() {
204
- this.element.remove(), this.styleTag.remove(), this.container.style.cursor = this.originalContainerCursor, this.container.classList.remove(this.scopeClass), this.container !== document.body && this.originalContainerPosition === "static" && (this.container.style.position = "");
277
+ this.element.remove(), this.styleTag.remove(), this.container.style.cursor = this.originalContainerCursor, this.container.classList.remove("supermouse-scope", this.scopeClass, this.hideClass), this.container !== document.body && this.originalContainerPosition === "static" && (this.container.style.position = "");
205
278
  }
206
279
  }
207
- const l = { x: -100, y: -100 }, m = [
208
- "a",
209
- "button",
210
- "input",
211
- "textarea",
212
- "[data-hover]",
213
- "[data-cursor]"
214
- ];
215
- class y {
216
- static version = d;
217
- version = d;
280
+ class C {
281
+ static version = c;
282
+ version = c;
218
283
  state;
219
- /**
220
- * Configuration options.
221
- */
284
+ /** Configuration options, fully resolved with defaults applied. */
222
285
  options;
223
286
  plugins = [];
224
- stage;
287
+ _stage;
225
288
  input;
226
289
  rafId = 0;
227
290
  lastTime = 0;
228
291
  isRunning = !1;
292
+ isSuspended = !1;
293
+ visibilityAbortController = new AbortController();
229
294
  hoverSelectors;
230
- /**
231
- * Creates a new Supermouse instance.
232
- *
233
- * @param options - Global configuration options.
234
- * @throws Will throw if running in a non-browser environment (window/document undefined).
235
- */
295
+ hoverSelectorString;
296
+ crashedPlugins = [];
236
297
  constructor(t = {}) {
237
298
  this.options = {
238
299
  smoothness: 0.15,
239
300
  enableTouch: !1,
240
301
  autoDisableOnMobile: !0,
241
- ignoreOnNative: "auto",
242
- hideCursor: !0,
302
+ cursor: "auto",
243
303
  hideOnLeave: !0,
244
304
  autoStart: !0,
245
305
  container: document.body,
246
306
  dataPrefix: "supermouse",
307
+ zIndex: 9999,
247
308
  ...t
248
309
  }, this.state = {
249
- pointer: { x: -100, y: -100 },
250
- target: { x: -100, y: -100 },
251
- smooth: { x: -100, y: -100 },
310
+ pointer: { ...h },
311
+ target: { ...h },
312
+ smooth: { ...h },
252
313
  velocity: { x: 0, y: 0 },
314
+ displacement: { x: 0, y: 0 },
253
315
  angle: 0,
254
316
  isDown: !1,
255
317
  isHover: !1,
256
318
  isNative: !1,
257
- forcedCursor: null,
319
+ cursorMode: this.options.cursor,
258
320
  hoverTarget: null,
259
321
  reducedMotion: !1,
260
322
  hasReceivedInput: !1,
261
323
  shape: null,
262
324
  interaction: {}
263
- }, this.options.hoverSelectors ? this.hoverSelectors = new Set(this.options.hoverSelectors) : this.hoverSelectors = new Set(m), this.stage = new v(this.options.container, !!this.options.hideCursor), this.hoverSelectors.forEach((e) => this.stage.addSelector(e)), this.input = new f(
325
+ }, this.hoverSelectors = new Set(this.options.hoverSelectors ?? m), this.hoverSelectorString = Array.from(this.hoverSelectors).join(", "), this._stage = new y(this.options.container, this.options.zIndex), this._stage.addSelectors(this.hoverSelectors), this.input = new v(
264
326
  this.state,
265
327
  this.options,
266
- () => Array.from(this.hoverSelectors).join(", "),
328
+ () => this.hoverSelectorString,
267
329
  (e) => {
268
330
  e || this.reset(!0);
269
331
  }
270
- ), this.options.plugins && this.options.plugins.forEach((e) => this.use(e)), this.init();
332
+ ), this.options.plugins?.forEach((e) => this.use(e)), this.bindVisibilityHandling(), this.init();
271
333
  }
272
- /**
273
- * Retrieves a registered plugin instance by its unique name.
274
- */
334
+ /** Look up a registered plugin by name. */
275
335
  getPlugin(t) {
276
336
  return this.plugins.find((e) => e.name === t);
277
337
  }
278
- /**
279
- * Returns whether the cursor system is currently enabled (processing input).
280
- */
338
+ /** Whether the instance is not disabled/suspended and is processing input. */
281
339
  get isEnabled() {
282
340
  return this.input.isEnabled;
283
341
  }
284
- /**
285
- * Enables a specific plugin by name.
286
- * Triggers the `onEnable` lifecycle hook of the plugin.
287
- */
342
+ /** Enable a plugin by name. */
288
343
  enablePlugin(t) {
289
344
  const e = this.getPlugin(t);
290
- e && e.isEnabled === !1 && (e.isEnabled = !0, e.onEnable?.(this));
345
+ e && e.isEnabled === !1 && (e.isEnabled = !0, e.element && (e.element.style.display = ""), e.onEnable?.(this));
291
346
  }
292
- /**
293
- * Disables a specific plugin by name.
294
- * Triggers the `onDisable` lifecycle hook.
295
- */
347
+ /** Disable a plugin by name and hide its element. */
296
348
  disablePlugin(t) {
297
349
  const e = this.getPlugin(t);
298
- e && e.isEnabled !== !1 && (e.isEnabled = !1, e.onDisable?.(this));
350
+ if (e && e.isEnabled !== !1) {
351
+ e.isEnabled = !1;
352
+ const s = () => {
353
+ e.element && (e.element.style.display = "none"), e.onDisable?.(this);
354
+ }, i = e.onBeforeDisable?.(this);
355
+ i && typeof i.then == "function" ? Promise.resolve(i).then(s).catch((n) => {
356
+ console.error(`[Supermouse] Plugin '${e.name}' onBeforeDisable threw:`, n), s();
357
+ }) : s();
358
+ }
299
359
  }
300
- /**
301
- * Toggles the enabled state of a plugin.
302
- */
360
+ /** Toggle a plugin's enabled state by name. */
303
361
  togglePlugin(t) {
304
362
  const e = this.getPlugin(t);
305
363
  e && (e.isEnabled === !1 ? this.enablePlugin(t) : this.disablePlugin(t));
306
364
  }
365
+ /** Add a selector to hover detection and cursor suppression. */
307
366
  registerHoverTarget(t) {
308
- this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.stage.addSelector(t));
367
+ this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.hoverSelectorString = Array.from(this.hoverSelectors).join(", "), this._stage.addSelector(t));
309
368
  }
310
- /**
311
- * The fixed container element where plugins should append their DOM nodes.
312
- */
369
+ /** The DOM element the instance is scoped to. */
313
370
  get container() {
314
- return this.stage.element;
371
+ return this.options.container;
315
372
  }
316
- /**
317
- * Sets the native cursor visibility.
318
- *
319
- * @param mode
320
- */
321
- setNativeCursor(t) {
322
- this.state.forcedCursor = t === "auto" ? null : t === "hide" ? "none" : "auto";
373
+ /** The stage element that plugins append their visuals into. */
374
+ get stage() {
375
+ return this._stage.element;
376
+ }
377
+ /** Set the current cursor mode. */
378
+ setCursor(t) {
379
+ this.state.cursorMode = t;
323
380
  }
324
381
  init() {
325
382
  this.options.autoStart && this.startLoop();
326
383
  }
384
+ /** Re‑enable input processing and re‑apply cursor state. */
327
385
  enable() {
328
- this.input.isEnabled = !0, this.stage.setNativeCursor("none");
386
+ this.input.isEnabled = !0, this.input.hasSeenPointer && (this.state.target.x = this.state.smooth.x = this.state.pointer.x, this.state.target.y = this.state.smooth.y = this.state.pointer.y, this.resetMotion(), this.state.hasReceivedInput = !0), this._stage.setNativeCursor(this.resolveCursorState());
329
387
  }
388
+ /** Disable input processing and restore native cursor. */
330
389
  disable() {
331
- this.input.isEnabled = !1, this.stage.setNativeCursor("auto"), this.reset(!0);
390
+ this.input.isEnabled = !1, this._stage.setNativeCursor("auto"), this._stage.setVisibility(!1), this.reset(!0);
391
+ }
392
+ /** Temporarily yield to a scoped instance. */
393
+ suspend() {
394
+ this.input.isEnabled && (this.isSuspended = !0, this.input.isEnabled = !1, this.input.clearHover(), this._stage.setVisibility(!1));
395
+ }
396
+ /** Resume from `suspend()`. */
397
+ resume() {
398
+ if (this.isSuspended) {
399
+ this.isSuspended = !1, this.input.isEnabled = !0, this.state.hasReceivedInput && (this.state.target.x = this.state.smooth.x = this.state.pointer.x, this.state.target.y = this.state.smooth.y = this.state.pointer.y, this.resetMotion());
400
+ for (let t = this.plugins.length - 1; t >= 0; t--)
401
+ this.runPluginSafe(this.plugins[t], 0);
402
+ this._stage.setVisibility(!0);
403
+ }
332
404
  }
333
- /**
334
- * Registers a new plugin.
335
- *
336
- * @param plugin - The plugin object to install.
337
- */
405
+ /** Register a new plugin. */
338
406
  use(t) {
339
- if (this.plugins.some((i) => i.name === t.name))
407
+ if (this.plugins.some((e) => e.name === t.name))
340
408
  return console.warn(`[Supermouse] Plugin "${t.name}" already installed.`), this;
341
- t.isEnabled === void 0 && (t.isEnabled = !0);
409
+ t.isEnabled ??= !0;
342
410
  try {
343
411
  t.install?.(this);
344
- } catch (i) {
345
- return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`, i), this;
412
+ } catch (e) {
413
+ return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`, e), this;
346
414
  }
347
- return this.plugins.push(t), this.plugins.sort((i, a) => (i.priority || 0) - (a.priority || 0)), this;
415
+ return this.plugins.push(t), this.plugins.sort((e, s) => (e.priority ?? 0) - (s.priority ?? 0)), this;
348
416
  }
417
+ /** Reset physics; optionally clear all input state. */
349
418
  reset(t = !1) {
350
- this.state.pointer = { ...l }, this.state.target = { ...l }, this.state.smooth = { ...l }, this.state.velocity = { x: 0, y: 0 }, this.state.angle = 0, t && (this.state.hasReceivedInput = !1, this.state.shape = null, this.state.interaction = {});
419
+ this.state.target = { ...h }, this.state.smooth = { ...h }, this.resetMotion(), this.state.angle = 0, t && (this.state.hasReceivedInput = !1, this.state.shape = null, this.state.interaction = {});
351
420
  }
352
421
  startLoop() {
353
- this.isRunning || (this.isRunning = !0, this.lastTime = performance.now(), this.tick(this.lastTime));
422
+ this.isRunning || (this.isRunning = !0, !document.hidden && (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
354
423
  }
355
- /**
356
- * Starts the animation loop. This is automatically called if `autoStart` is true.
357
- * Plugins can call this method to resume the loop if it has been stopped.
358
- */
424
+ /** Start the animation loop. */
359
425
  start() {
360
426
  this.startLoop();
361
427
  }
362
- /**
363
- * Manually steps the animation loop.
364
- *
365
- * @param time Current timestamp in milliseconds.
366
- */
428
+ /** Manually step the animation loop. */
367
429
  step(t) {
368
- this.tick(t);
430
+ this.update(t);
369
431
  }
370
432
  runPluginSafe(t, e) {
371
433
  if (t.isEnabled !== !1)
372
434
  try {
373
435
  t.update?.(this, e);
374
- } catch (i) {
375
- console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`, i);
376
- const a = this.plugins.indexOf(t);
377
- a > -1 && this.plugins.splice(a, 1), t.isEnabled = !1;
436
+ } catch (s) {
437
+ console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`, s), t.isEnabled = !1, this.crashedPlugins.push(t);
438
+ }
439
+ }
440
+ cleanupCrashedPlugins() {
441
+ if (this.crashedPlugins.length !== 0) {
442
+ for (const t of this.crashedPlugins) {
443
+ const e = this.plugins.indexOf(t);
444
+ e > -1 && this.plugins.splice(e, 1);
378
445
  try {
379
- t.destroy?.(this), t.onDisable?.(this);
446
+ t.onDisable?.(this), t.destroy?.(this);
380
447
  } catch (s) {
381
448
  console.error(`[Supermouse] Failed to cleanup crashed plugin '${t.name}'.`, s);
382
449
  }
450
+ t.element?.remove();
383
451
  }
384
- }
385
- /**
386
- * Runs on every animation frame.
387
- */
388
- tick = (t) => {
389
- const e = t - this.lastTime, i = Math.min(e / 1e3, 0.1);
390
- this.lastTime = t, this.state.hoverTarget && !this.state.hoverTarget.isConnected && this.input.clearHover();
391
- const a = this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
392
- if (this.stage.setVisibility(a), this.input.isEnabled && this.options.hideCursor) {
393
- let s = "auto";
394
- this.state.forcedCursor !== null ? s = this.state.forcedCursor : s = this.state.isNative || !this.state.hasReceivedInput ? "auto" : "none", this.stage.setNativeCursor(s);
452
+ this.crashedPlugins = [];
395
453
  }
396
- this.input.isEnabled && this.state.hasReceivedInput ? (this.state.target.x = this.state.pointer.x, this.state.target.y = this.state.pointer.y) : this.state.target = { ...l };
397
- for (let s = 0; s < this.plugins.length; s++)
398
- this.runPluginSafe(this.plugins[s], e);
399
- if (this.input.isEnabled) {
400
- const s = this.state.reducedMotion ? 1e3 : 1 / this.options.smoothness * 2;
401
- this.state.smooth.x = c(this.state.smooth.x, this.state.target.x, s, i), this.state.smooth.y = c(this.state.smooth.y, this.state.target.y, s, i), this.state.velocity.x = this.state.target.x - this.state.smooth.x, this.state.velocity.y = this.state.target.y - this.state.smooth.y;
402
- const { x: o, y: r } = this.state.velocity;
403
- (Math.abs(o) > 0.1 || Math.abs(r) > 0.1) && (this.state.angle = Math.atan2(r, o) * (180 / Math.PI));
454
+ }
455
+ resolveStageVisibility() {
456
+ return this.state.cursorMode === "native" ? !1 : this.state.cursorMode === "both" ? this.input.isEnabled && this.state.hasReceivedInput : this.state.cursorMode === "custom" ? this.input.isEnabled && this.state.hasReceivedInput : this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
457
+ }
458
+ resolveCursorState() {
459
+ return !this.input.isEnabled || this.state.cursorMode === "both" || this.state.cursorMode === "native" ? "auto" : this.state.cursorMode === "custom" ? "none" : this.state.isNative || !this.state.hasReceivedInput ? "auto" : "none";
460
+ }
461
+ resetMotion() {
462
+ this.state.velocity = { x: 0, y: 0 }, this.state.displacement = { x: 0, y: 0 };
463
+ }
464
+ update(t) {
465
+ const e = t - this.lastTime, s = Math.min(e / 1e3, 0.1);
466
+ this.lastTime = t;
467
+ const i = this.input.getCurrentTarget();
468
+ i && !i.isConnected ? this.input.clearHover() : i && this.input.parseDOMInteraction(i), this._stage.setVisibility(this.resolveStageVisibility()), this.input.isEnabled && this._stage.setNativeCursor(this.resolveCursorState()), this.input.isEnabled && this.state.hasReceivedInput && (this.state.target.x = this.state.pointer.x, this.state.target.y = this.state.pointer.y);
469
+ for (let n = 0; n < this.plugins.length; n++)
470
+ this.runPluginSafe(this.plugins[n], e);
471
+ if (this.cleanupCrashedPlugins(), this.input.isEnabled) {
472
+ const n = this.state.reducedMotion ? 1e3 : 1 / this.options.smoothness * 2, r = this.state.smooth.x, a = this.state.smooth.y;
473
+ this.state.smooth.x = d(this.state.smooth.x, this.state.target.x, n, s), this.state.smooth.y = d(this.state.smooth.y, this.state.target.y, n, s), this.state.displacement.x = this.state.target.x - this.state.smooth.x, this.state.displacement.y = this.state.target.y - this.state.smooth.y, s > 0 ? (this.state.velocity.x = (this.state.smooth.x - r) / s, this.state.velocity.y = (this.state.smooth.y - a) / s) : (this.state.velocity.x = 0, this.state.velocity.y = 0);
474
+ const { x: l, y: u } = this.state.velocity;
475
+ (Math.abs(l) > 0.1 || Math.abs(u) > 0.1) && (this.state.angle = Math.atan2(u, l) * (180 / Math.PI));
404
476
  }
405
- this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
477
+ }
478
+ tick = (t) => {
479
+ this.update(t), this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
406
480
  };
407
- /**
408
- * Destroys the instance.
409
- */
481
+ /** Pause rAF loop when tab hidden; resume on visible. */
482
+ bindVisibilityHandling() {
483
+ document.addEventListener(
484
+ "visibilitychange",
485
+ () => {
486
+ this.isRunning && (document.hidden ? cancelAnimationFrame(this.rafId) : (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
487
+ },
488
+ { signal: this.visibilityAbortController.signal }
489
+ );
490
+ }
491
+ /** Destroy the instance, freeing all resources. */
410
492
  destroy() {
411
- this.isRunning = !1, cancelAnimationFrame(this.rafId), this.input.destroy(), this.stage.destroy(), this.plugins.forEach((t) => t.destroy?.(this)), this.plugins = [];
493
+ this.isRunning = !1, cancelAnimationFrame(this.rafId), this.visibilityAbortController.abort(), this.input.destroy(), this._stage.destroy(), this.plugins.forEach((t) => t.destroy?.(this)), this.plugins = [];
412
494
  }
413
495
  }
414
496
  export {
415
497
  m as DEFAULT_HOVER_SELECTORS,
416
- f as Input,
417
- v as Stage,
418
- y as Supermouse
498
+ v as Input,
499
+ y as Stage,
500
+ C as Supermouse
419
501
  };