@supermousejs/core 2.4.1 → 2.4.2

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.
@@ -1,235 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
2
- import { Stage } from "../Supermouse";
3
-
4
- describe("Supermouse Stage", () => {
5
- let container: HTMLElement;
6
- let stage: Stage;
7
-
8
- beforeEach(() => {
9
- container = document.createElement("div");
10
- document.body.appendChild(container);
11
- });
12
-
13
- afterEach(() => {
14
- stage?.destroy();
15
- document.body.innerHTML = "";
16
- document.head.innerHTML = "";
17
- vi.restoreAllMocks();
18
- });
19
-
20
- const getStyleTag = () =>
21
- document.querySelector('style[id^="supermouse-style-"]') as HTMLStyleElement | null;
22
-
23
- const hasClassPrefix = (el: HTMLElement, prefix: string) =>
24
- Array.from(el.classList).some((c) => c.startsWith(prefix));
25
-
26
- describe("Element creation & positioning", () => {
27
- it("creates stage element with default styles", () => {
28
- stage = new Stage(container, 9999);
29
- const el = stage.element;
30
- expect(el).toBeInstanceOf(HTMLDivElement);
31
- expect(el.style.position).toBe("absolute");
32
- expect(el.style.inset).toBe("0px");
33
- expect(el.style.pointerEvents).toBe("none");
34
- expect(el.style.zIndex).toBe("9999");
35
- expect(el.style.opacity).toBe("1");
36
- expect(el.style.transition).toBe("opacity 0.15s ease");
37
- expect(container.contains(el)).toBe(true);
38
- });
39
-
40
- it("uses fixed positioning when container is body", () => {
41
- stage = new Stage(document.body, 9999);
42
- expect(stage.element.style.position).toBe("fixed");
43
- });
44
-
45
- it("throws if container is not an HTMLElement", () => {
46
- expect(() => new Stage(null as any, 9999)).toThrowError(
47
- "[Supermouse] Invalid container: null. Must be an HTMLElement."
48
- );
49
- });
50
-
51
- it("warns if container is not connected", () => {
52
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
53
- const detached = document.createElement("div");
54
- new Stage(detached, 9999).destroy();
55
- expect(warn).toHaveBeenCalledWith(
56
- expect.stringContaining("container is not attached to the document")
57
- );
58
- });
59
- });
60
-
61
- describe("Stylesheet & selectors", () => {
62
- it("injects style tag with cursor suppression rules on creation", () => {
63
- stage = new Stage(container, 9999);
64
- const styleTag = getStyleTag();
65
- expect(styleTag).toBeInstanceOf(HTMLStyleElement);
66
- expect(styleTag!.parentNode).toBe(document.head);
67
- expect(styleTag!.innerText).toContain("cursor: none !important");
68
- });
69
-
70
- it("adds supermouse scope classes to container", () => {
71
- stage = new Stage(container, 9999);
72
- expect(container.classList.contains("supermouse-scope")).toBe(true);
73
- expect(hasClassPrefix(container, "supermouse-scope-")).toBe(true);
74
- });
75
-
76
- it("addSelector updates the stylesheet", () => {
77
- stage = new Stage(container, 9999);
78
- const styleTag = getStyleTag()!;
79
- const before = styleTag.innerText;
80
-
81
- stage.addSelector(".my-selector");
82
- const after = styleTag.innerText;
83
-
84
- expect(before).not.toBe(after);
85
- expect(after).toContain(".my-selector");
86
- });
87
-
88
- it("addSelectors batch updates stylesheet only once", () => {
89
- stage = new Stage(container, 9999);
90
- const styleTag = getStyleTag()!;
91
- const originalInnerText = styleTag.innerText;
92
-
93
- stage.addSelectors([".a", ".b", ".c"]);
94
- const finalInnerText = styleTag.innerText;
95
-
96
- expect(originalInnerText).not.toBe(finalInnerText);
97
- expect(finalInnerText).toContain(".a");
98
- expect(finalInnerText).toContain(".b");
99
- expect(finalInnerText).toContain(".c");
100
- });
101
-
102
- it("scopes all hover selectors correctly, splitting comma groups", () => {
103
- const container = document.createElement("div");
104
- document.body.appendChild(container);
105
- const stage = new Stage(container, 9999);
106
-
107
- stage.addSelector("p, span, h1, h2");
108
-
109
- const styleTag = document.querySelector("style[id^='supermouse-style-']")!;
110
- const styleText = styleTag.innerText;
111
-
112
- const scopeClass = Array.from(container.classList).find((c) =>
113
- c.startsWith("supermouse-scope-")
114
- );
115
- const hideClass = `supermouse-hide-${scopeClass!.split("-").pop()}`;
116
-
117
- expect(styleText).toContain(`.${scopeClass}.${hideClass} p`);
118
- expect(styleText).toContain(`.${scopeClass}.${hideClass} span`);
119
- expect(styleText).toContain(`.${scopeClass}.${hideClass} h1`);
120
- expect(styleText).toContain(`.${scopeClass}.${hideClass} h2`);
121
-
122
- expect(styleText).not.toMatch(/(^|,)\s*span\s*{/);
123
- expect(styleText).not.toMatch(/(^|,)\s*h1\s*{/);
124
-
125
- stage.destroy();
126
- container.remove();
127
- });
128
- });
129
-
130
- describe("Native cursor suppression", () => {
131
- it("setNativeCursor toggles hide class and inline cursor style", () => {
132
- stage = new Stage(container, 9999);
133
- stage.setNativeCursor("none");
134
- expect(container.style.cursor).toBe("none");
135
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(true);
136
-
137
- stage.setNativeCursor("auto");
138
- expect(container.style.cursor).toBe("");
139
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(false);
140
- });
141
-
142
- it("restores original container cursor when showing native", () => {
143
- container.style.cursor = "pointer";
144
- stage = new Stage(container, 9999);
145
- stage.setNativeCursor("none");
146
- expect(container.style.cursor).toBe("none"); // suppressed
147
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(true);
148
-
149
- stage.setNativeCursor("auto");
150
- expect(container.style.cursor).toBe("pointer"); // restored
151
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(false);
152
- });
153
-
154
- it("keeps original container cursor even if empty", () => {
155
- container.style.cursor = "";
156
- stage = new Stage(container, 9999);
157
- stage.setNativeCursor("auto");
158
- expect(container.style.cursor).toBe("");
159
- });
160
- });
161
-
162
- describe("Visibility & layout", () => {
163
- it("setVisibility toggles opacity", () => {
164
- stage = new Stage(container, 9999);
165
- stage.setVisibility(false);
166
- expect(stage.element.style.opacity).toBe("0");
167
- stage.setVisibility(true);
168
- expect(stage.element.style.opacity).toBe("1");
169
- });
170
-
171
- it("sets container position to relative if static", () => {
172
- container.style.position = "static";
173
- stage = new Stage(container, 9999);
174
- expect(container.style.position).toBe("relative");
175
- stage.destroy();
176
- expect(container.style.position).toBe("");
177
- });
178
-
179
- it("does not modify container position if already non-static", () => {
180
- container.style.position = "absolute";
181
- stage = new Stage(container, 9999);
182
- expect(container.style.position).toBe("absolute");
183
- stage.destroy();
184
- expect(container.style.position).toBe("absolute");
185
- });
186
-
187
- it("does not modify position if container is body", () => {
188
- stage = new Stage(document.body, 9999);
189
- expect(document.body.style.position).toBe("");
190
- stage.destroy();
191
- });
192
- });
193
-
194
- describe("Cleanup", () => {
195
- it("destroy removes stage element, style tag, and classes", () => {
196
- stage = new Stage(container, 9999);
197
- const el = stage.element;
198
- const styleTag = getStyleTag()!;
199
- expect(document.body.contains(el)).toBe(true);
200
- expect(document.head.contains(styleTag)).toBe(true);
201
-
202
- stage.destroy();
203
-
204
- expect(document.body.contains(el)).toBe(false);
205
- expect(document.head.contains(styleTag)).toBe(false);
206
- expect(container.classList.contains("supermouse-scope")).toBe(false);
207
- expect(hasClassPrefix(container, "supermouse-scope-")).toBe(false);
208
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(false);
209
- expect(container.style.cursor).toBe("");
210
- });
211
-
212
- it("destroy restores original container cursor", () => {
213
- container.style.cursor = "crosshair";
214
- stage = new Stage(container, 9999);
215
- stage.destroy();
216
- expect(container.style.cursor).toBe("crosshair");
217
- });
218
-
219
- it("creates unique style IDs for multiple instances", () => {
220
- const stage1 = new Stage(container, 9999);
221
- const stage2Container = document.createElement("div");
222
- document.body.appendChild(stage2Container);
223
- const stage2 = new Stage(stage2Container, 9999);
224
-
225
- const styleTags = document.querySelectorAll('style[id^="supermouse-style-"]');
226
- expect(styleTags.length).toBe(2);
227
- const ids = Array.from(styleTags).map((tag) => tag.id);
228
- expect(ids[0]).not.toBe(ids[1]);
229
-
230
- stage1.destroy();
231
- stage2.destroy();
232
- stage2Container.remove();
233
- });
234
- });
235
- });
@@ -1,50 +0,0 @@
1
- import { describe, it, expect, afterEach, vi } from "vitest";
2
- import { Supermouse } from "../Supermouse";
3
-
4
- describe("Suspend/Resume cursor state", () => {
5
- let app: Supermouse;
6
- let container: HTMLElement;
7
-
8
- const hasClassPrefix = (el: HTMLElement, prefix: string) =>
9
- Array.from(el.classList).some((c) => c.startsWith(prefix));
10
-
11
- afterEach(() => {
12
- app?.destroy();
13
- document.body.innerHTML = "";
14
- document.head.innerHTML = "";
15
- vi.restoreAllMocks();
16
- });
17
-
18
- function createApp(cursor: "auto" | "custom" | "native" | "both" = "auto") {
19
- container = document.createElement("div");
20
- document.body.appendChild(container);
21
- app = new Supermouse({ container, cursor, autoStart: false });
22
- window.dispatchEvent(
23
- new PointerEvent("pointermove", { clientX: 100, clientY: 100, pointerType: "mouse" })
24
- );
25
- app.start();
26
- app.step(performance.now() + 16);
27
- }
28
-
29
- it("resume() restores native cursor suppression synchronously", () => {
30
- createApp("auto");
31
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(true);
32
- app.suspend();
33
- expect(app.stage.style.opacity).toBe("0");
34
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(true);
35
-
36
- app.resume();
37
- // Immediately after resume, hide class should still be present (native cursor hidden)
38
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(true);
39
- expect(container.style.cursor).toBe("none");
40
- });
41
-
42
- it("suspend() hides stage but preserves native cursor suppression state", () => {
43
- createApp("auto");
44
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(true);
45
-
46
- app.suspend();
47
- expect(app.stage.style.opacity).toBe("0");
48
- expect(hasClassPrefix(container, "supermouse-hide-")).toBe(true);
49
- });
50
- });
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./Supermouse";
2
- export * from "./types";
package/src/types.ts DELETED
@@ -1,165 +0,0 @@
1
- import type { SupermouseInstance } from "./Supermouse";
2
-
3
- export interface MousePosition {
4
- x: number;
5
- y: number;
6
- }
7
-
8
- export interface ShapeState {
9
- width: number;
10
- height: number;
11
- borderRadius: number;
12
- }
13
-
14
- export type RuleValue = string | boolean | number;
15
- export type RuleSet = Record<string, RuleValue | ((el: HTMLElement) => RuleValue)>;
16
- export type RuleDefinition = RuleSet | ((el: HTMLElement) => RuleSet);
17
-
18
- /**
19
- * Interaction state consumed by plugins.
20
- * Extend via module augmentation for type safety.
21
- *
22
- * @example
23
- * declare module '@supermousejs/core' {
24
- * interface InteractionState {
25
- * magnetic: boolean | number;
26
- * text: string;
27
- * }
28
- * }
29
- */
30
- export interface InteractionState {
31
- /** Arbitrary keys allowed for quick prototyping. */
32
- [key: string]: any;
33
- }
34
-
35
- export interface MouseState {
36
- /** Raw pointer position from the latest event (before smoothing). */
37
- pointer: MousePosition;
38
- /** Goal position the core loop drives toward. */
39
- target: MousePosition;
40
- /** Smoothed position used for rendering. */
41
- smooth: MousePosition;
42
- /** Movement vector derived from smoothed state. */
43
- velocity: MousePosition;
44
- /** Remaining distance to target. */
45
- displacement: MousePosition;
46
- /** Movement angle in degrees. */
47
- angle: number;
48
- /** Pointer is pressed down. */
49
- isDown: boolean;
50
- /** Hovering over a registered interactive element. */
51
- isHover: boolean;
52
- /** Native cursor temporarily restored due to native-input heuristics. */
53
- isNative: boolean;
54
- /** Current cursor mode: auto, custom, native, or both. */
55
- cursorMode: "auto" | "custom" | "native" | "both";
56
- /** Currently hovered DOM element, if any. */
57
- hoverTarget: HTMLElement | null;
58
- /** User has `prefers-reduced-motion` enabled. */
59
- reducedMotion: boolean;
60
- /** At least one valid input coordinate received. */
61
- hasReceivedInput: boolean;
62
- /** Geometric shape the cursor should conform to. */
63
- shape: ShapeState | null;
64
- /** Centralized store for hover metadata from data attributes and rules. */
65
- interaction: InteractionState;
66
- }
67
-
68
- /** Configuration options for the Supermouse constructor. */
69
- export interface SupermouseOptions {
70
- /**
71
- * The interpolation factor (0 to 1). Lower is smoother/slower.
72
- * @default 0.15
73
- */
74
- smoothness?: number;
75
- /**
76
- * List of CSS selectors that trigger the "Hover" state.
77
- * Overrides the default set if provided.
78
- */
79
- hoverSelectors?: string[];
80
- /**
81
- * Whether to enable custom cursor effects on touch devices.
82
- * @default false
83
- */
84
- enableTouch?: boolean;
85
- /**
86
- * Whether to automatically disable the custom cursor on devices with coarse pointers.
87
- * @default true
88
- */
89
- autoDisableOnMobile?: boolean;
90
- /**
91
- * Overall cursor mode.
92
- * - `"auto"`: use built-in heuristic to decide per element.
93
- * - `"custom"`: always show custom cursor, hide native.
94
- * - `"native"`: always show native cursor, hide custom.
95
- * - `"both"`: always show custom cursor **and** native cursor together (no suppression).
96
- * @default "auto"
97
- */
98
- cursor?: "auto" | "custom" | "native" | "both";
99
- /**
100
- * Whether to hide the custom cursor when the pointer leaves the browser viewport.
101
- * @default true
102
- */
103
- hideOnLeave?: boolean;
104
- /**
105
- * List of plugins to initialize with the instance.
106
- */
107
- plugins?: SupermousePlugin[];
108
- /**
109
- * The DOM element to append the cursor stage to.
110
- * @default document.body
111
- */
112
- container?: HTMLElement;
113
- /**
114
- * Whether to start the internal animation loop automatically.
115
- * @default true
116
- */
117
- autoStart?: boolean;
118
- /**
119
- * Semantic rules mapping CSS selectors to interaction state.
120
- * Rules are evaluated against hovered elements and merged into `state.interaction`.
121
- * @example { 'button': { icon: 'pointer' } }
122
- */
123
- rules?: Record<string, RuleDefinition>;
124
- /**
125
- * The prefix used for data attributes to store hover metadata.
126
- * @default "supermouse"
127
- */
128
- dataPrefix?: string;
129
- /**
130
- * The `z-index` applied to the cursor stage element.
131
- * @default 9999
132
- */
133
- zIndex?: number;
134
- }
135
-
136
- /** Static value or function that returns a value based on state. */
137
- export type ValueOrGetter<T> = T | ((state: MouseState) => T);
138
-
139
- /** Interface for defining a Supermouse plugin. */
140
- export interface SupermousePlugin {
141
- /** Unique plugin name. */
142
- name: string;
143
- /** Execution priority; lower runs first. */
144
- priority?: number;
145
- /** If false, `update()` will not be called. */
146
- isEnabled?: boolean;
147
- /** Root DOM element, if any. Hidden when plugin disabled. */
148
- element?: HTMLElement | SVGElement;
149
-
150
- /** Called when `app.use()` is executed. */
151
- install?: (instance: SupermouseInstance) => void;
152
- /** Called every animation frame with delta time in ms. */
153
- update?: (instance: SupermouseInstance, deltaTime: number) => void;
154
- /** Called when plugin removed or instance destroyed. */
155
- destroy?: (instance: SupermouseInstance) => void;
156
-
157
- /** Called when plugin enabled via `.enablePlugin()`. */
158
- onEnable?: (instance: SupermouseInstance) => void;
159
- /** Called when plugin disabled via `.disablePlugin()`. */
160
- onDisable?: (instance: SupermouseInstance) => void;
161
- /**
162
- * Called before the plugin is disabled.
163
- */
164
- onBeforeDisable?(instance: SupermouseInstance): void | Promise<void>;
165
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.composite-lib.json",
3
- "compilerOptions": {
4
- "rootDir": "src",
5
- "outDir": "dist"
6
- },
7
- "include": ["src"],
8
- "references": []
9
- }
@@ -1 +0,0 @@
1
- {"fileNames":[],"fileInfos":[],"root":[],"options":{"allowImportingTsExtensions":true,"composite":true,"declaration":true,"declarationMap":true,"esModuleInterop":true,"module":99,"noFallthroughCasesInSwitch":true,"noImplicitAny":true,"noImplicitReturns":true,"noUnusedLocals":true,"noUnusedParameters":true,"outDir":"../../dist","rootDir":"../../src","skipLibCheck":true,"strict":true,"target":7,"useDefineForClassFields":true},"version":"5.8.3"}
package/vite.config.ts DELETED
@@ -1,32 +0,0 @@
1
- import { defineConfig } from "vite";
2
- import dts from "vite-plugin-dts";
3
- import path from "path";
4
- import { fileURLToPath } from "url";
5
- import { readFileSync } from "fs";
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
- const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "package.json"), "utf-8"));
11
-
12
- export default defineConfig({
13
- define: {
14
- __VERSION__: JSON.stringify(pkg.version)
15
- },
16
- build: {
17
- lib: {
18
- entry: path.resolve(__dirname, "src/index.ts"),
19
- name: "SupermouseCore",
20
- fileName: (format) => (format === "es" ? "index.mjs" : "index.umd.js")
21
- },
22
- rollupOptions: {
23
- external: [],
24
- output: {
25
- globals: {
26
- "@supermousejs/core": "SupermouseCore"
27
- }
28
- }
29
- }
30
- },
31
- plugins: [dts({ rollupTypes: true })]
32
- });
package/vitest.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- environment: "jsdom",
6
- globals: true,
7
- setupFiles: ["src/__tests__/setup.ts"],
8
- include: ["src/**/*.test.ts", "src/**/*.spec.ts"]
9
- }
10
- });