@haptics/vanilla 1.0.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.cjs ADDED
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ var core = require('@haptics/core');
4
+
5
+ // src/haptics.ts
6
+ var Haptics = class {
7
+ constructor(options = {}) {
8
+ this.prefersReducedMotion = false;
9
+ this.destroyed = false;
10
+ this.mqlHandler = null;
11
+ this.mql = null;
12
+ this.patterns = { ...core.PRESETS, ...options.patterns };
13
+ this.respectReducedMotion = options.respectReducedMotion ?? true;
14
+ this.delegateFrom = options.delegateFrom ?? document;
15
+ this.selector = options.selector ?? "[data-haptic]";
16
+ this.isSupported = core.isVibrationSupported();
17
+ this.isIOSSupported = core.isIOS();
18
+ if (this.respectReducedMotion && typeof window !== "undefined" && window.matchMedia) {
19
+ this.mql = window.matchMedia("(prefers-reduced-motion: reduce)");
20
+ this.prefersReducedMotion = this.mql.matches;
21
+ this.mqlHandler = (e) => {
22
+ this.prefersReducedMotion = e.matches;
23
+ };
24
+ this.mql.addEventListener("change", this.mqlHandler);
25
+ }
26
+ this.clickHandler = (e) => {
27
+ if (this.destroyed) return;
28
+ if (this.respectReducedMotion && this.prefersReducedMotion) return;
29
+ const target = e.target?.closest(this.selector);
30
+ if (!target) return;
31
+ const action = target.getAttribute("data-haptic");
32
+ if (!action || !(action in this.patterns)) return;
33
+ const pattern = this.patterns[action];
34
+ if (this.isSupported) {
35
+ navigator.vibrate(core.toVibrateSequence(pattern));
36
+ } else if (this.isIOSSupported) {
37
+ core.schedulePattern(pattern);
38
+ }
39
+ };
40
+ this.delegateFrom.addEventListener("click", this.clickHandler, {
41
+ capture: true,
42
+ passive: true
43
+ });
44
+ }
45
+ /** Trigger haptic feedback imperatively by pattern name. */
46
+ trigger(action) {
47
+ if (this.destroyed) return;
48
+ if (this.respectReducedMotion && this.prefersReducedMotion) return;
49
+ const pattern = this.patterns[action];
50
+ if (!pattern) return;
51
+ if (this.isSupported) {
52
+ navigator.vibrate(core.toVibrateSequence(pattern));
53
+ } else if (this.isIOSSupported) {
54
+ core.schedulePattern(pattern);
55
+ }
56
+ }
57
+ /** Cancel active vibration (Android only). */
58
+ cancel() {
59
+ if (this.isSupported) navigator.vibrate(0);
60
+ }
61
+ /** Remove all listeners and clean up. */
62
+ destroy() {
63
+ if (this.destroyed) return;
64
+ this.destroyed = true;
65
+ this.delegateFrom.removeEventListener("click", this.clickHandler, {
66
+ capture: true
67
+ });
68
+ if (this.mql && this.mqlHandler) {
69
+ this.mql.removeEventListener("change", this.mqlHandler);
70
+ }
71
+ }
72
+ };
73
+
74
+ exports.Haptics = Haptics;
75
+ //# sourceMappingURL=index.cjs.map
76
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/haptics.ts"],"names":["PRESETS","isVibrationSupported","isIOS","toVibrateSequence","schedulePattern"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n\tPRESETS,\n\tisVibrationSupported,\n\tisIOS,\n\ttoVibrateSequence,\n\tschedulePattern,\n} from \"@haptics/core\";\nimport type { HapticPattern, PresetName } from \"@haptics/core\";\n\nexport interface HapticsOptions {\n\t/**\n\t * Element to delegate click events from. The capture-phase listener\n\t * is registered on this element. Default: document.\n\t */\n\tdelegateFrom?: Element | Document;\n\t/**\n\t * CSS selector for elements that trigger haptics on click.\n\t * Default: \"[data-haptic]\"\n\t */\n\tselector?: string;\n\t/** Custom patterns merged with built-in presets. */\n\tpatterns?: Record<string, HapticPattern>;\n\t/** Suppress haptics when prefers-reduced-motion is active. Default: true */\n\trespectReducedMotion?: boolean;\n}\n\n/**\n * Zero-framework haptic feedback controller.\n *\n * Registers a capture-phase click listener that intercepts clicks on\n * elements matching the selector, reads the pattern name from data-haptic,\n * and fires the appropriate haptic feedback.\n *\n * Works with HTMX, Alpine.js, Stimulus, plain HTML — anything.\n */\nexport class Haptics {\n\tprivate readonly patterns: Record<string, HapticPattern>;\n\tprivate readonly respectReducedMotion: boolean;\n\tprivate readonly delegateFrom: Element | Document;\n\tprivate readonly selector: string;\n\tprivate prefersReducedMotion = false;\n\tprivate destroyed = false;\n\tprivate readonly clickHandler: (e: Event) => void;\n\tprivate mqlHandler: ((e: MediaQueryListEvent) => void) | null = null;\n\tprivate mql: MediaQueryList | null = null;\n\n\treadonly isSupported: boolean;\n\treadonly isIOSSupported: boolean;\n\n\tconstructor(options: HapticsOptions = {}) {\n\t\tthis.patterns = { ...PRESETS, ...options.patterns };\n\t\tthis.respectReducedMotion = options.respectReducedMotion ?? true;\n\t\tthis.delegateFrom = options.delegateFrom ?? document;\n\t\tthis.selector = options.selector ?? \"[data-haptic]\";\n\t\tthis.isSupported = isVibrationSupported();\n\t\tthis.isIOSSupported = isIOS();\n\n\t\tif (\n\t\t\tthis.respectReducedMotion &&\n\t\t\ttypeof window !== \"undefined\" &&\n\t\t\twindow.matchMedia\n\t\t) {\n\t\t\tthis.mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\t\tthis.prefersReducedMotion = this.mql.matches;\n\t\t\tthis.mqlHandler = (e: MediaQueryListEvent) => {\n\t\t\t\tthis.prefersReducedMotion = e.matches;\n\t\t\t};\n\t\t\tthis.mql.addEventListener(\"change\", this.mqlHandler);\n\t\t}\n\n\t\tthis.clickHandler = (e: Event) => {\n\t\t\tif (this.destroyed) return;\n\t\t\tif (this.respectReducedMotion && this.prefersReducedMotion) return;\n\n\t\t\tconst target = (e.target as Element)?.closest(this.selector);\n\t\t\tif (!target) return;\n\n\t\t\tconst action = target.getAttribute(\"data-haptic\");\n\t\t\tif (!action || !(action in this.patterns)) return;\n\n\t\t\tconst pattern = this.patterns[action];\n\t\t\tif (this.isSupported) {\n\t\t\t\tnavigator.vibrate(toVibrateSequence(pattern));\n\t\t\t} else if (this.isIOSSupported) {\n\t\t\t\tschedulePattern(pattern);\n\t\t\t}\n\t\t};\n\n\t\tthis.delegateFrom.addEventListener(\"click\", this.clickHandler, {\n\t\t\tcapture: true,\n\t\t\tpassive: true,\n\t\t});\n\t}\n\n\t/** Trigger haptic feedback imperatively by pattern name. */\n\ttrigger(action: PresetName | (string & {})): void {\n\t\tif (this.destroyed) return;\n\t\tif (this.respectReducedMotion && this.prefersReducedMotion) return;\n\n\t\tconst pattern = this.patterns[action];\n\t\tif (!pattern) return;\n\n\t\tif (this.isSupported) {\n\t\t\tnavigator.vibrate(toVibrateSequence(pattern));\n\t\t} else if (this.isIOSSupported) {\n\t\t\tschedulePattern(pattern);\n\t\t}\n\t}\n\n\t/** Cancel active vibration (Android only). */\n\tcancel(): void {\n\t\tif (this.isSupported) navigator.vibrate(0);\n\t}\n\n\t/** Remove all listeners and clean up. */\n\tdestroy(): void {\n\t\tif (this.destroyed) return;\n\t\tthis.destroyed = true;\n\n\t\tthis.delegateFrom.removeEventListener(\"click\", this.clickHandler, {\n\t\t\tcapture: true,\n\t\t} as EventListenerOptions);\n\n\t\tif (this.mql && this.mqlHandler) {\n\t\t\tthis.mql.removeEventListener(\"change\", this.mqlHandler);\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { HapticPattern, PresetName } from '@haptics/core';
2
+
3
+ interface HapticsOptions {
4
+ /**
5
+ * Element to delegate click events from. The capture-phase listener
6
+ * is registered on this element. Default: document.
7
+ */
8
+ delegateFrom?: Element | Document;
9
+ /**
10
+ * CSS selector for elements that trigger haptics on click.
11
+ * Default: "[data-haptic]"
12
+ */
13
+ selector?: string;
14
+ /** Custom patterns merged with built-in presets. */
15
+ patterns?: Record<string, HapticPattern>;
16
+ /** Suppress haptics when prefers-reduced-motion is active. Default: true */
17
+ respectReducedMotion?: boolean;
18
+ }
19
+ /**
20
+ * Zero-framework haptic feedback controller.
21
+ *
22
+ * Registers a capture-phase click listener that intercepts clicks on
23
+ * elements matching the selector, reads the pattern name from data-haptic,
24
+ * and fires the appropriate haptic feedback.
25
+ *
26
+ * Works with HTMX, Alpine.js, Stimulus, plain HTML — anything.
27
+ */
28
+ declare class Haptics {
29
+ private readonly patterns;
30
+ private readonly respectReducedMotion;
31
+ private readonly delegateFrom;
32
+ private readonly selector;
33
+ private prefersReducedMotion;
34
+ private destroyed;
35
+ private readonly clickHandler;
36
+ private mqlHandler;
37
+ private mql;
38
+ readonly isSupported: boolean;
39
+ readonly isIOSSupported: boolean;
40
+ constructor(options?: HapticsOptions);
41
+ /** Trigger haptic feedback imperatively by pattern name. */
42
+ trigger(action: PresetName | (string & {})): void;
43
+ /** Cancel active vibration (Android only). */
44
+ cancel(): void;
45
+ /** Remove all listeners and clean up. */
46
+ destroy(): void;
47
+ }
48
+
49
+ export { Haptics, type HapticsOptions };
@@ -0,0 +1,49 @@
1
+ import { HapticPattern, PresetName } from '@haptics/core';
2
+
3
+ interface HapticsOptions {
4
+ /**
5
+ * Element to delegate click events from. The capture-phase listener
6
+ * is registered on this element. Default: document.
7
+ */
8
+ delegateFrom?: Element | Document;
9
+ /**
10
+ * CSS selector for elements that trigger haptics on click.
11
+ * Default: "[data-haptic]"
12
+ */
13
+ selector?: string;
14
+ /** Custom patterns merged with built-in presets. */
15
+ patterns?: Record<string, HapticPattern>;
16
+ /** Suppress haptics when prefers-reduced-motion is active. Default: true */
17
+ respectReducedMotion?: boolean;
18
+ }
19
+ /**
20
+ * Zero-framework haptic feedback controller.
21
+ *
22
+ * Registers a capture-phase click listener that intercepts clicks on
23
+ * elements matching the selector, reads the pattern name from data-haptic,
24
+ * and fires the appropriate haptic feedback.
25
+ *
26
+ * Works with HTMX, Alpine.js, Stimulus, plain HTML — anything.
27
+ */
28
+ declare class Haptics {
29
+ private readonly patterns;
30
+ private readonly respectReducedMotion;
31
+ private readonly delegateFrom;
32
+ private readonly selector;
33
+ private prefersReducedMotion;
34
+ private destroyed;
35
+ private readonly clickHandler;
36
+ private mqlHandler;
37
+ private mql;
38
+ readonly isSupported: boolean;
39
+ readonly isIOSSupported: boolean;
40
+ constructor(options?: HapticsOptions);
41
+ /** Trigger haptic feedback imperatively by pattern name. */
42
+ trigger(action: PresetName | (string & {})): void;
43
+ /** Cancel active vibration (Android only). */
44
+ cancel(): void;
45
+ /** Remove all listeners and clean up. */
46
+ destroy(): void;
47
+ }
48
+
49
+ export { Haptics, type HapticsOptions };
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ import { PRESETS, isVibrationSupported, isIOS, toVibrateSequence, schedulePattern } from '@haptics/core';
2
+
3
+ // src/haptics.ts
4
+ var Haptics = class {
5
+ constructor(options = {}) {
6
+ this.prefersReducedMotion = false;
7
+ this.destroyed = false;
8
+ this.mqlHandler = null;
9
+ this.mql = null;
10
+ this.patterns = { ...PRESETS, ...options.patterns };
11
+ this.respectReducedMotion = options.respectReducedMotion ?? true;
12
+ this.delegateFrom = options.delegateFrom ?? document;
13
+ this.selector = options.selector ?? "[data-haptic]";
14
+ this.isSupported = isVibrationSupported();
15
+ this.isIOSSupported = isIOS();
16
+ if (this.respectReducedMotion && typeof window !== "undefined" && window.matchMedia) {
17
+ this.mql = window.matchMedia("(prefers-reduced-motion: reduce)");
18
+ this.prefersReducedMotion = this.mql.matches;
19
+ this.mqlHandler = (e) => {
20
+ this.prefersReducedMotion = e.matches;
21
+ };
22
+ this.mql.addEventListener("change", this.mqlHandler);
23
+ }
24
+ this.clickHandler = (e) => {
25
+ if (this.destroyed) return;
26
+ if (this.respectReducedMotion && this.prefersReducedMotion) return;
27
+ const target = e.target?.closest(this.selector);
28
+ if (!target) return;
29
+ const action = target.getAttribute("data-haptic");
30
+ if (!action || !(action in this.patterns)) return;
31
+ const pattern = this.patterns[action];
32
+ if (this.isSupported) {
33
+ navigator.vibrate(toVibrateSequence(pattern));
34
+ } else if (this.isIOSSupported) {
35
+ schedulePattern(pattern);
36
+ }
37
+ };
38
+ this.delegateFrom.addEventListener("click", this.clickHandler, {
39
+ capture: true,
40
+ passive: true
41
+ });
42
+ }
43
+ /** Trigger haptic feedback imperatively by pattern name. */
44
+ trigger(action) {
45
+ if (this.destroyed) return;
46
+ if (this.respectReducedMotion && this.prefersReducedMotion) return;
47
+ const pattern = this.patterns[action];
48
+ if (!pattern) return;
49
+ if (this.isSupported) {
50
+ navigator.vibrate(toVibrateSequence(pattern));
51
+ } else if (this.isIOSSupported) {
52
+ schedulePattern(pattern);
53
+ }
54
+ }
55
+ /** Cancel active vibration (Android only). */
56
+ cancel() {
57
+ if (this.isSupported) navigator.vibrate(0);
58
+ }
59
+ /** Remove all listeners and clean up. */
60
+ destroy() {
61
+ if (this.destroyed) return;
62
+ this.destroyed = true;
63
+ this.delegateFrom.removeEventListener("click", this.clickHandler, {
64
+ capture: true
65
+ });
66
+ if (this.mql && this.mqlHandler) {
67
+ this.mql.removeEventListener("change", this.mqlHandler);
68
+ }
69
+ }
70
+ };
71
+
72
+ export { Haptics };
73
+ //# sourceMappingURL=index.js.map
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/haptics.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n\tPRESETS,\n\tisVibrationSupported,\n\tisIOS,\n\ttoVibrateSequence,\n\tschedulePattern,\n} from \"@haptics/core\";\nimport type { HapticPattern, PresetName } from \"@haptics/core\";\n\nexport interface HapticsOptions {\n\t/**\n\t * Element to delegate click events from. The capture-phase listener\n\t * is registered on this element. Default: document.\n\t */\n\tdelegateFrom?: Element | Document;\n\t/**\n\t * CSS selector for elements that trigger haptics on click.\n\t * Default: \"[data-haptic]\"\n\t */\n\tselector?: string;\n\t/** Custom patterns merged with built-in presets. */\n\tpatterns?: Record<string, HapticPattern>;\n\t/** Suppress haptics when prefers-reduced-motion is active. Default: true */\n\trespectReducedMotion?: boolean;\n}\n\n/**\n * Zero-framework haptic feedback controller.\n *\n * Registers a capture-phase click listener that intercepts clicks on\n * elements matching the selector, reads the pattern name from data-haptic,\n * and fires the appropriate haptic feedback.\n *\n * Works with HTMX, Alpine.js, Stimulus, plain HTML — anything.\n */\nexport class Haptics {\n\tprivate readonly patterns: Record<string, HapticPattern>;\n\tprivate readonly respectReducedMotion: boolean;\n\tprivate readonly delegateFrom: Element | Document;\n\tprivate readonly selector: string;\n\tprivate prefersReducedMotion = false;\n\tprivate destroyed = false;\n\tprivate readonly clickHandler: (e: Event) => void;\n\tprivate mqlHandler: ((e: MediaQueryListEvent) => void) | null = null;\n\tprivate mql: MediaQueryList | null = null;\n\n\treadonly isSupported: boolean;\n\treadonly isIOSSupported: boolean;\n\n\tconstructor(options: HapticsOptions = {}) {\n\t\tthis.patterns = { ...PRESETS, ...options.patterns };\n\t\tthis.respectReducedMotion = options.respectReducedMotion ?? true;\n\t\tthis.delegateFrom = options.delegateFrom ?? document;\n\t\tthis.selector = options.selector ?? \"[data-haptic]\";\n\t\tthis.isSupported = isVibrationSupported();\n\t\tthis.isIOSSupported = isIOS();\n\n\t\tif (\n\t\t\tthis.respectReducedMotion &&\n\t\t\ttypeof window !== \"undefined\" &&\n\t\t\twindow.matchMedia\n\t\t) {\n\t\t\tthis.mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\t\tthis.prefersReducedMotion = this.mql.matches;\n\t\t\tthis.mqlHandler = (e: MediaQueryListEvent) => {\n\t\t\t\tthis.prefersReducedMotion = e.matches;\n\t\t\t};\n\t\t\tthis.mql.addEventListener(\"change\", this.mqlHandler);\n\t\t}\n\n\t\tthis.clickHandler = (e: Event) => {\n\t\t\tif (this.destroyed) return;\n\t\t\tif (this.respectReducedMotion && this.prefersReducedMotion) return;\n\n\t\t\tconst target = (e.target as Element)?.closest(this.selector);\n\t\t\tif (!target) return;\n\n\t\t\tconst action = target.getAttribute(\"data-haptic\");\n\t\t\tif (!action || !(action in this.patterns)) return;\n\n\t\t\tconst pattern = this.patterns[action];\n\t\t\tif (this.isSupported) {\n\t\t\t\tnavigator.vibrate(toVibrateSequence(pattern));\n\t\t\t} else if (this.isIOSSupported) {\n\t\t\t\tschedulePattern(pattern);\n\t\t\t}\n\t\t};\n\n\t\tthis.delegateFrom.addEventListener(\"click\", this.clickHandler, {\n\t\t\tcapture: true,\n\t\t\tpassive: true,\n\t\t});\n\t}\n\n\t/** Trigger haptic feedback imperatively by pattern name. */\n\ttrigger(action: PresetName | (string & {})): void {\n\t\tif (this.destroyed) return;\n\t\tif (this.respectReducedMotion && this.prefersReducedMotion) return;\n\n\t\tconst pattern = this.patterns[action];\n\t\tif (!pattern) return;\n\n\t\tif (this.isSupported) {\n\t\t\tnavigator.vibrate(toVibrateSequence(pattern));\n\t\t} else if (this.isIOSSupported) {\n\t\t\tschedulePattern(pattern);\n\t\t}\n\t}\n\n\t/** Cancel active vibration (Android only). */\n\tcancel(): void {\n\t\tif (this.isSupported) navigator.vibrate(0);\n\t}\n\n\t/** Remove all listeners and clean up. */\n\tdestroy(): void {\n\t\tif (this.destroyed) return;\n\t\tthis.destroyed = true;\n\n\t\tthis.delegateFrom.removeEventListener(\"click\", this.clickHandler, {\n\t\t\tcapture: true,\n\t\t} as EventListenerOptions);\n\n\t\tif (this.mql && this.mqlHandler) {\n\t\t\tthis.mql.removeEventListener(\"change\", this.mqlHandler);\n\t\t}\n\t}\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@haptics/vanilla",
3
+ "version": "1.0.0",
4
+ "description": "Zero-framework haptic feedback controller. Event delegation, imperative API, works with any stack.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "dev": "tsup --watch",
28
+ "type-check": "tsc --noEmit",
29
+ "test": "vitest run"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/howdoiusekeyboard/haptics.git",
37
+ "directory": "packages/vanilla"
38
+ },
39
+ "author": "Howdoiusekeyboard",
40
+ "license": "MIT",
41
+ "keywords": [
42
+ "haptics",
43
+ "vanilla",
44
+ "haptic-feedback",
45
+ "vibration",
46
+ "ios",
47
+ "android",
48
+ "no-framework",
49
+ "htmx",
50
+ "alpine"
51
+ ],
52
+ "dependencies": {
53
+ "@haptics/core": "^1.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "jsdom": "^29.0.0",
57
+ "tsup": "^8.0.0",
58
+ "vitest": "^4.1.0"
59
+ }
60
+ }