@supermousejs/utils 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/src/plugin.ts CHANGED
@@ -1,147 +1,211 @@
1
- import type { Supermouse, SupermousePlugin } from "@supermousejs/core";
2
- import { normalize } from "./options";
3
- import { setStyle } from "./dom";
1
+ import type { SupermouseInstance, SupermousePlugin } from "@supermousejs/core";
4
2
 
3
+ /** Options that can be passed to any plugin factory. */
5
4
  export interface BasePluginOptions {
5
+ /** Overrides the plugin's internal name. */
6
6
  name?: string;
7
+ /** Whether the plugin starts enabled. Defaults to `true`. */
7
8
  isEnabled?: boolean;
8
9
  }
9
10
 
10
- interface LogicConfig {
11
+ interface CoreConfig {
12
+ /** Unique identifier. Used by `getPlugin()`, `enablePlugin()`, etc. */
11
13
  name: string;
14
+ /** Execution order in the frame loop. Lower values are executed earlier */
12
15
  priority?: number;
13
- install?: (app: Supermouse) => void;
14
- update?: (app: Supermouse, deltaTime: number) => void;
15
- destroy?: (app: Supermouse) => void;
16
- onEnable?: (app: Supermouse) => void;
17
- onDisable?: (app: Supermouse) => void;
18
- // Explicitly disallow 'create' here to ensure type separation
19
- create?: never;
16
+ /** Whether the plugin starts enabled. Defaults to `true`. */
17
+ isEnabled?: boolean;
18
+ /** Called once when the plugin is registered via `app.use()`. */
19
+ install?(app: SupermouseInstance): void;
20
20
  }
21
21
 
22
- // --- MODE B: VISUAL PLUGIN CONFIG ---
23
- interface VisualConfig<E extends HTMLElement, O extends object> {
24
- name: string;
25
- /** Automatically register this attribute selector */
26
- selector?: string;
27
- /** Create and return the DOM Element */
28
- create: (app: Supermouse) => E;
29
- /** Map option keys to CSS properties */
30
- styles?: Partial<Record<keyof O, keyof CSSStyleDeclaration>>;
31
- /** Update loop with access to the element */
32
- update?: (app: Supermouse, element: E, deltaTime: number) => void;
33
- onEnable?: (app: Supermouse, element: E) => void;
34
- onDisable?: (app: Supermouse, element: E) => void;
35
- cleanup?: (element: E) => void;
36
- destroy?: never; // Visual plugins use cleanup(), not destroy()
22
+ /**
23
+ * Declarative config for a logic plugin.
24
+ *
25
+ * Logic plugins modify cursor intent (e.g. magnetism, snapping, gravity).
26
+ */
27
+ export interface LogicConfig extends CoreConfig {
28
+ /**
29
+ * Called every frame while the plugin is enabled.
30
+ * @param app The Supermouse instance.
31
+ * @param deltaTime Elapsed time since last frame, in milliseconds.
32
+ */
33
+ update?(app: SupermouseInstance, deltaTime: number): void;
34
+ /** Called when the plugin is removed or the app is destroyed. */
35
+ destroy?(app: SupermouseInstance): void;
36
+ /** Called when `app.enablePlugin(name)` is invoked. */
37
+ onEnable?(app: SupermouseInstance): void;
38
+ /** Called when `app.disablePlugin(name)` is invoked. */
39
+ onDisable?(app: SupermouseInstance): void;
40
+ /**
41
+ * Called before the plugin is disabled. Can return a Promise to delay
42
+ * hiding until an exit animation finishes. This is the declarative
43
+ * equivalent of `SupermousePlugin.onBeforeDisable`.
44
+ */
45
+ beforeDisable?(app: SupermouseInstance): void | Promise<void>;
37
46
  }
38
47
 
39
- // Helper Type Guard to safely distinguish VisualConfig at runtime
40
- function isVisualConfig<E extends HTMLElement, O extends object>(
41
- config: LogicConfig | VisualConfig<E, O>
42
- ): config is VisualConfig<E, O> {
43
- return "create" in config && typeof (config as any).create === "function";
44
- }
48
+ /**
49
+ * Declarative config for a visual plugin.
50
+ *
51
+ * @typeParam E - The specific element subtype returned by `create`.
52
+ * Can be an `HTMLElement` (e.g. `HTMLDivElement`) or an `SVGElement`
53
+ * (e.g. `SVGSVGElement`).
54
+ */
55
+ export interface VisualConfig<
56
+ E extends HTMLElement | SVGElement = HTMLElement | SVGElement
57
+ > extends CoreConfig {
58
+ /**
59
+ * Factory that creates the plugin's root DOM element.
60
+ * Called once during `install()`. The returned element is automatically
61
+ * appended to the **stage** (not `app.container`).
62
+ */
63
+ create: (app: SupermouseInstance) => E;
64
+
65
+ /**
66
+ * Called every frame while the plugin is enabled.
67
+ * Use `css()` from `@supermousejs/utils` for style writes,
68
+ * and `setTransform()` for positioning.
69
+ */
70
+ update?(app: SupermouseInstance, element: E, deltaTime: number): void;
71
+
72
+ /** Called when the plugin is enabled. The element is already visible. */
73
+ onEnable?(app: SupermouseInstance, element: E): void;
74
+ /**
75
+ * Called when the plugin is disabled.
76
+ * The element is still in the DOM when this runs, so you can start
77
+ * CSS transitions. If `beforeDisable` is defined and returns a Promise,
78
+ * the core will wait for it to resolve before hiding the element.
79
+ */
80
+ onDisable?(app: SupermouseInstance, element: E): void;
81
+
82
+ /**
83
+ * Called during `destroy()`, before the element is removed from the DOM.
84
+ * Use this to tear down external listeners or GSAP timelines.
85
+ */
86
+ cleanup?(app: SupermouseInstance, element: E): void;
87
+ /** General teardown hook, called after cleanup and element removal. */
88
+ destroy?(app: SupermouseInstance): void;
89
+
90
+ /**
91
+ * Auto-registers this selector as a hover target on install.
92
+ * Equivalent to `app.registerHoverTarget(selector)` inside `install()`.
93
+ */
94
+ selector?: string;
45
95
 
46
- // --- THE OVERLOADS ---
96
+ /**
97
+ * Called before the plugin is disabled. Can return a Promise to delay
98
+ * hiding until an exit animation finishes.
99
+ * Receives the element as the second argument.
100
+ */
101
+ beforeDisable?(app: SupermouseInstance, element: E): void | Promise<void>;
102
+ }
47
103
 
48
- // Overload 1: Visual Plugin (Infer Element E and Options O)
49
- export function definePlugin<E extends HTMLElement, O extends BasePluginOptions>(
50
- config: VisualConfig<E, O>,
51
- userOptions?: O
52
- ): SupermousePlugin;
104
+ function isVisual<E extends HTMLElement | SVGElement>(
105
+ config: LogicConfig | VisualConfig<E>
106
+ ): config is VisualConfig<E> {
107
+ return typeof (config as VisualConfig).create === "function";
108
+ }
53
109
 
54
- // Overload 2: Logic Plugin
110
+ /**
111
+ * Creates a visual plugin with automatic DOM mounting and lifecycle management.
112
+ *
113
+ * @param config Visual plugin definition.
114
+ * @param userOptions Optional overrides for `name` and `isEnabled`.
115
+ * @returns A `SupermousePlugin` whose `element` property is typed as `E`.
116
+ */
117
+ export function definePlugin<E extends HTMLElement | SVGElement>(
118
+ config: VisualConfig<E>,
119
+ userOptions?: BasePluginOptions
120
+ ): SupermousePlugin & { element?: E };
121
+
122
+ /**
123
+ * Creates a logic plugin that is passed through directly.
124
+ *
125
+ * @param config Logic plugin definition.
126
+ * @param userOptions Optional overrides for `name` and `isEnabled`.
127
+ * @returns A `SupermousePlugin`.
128
+ */
55
129
  export function definePlugin(
56
130
  config: LogicConfig,
57
131
  userOptions?: BasePluginOptions
58
132
  ): SupermousePlugin;
59
133
 
60
- // --- THE IMPLEMENTATION ---
61
-
62
134
  export function definePlugin(
63
- config: LogicConfig | VisualConfig<HTMLElement, any>,
64
- userOptions: any = {}
135
+ config: LogicConfig | VisualConfig,
136
+ userOptions: BasePluginOptions = {}
65
137
  ): SupermousePlugin {
66
- const name = userOptions.name || config.name;
67
- const initialEnabled = userOptions.isEnabled ?? true;
68
-
69
- // MODE A: VISUAL
70
- if (isVisualConfig(config)) {
71
- let element: HTMLElement;
72
-
73
- // PRE-COMPILE STYLE SETTERS
74
- const styleSetters: ((app: Supermouse, el: HTMLElement) => void)[] = [];
75
-
76
- if (config.styles) {
77
- for (const [optKey, cssProp] of Object.entries(config.styles)) {
78
- const getter = normalize(userOptions[optKey], undefined);
79
- const prop = cssProp as any;
80
-
81
- styleSetters.push((app, el) => {
82
- const val = getter(app.state);
83
- if (val !== undefined) {
84
- setStyle(el, prop, val);
85
- }
86
- });
87
- }
88
- }
138
+ const resolvedName = userOptions.name ?? config.name;
139
+ const resolvedEnabled = userOptions.isEnabled ?? true;
89
140
 
90
- return {
91
- name,
92
- isEnabled: initialEnabled,
93
-
94
- install(app) {
95
- // 1. Create & Append
96
- element = config.create(app);
97
- if (config.selector) app.registerHoverTarget(config.selector);
98
-
99
- // 2. Handle Initial State
100
- if (this.isEnabled === false) {
101
- element.style.opacity = "0";
102
- }
103
- app.container.appendChild(element);
104
- },
105
-
106
- update(app, dt) {
107
- if (!element) return;
108
-
109
- // 3. Run Pre-compiled Style Setters
110
- for (let i = 0; i < styleSetters.length; i++) {
111
- styleSetters[i](app, element);
112
- }
113
-
114
- // 4. Run Custom Update
115
- config.update?.(app, element, dt);
116
- },
117
-
118
- onDisable(app) {
119
- if (!element) return;
120
- // Use setStyle to ensure cache remains in sync (0)
121
- setStyle(element, "opacity", 0);
122
- config.onDisable?.(app, element);
123
- },
124
-
125
- onEnable(app) {
126
- if (!element) return;
127
- setStyle(element, "opacity", 1);
128
- config.onEnable?.(app, element);
129
- },
130
-
131
- destroy() {
132
- if (!element) return;
133
- config.cleanup?.(element);
134
- element.remove();
135
- }
136
- };
137
- }
138
-
139
- // MODE B: LOGIC (Standard Pass-through)
140
- else {
141
+ if (!isVisual(config)) {
141
142
  return {
142
143
  ...config,
143
- name,
144
- isEnabled: initialEnabled
144
+ name: resolvedName,
145
+ isEnabled: resolvedEnabled
145
146
  };
146
147
  }
148
+
149
+ let root: HTMLElement | SVGElement | null = null;
150
+ let isMounted = false;
151
+
152
+ const beforeDisable = (app: SupermouseInstance): void | Promise<void> => {
153
+ if (!root) return;
154
+ return config.beforeDisable?.(app, root);
155
+ };
156
+
157
+ return {
158
+ name: resolvedName,
159
+ isEnabled: resolvedEnabled,
160
+ priority: config.priority,
161
+
162
+ install(app) {
163
+ root = config.create(app);
164
+ if (!(root instanceof HTMLElement) && !(root instanceof SVGElement)) {
165
+ console.warn(
166
+ `[supermouse] Plugin "${resolvedName}" create() did not return an HTMLElement or SVGElement.`
167
+ );
168
+ return;
169
+ }
170
+ this.element = app.stage.appendChild(root);
171
+ isMounted = true;
172
+ if (config.selector) {
173
+ app.registerHoverTarget(config.selector);
174
+ }
175
+ if (!resolvedEnabled) {
176
+ root.style.display = "none";
177
+ }
178
+ config.install?.(app);
179
+ },
180
+
181
+ update(app, dt) {
182
+ if (!root || !isMounted) return;
183
+ config.update?.(app, root, dt);
184
+ },
185
+
186
+ onEnable(app) {
187
+ if (!root) return;
188
+ root.style.display = "";
189
+ config.onEnable?.(app, root);
190
+ },
191
+
192
+ async onBeforeDisable(app) {
193
+ await beforeDisable(app);
194
+ },
195
+
196
+ onDisable(app) {
197
+ if (!root) return;
198
+ config.onDisable?.(app, root);
199
+ },
200
+
201
+ destroy(app) {
202
+ if (root) {
203
+ config.cleanup?.(app, root);
204
+ root.remove();
205
+ root = null;
206
+ isMounted = false;
207
+ }
208
+ config.destroy?.(app);
209
+ }
210
+ };
147
211
  }
package/src/svg.ts ADDED
@@ -0,0 +1,119 @@
1
+ type SVGAttrs = Record<string, string | number | boolean>;
2
+
3
+ /** Create any SVG element by tag name with attributes. */
4
+ export function createSVGElement<K extends keyof SVGElementTagNameMap>(
5
+ tag: K,
6
+ attrs: SVGAttrs = {}
7
+ ): SVGElementTagNameMap[K] {
8
+ const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
9
+ for (const [key, value] of Object.entries(attrs)) {
10
+ el.setAttribute(key, String(value));
11
+ }
12
+ return el;
13
+ }
14
+
15
+ /** Set multiple attributes on an SVG element. */
16
+ export function setSVGAttrs(el: SVGElement, attrs: SVGAttrs): void {
17
+ for (const [key, value] of Object.entries(attrs)) {
18
+ el.setAttribute(key, String(value));
19
+ }
20
+ }
21
+
22
+ /** Create a `<g>` group. */
23
+ export function group(attrs: SVGAttrs = {}): SVGGElement {
24
+ return createSVGElement("g", attrs);
25
+ }
26
+
27
+ /** Create a `<circle>`. */
28
+ export function circle(attrs: SVGAttrs = {}): SVGCircleElement {
29
+ return createSVGElement("circle", attrs);
30
+ }
31
+
32
+ /** Create a `<rect>`. */
33
+ export function rect(attrs: SVGAttrs = {}): SVGRectElement {
34
+ return createSVGElement("rect", attrs);
35
+ }
36
+
37
+ /** Create a `<path>` with optional `d` attribute. */
38
+ export function path(d?: string, attrs: SVGAttrs = {}): SVGPathElement {
39
+ const el = createSVGElement("path", attrs);
40
+ if (d !== undefined) el.setAttribute("d", d);
41
+ return el;
42
+ }
43
+
44
+ /** Create a `<text>` element. */
45
+ export function text(attrs: SVGAttrs = {}, content?: string): SVGTextElement {
46
+ const el = createSVGElement("text", attrs);
47
+ if (content !== undefined) el.textContent = content;
48
+ return el;
49
+ }
50
+
51
+ /** Create a `<textPath>` with proper `href` and `xlink:href`. */
52
+ export function textPath(href: string, attrs: SVGAttrs = {}): SVGTextPathElement {
53
+ const el = createSVGElement("textPath", { ...attrs, href });
54
+ el.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", href);
55
+ return el;
56
+ }
57
+
58
+ /** Create a `<filter>` with region attributes and children. */
59
+ export function filter(
60
+ id: string,
61
+ attrs: SVGAttrs = {},
62
+ children: SVGElement[] = []
63
+ ): SVGFilterElement {
64
+ const el = createSVGElement("filter", { id, ...attrs });
65
+ children.forEach((child) => el.appendChild(child));
66
+ return el;
67
+ }
68
+
69
+ /** Create a `<feGaussianBlur>` primitive. */
70
+ export function gaussianBlur(
71
+ stdDeviation: string | number = 0,
72
+ attrs: SVGAttrs = {}
73
+ ): SVGFEGaussianBlurElement {
74
+ return createSVGElement("feGaussianBlur", {
75
+ stdDeviation: String(stdDeviation),
76
+ ...attrs
77
+ });
78
+ }
79
+
80
+ /** Create a `<feTurbulence>` primitive. */
81
+ export function turbulence(
82
+ baseFrequency: string | number = 0.1,
83
+ attrs: SVGAttrs = {}
84
+ ): SVGFETurbulenceElement {
85
+ return createSVGElement("feTurbulence", {
86
+ baseFrequency: String(baseFrequency),
87
+ ...attrs
88
+ });
89
+ }
90
+
91
+ /** Create a `<feMergeNode>` primitive. */
92
+ export function mergeNode(inAttr?: string): SVGFEMergeNodeElement {
93
+ return createSVGElement("feMergeNode", inAttr ? { in: inAttr } : {});
94
+ }
95
+
96
+ /** Create a `<feMerge>` container with merge node children. */
97
+ export function merge(nodes: SVGFEMergeNodeElement[] = []): SVGFEMergeElement {
98
+ const el = createSVGElement("feMerge");
99
+ nodes.forEach((node) => el.appendChild(node));
100
+ return el;
101
+ }
102
+
103
+ /**
104
+ * Calculates the SVG Path data for a perfect circle starting at the top center (12 o'clock).
105
+ *
106
+ * Path moves to (0, -r), then draws two semi-circles.
107
+ * @param r Radius in pixels
108
+ */
109
+ export function circlePath(r: number): string {
110
+ const rClean = Math.round(r * 100) / 100;
111
+
112
+ return `
113
+ M 0, -${rClean}
114
+ A ${rClean},${rClean} 0 1,1 0,${rClean}
115
+ A ${rClean},${rClean} 0 1,1 0,-${rClean}
116
+ `
117
+ .replace(/\s+/g, " ")
118
+ .trim();
119
+ }
package/dist/layers.d.ts DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * Standard Z-Index layers for the Supermouse ecosystem.
3
- * Relative to the Supermouse Container.
4
- */
5
- export declare const Layers: {
6
- /** The top-most layer. For text, tooltips, and crucial UI. */
7
- readonly OVERLAY: "400";
8
- /** The main cursor layer. For the primary Dot/Pointer. */
9
- readonly CURSOR: "300";
10
- /** The secondary layer. For Rings, brackets, or followers. */
11
- readonly FOLLOWER: "200";
12
- /** The background layer. For trails, sparkles, and particles. */
13
- readonly TRACE: "100";
14
- };
15
- //# sourceMappingURL=layers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../src/layers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,MAAM;IACjB,8DAA8D;;IAG9D,0DAA0D;;IAG1D,8DAA8D;;IAG9D,iEAAiE;;CAEzD,CAAC"}
package/src/layers.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Standard Z-Index layers for the Supermouse ecosystem.
3
- * Relative to the Supermouse Container.
4
- */
5
- export const Layers = {
6
- /** The top-most layer. For text, tooltips, and crucial UI. */
7
- OVERLAY: "400",
8
-
9
- /** The main cursor layer. For the primary Dot/Pointer. */
10
- CURSOR: "300",
11
-
12
- /** The secondary layer. For Rings, brackets, or followers. */
13
- FOLLOWER: "200",
14
-
15
- /** The background layer. For trails, sparkles, and particles. */
16
- TRACE: "100"
17
- } as const;