@haptics/svelte 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 +119 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +73 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +115 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@haptics/core');
|
|
4
|
+
|
|
5
|
+
// src/context.ts
|
|
6
|
+
var _config = null;
|
|
7
|
+
function setupHaptics(options = {}) {
|
|
8
|
+
const patterns = {
|
|
9
|
+
...core.PRESETS,
|
|
10
|
+
...options.patterns
|
|
11
|
+
};
|
|
12
|
+
const respectReducedMotion = options.respectReducedMotion ?? true;
|
|
13
|
+
const config = { patterns, respectReducedMotion };
|
|
14
|
+
_config = config;
|
|
15
|
+
if (typeof document !== "undefined" && core.isIOS()) {
|
|
16
|
+
let prefersReducedMotion = false;
|
|
17
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
18
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
19
|
+
prefersReducedMotion = mql.matches;
|
|
20
|
+
mql.addEventListener("change", (e) => {
|
|
21
|
+
prefersReducedMotion = e.matches;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const handler = (e) => {
|
|
25
|
+
if (respectReducedMotion && prefersReducedMotion) return;
|
|
26
|
+
const target = e.target?.closest("[data-haptic]");
|
|
27
|
+
if (!target) return;
|
|
28
|
+
const action = target.getAttribute("data-haptic");
|
|
29
|
+
if (action && action in patterns) {
|
|
30
|
+
core.schedulePattern(patterns[action]);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
document.addEventListener("click", handler, {
|
|
34
|
+
capture: true,
|
|
35
|
+
passive: true
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getHapticsConfig() {
|
|
40
|
+
return _config;
|
|
41
|
+
}
|
|
42
|
+
function haptic(node, action) {
|
|
43
|
+
let currentAction = action;
|
|
44
|
+
const config = getHapticsConfig();
|
|
45
|
+
const patterns = config?.patterns ?? core.PRESETS;
|
|
46
|
+
const respectReducedMotion = config?.respectReducedMotion ?? true;
|
|
47
|
+
let prefersReducedMotion = false;
|
|
48
|
+
let mqlCleanup = null;
|
|
49
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
50
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
51
|
+
prefersReducedMotion = mql.matches;
|
|
52
|
+
const onChange = (e) => {
|
|
53
|
+
prefersReducedMotion = e.matches;
|
|
54
|
+
};
|
|
55
|
+
mql.addEventListener("change", onChange);
|
|
56
|
+
mqlCleanup = () => mql.removeEventListener("change", onChange);
|
|
57
|
+
}
|
|
58
|
+
node.setAttribute("data-haptic", currentAction);
|
|
59
|
+
const handler = () => {
|
|
60
|
+
if (respectReducedMotion && prefersReducedMotion) return;
|
|
61
|
+
const pattern = patterns[currentAction];
|
|
62
|
+
if (!pattern) return;
|
|
63
|
+
if (core.isVibrationSupported()) {
|
|
64
|
+
navigator.vibrate(core.toVibrateSequence(pattern));
|
|
65
|
+
} else if (core.isIOS() && !config) {
|
|
66
|
+
core.schedulePattern(pattern);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
node.addEventListener("click", handler);
|
|
70
|
+
return {
|
|
71
|
+
update(newAction) {
|
|
72
|
+
currentAction = newAction;
|
|
73
|
+
node.setAttribute("data-haptic", newAction);
|
|
74
|
+
},
|
|
75
|
+
destroy() {
|
|
76
|
+
node.removeEventListener("click", handler);
|
|
77
|
+
node.removeAttribute("data-haptic");
|
|
78
|
+
mqlCleanup?.();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function createHaptics() {
|
|
83
|
+
const config = getHapticsConfig();
|
|
84
|
+
const patterns = config?.patterns ?? core.PRESETS;
|
|
85
|
+
const respectReducedMotion = config?.respectReducedMotion ?? true;
|
|
86
|
+
let prefersReducedMotion = false;
|
|
87
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
88
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
89
|
+
prefersReducedMotion = mql.matches;
|
|
90
|
+
mql.addEventListener("change", (e) => {
|
|
91
|
+
prefersReducedMotion = e.matches;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const trigger = (action) => {
|
|
95
|
+
if (respectReducedMotion && prefersReducedMotion) return;
|
|
96
|
+
const pattern = patterns[action];
|
|
97
|
+
if (!pattern) return;
|
|
98
|
+
if (core.isVibrationSupported()) {
|
|
99
|
+
navigator.vibrate(core.toVibrateSequence(pattern));
|
|
100
|
+
} else if (core.isIOS()) {
|
|
101
|
+
core.schedulePattern(pattern);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const cancel = () => {
|
|
105
|
+
if (core.isVibrationSupported()) navigator.vibrate(0);
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
trigger,
|
|
109
|
+
cancel,
|
|
110
|
+
isSupported: core.isVibrationSupported(),
|
|
111
|
+
isIOSSupported: core.isIOS()
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
exports.createHaptics = createHaptics;
|
|
116
|
+
exports.haptic = haptic;
|
|
117
|
+
exports.setupHaptics = setupHaptics;
|
|
118
|
+
//# sourceMappingURL=index.cjs.map
|
|
119
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.ts","../src/action.ts","../src/create-haptics.ts"],"names":["PRESETS","isIOS","schedulePattern","isVibrationSupported","toVibrateSequence"],"mappings":"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","file":"index.cjs","sourcesContent":["import { PRESETS, isIOS, schedulePattern } from \"@haptics/core\";\nimport type { HapticPattern } from \"@haptics/core\";\n\nexport interface HapticsConfig {\n\tpatterns: Record<string, HapticPattern>;\n\trespectReducedMotion: boolean;\n}\n\nlet _config: HapticsConfig | null = null;\n\n/**\n * Initialize haptics context in a Svelte component tree.\n * Call this in your root layout's <script> block.\n *\n * Registers a capture-phase click listener on document for iOS haptics\n * and sets up context for child components using `use:haptic` and `createHaptics`.\n *\n * @example\n * ```svelte\n * <script>\n * import { setupHaptics } from '@haptics/svelte';\n * setupHaptics({ patterns: { 'my-buzz': [{ duration: 30 }] } });\n * </script>\n * <slot />\n * ```\n */\nexport function setupHaptics(\n\toptions: {\n\t\tpatterns?: Record<string, HapticPattern>;\n\t\trespectReducedMotion?: boolean;\n\t} = {},\n): void {\n\tconst patterns: Record<string, HapticPattern> = {\n\t\t...PRESETS,\n\t\t...options.patterns,\n\t};\n\tconst respectReducedMotion = options.respectReducedMotion ?? true;\n\tconst config: HapticsConfig = { patterns, respectReducedMotion };\n\t_config = config;\n\n\tif (typeof document !== \"undefined\" && isIOS()) {\n\t\tlet prefersReducedMotion = false;\n\n\t\tif (typeof window !== \"undefined\" && window.matchMedia) {\n\t\t\tconst mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\t\tprefersReducedMotion = mql.matches;\n\t\t\tmql.addEventListener(\"change\", (e) => {\n\t\t\t\tprefersReducedMotion = e.matches;\n\t\t\t});\n\t\t}\n\n\t\tconst handler = (e: MouseEvent) => {\n\t\t\tif (respectReducedMotion && prefersReducedMotion) return;\n\t\t\tconst target = (e.target as Element)?.closest(\"[data-haptic]\");\n\t\t\tif (!target) return;\n\t\t\tconst action = target.getAttribute(\"data-haptic\");\n\t\t\tif (action && action in patterns) {\n\t\t\t\tschedulePattern(patterns[action]);\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"click\", handler, {\n\t\t\tcapture: true,\n\t\t\tpassive: true,\n\t\t});\n\t}\n}\n\nexport function getHapticsConfig(): HapticsConfig | null {\n\treturn _config;\n}\n\n/** @internal Reset config for testing only. */\nexport function _resetConfig(): void {\n\t_config = null;\n}\n","import type { HapticPattern } from \"@haptics/core\";\nimport {\n\tPRESETS,\n\tisIOS,\n\tisVibrationSupported,\n\ttoVibrateSequence,\n\tschedulePattern,\n} from \"@haptics/core\";\nimport { getHapticsConfig } from \"./context\";\n\ntype HapticAction = string;\n\n/**\n * Svelte action for declarative haptic feedback.\n *\n * @example\n * ```svelte\n * <script>\n * import { haptic } from '@haptics/svelte';\n * </script>\n * <button use:haptic={'success'}>Save</button>\n * <button use:haptic={'impact-heavy'}>Delete</button>\n * ```\n *\n * On iOS: sets data-haptic for the capture-phase listener from setupHaptics().\n * On Android: calls navigator.vibrate() directly in the click handler.\n * Svelte actions have native event access — no gesture chain issues.\n */\nexport function haptic(\n\tnode: HTMLElement,\n\taction: HapticAction,\n): { update: (action: HapticAction) => void; destroy: () => void } {\n\tlet currentAction = action;\n\tconst config = getHapticsConfig();\n\tconst patterns: Record<string, HapticPattern> = config?.patterns ?? PRESETS;\n\tconst respectReducedMotion = config?.respectReducedMotion ?? true;\n\n\tlet prefersReducedMotion = false;\n\tlet mqlCleanup: (() => void) | null = null;\n\n\tif (typeof window !== \"undefined\" && window.matchMedia) {\n\t\tconst mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\tprefersReducedMotion = mql.matches;\n\t\tconst onChange = (e: MediaQueryListEvent) => {\n\t\t\tprefersReducedMotion = e.matches;\n\t\t};\n\t\tmql.addEventListener(\"change\", onChange);\n\t\tmqlCleanup = () => mql.removeEventListener(\"change\", onChange);\n\t}\n\n\tnode.setAttribute(\"data-haptic\", currentAction);\n\n\tconst handler = () => {\n\t\tif (respectReducedMotion && prefersReducedMotion) return;\n\t\tconst pattern = patterns[currentAction];\n\t\tif (!pattern) return;\n\n\t\tif (isVibrationSupported()) {\n\t\t\tnavigator.vibrate(toVibrateSequence(pattern));\n\t\t} else if (isIOS() && !config) {\n\t\t\tschedulePattern(pattern);\n\t\t}\n\t};\n\n\tnode.addEventListener(\"click\", handler);\n\n\treturn {\n\t\tupdate(newAction: HapticAction) {\n\t\t\tcurrentAction = newAction;\n\t\t\tnode.setAttribute(\"data-haptic\", newAction);\n\t\t},\n\t\tdestroy() {\n\t\t\tnode.removeEventListener(\"click\", handler);\n\t\t\tnode.removeAttribute(\"data-haptic\");\n\t\t\tmqlCleanup?.();\n\t\t},\n\t};\n}\n","import {\n\tPRESETS,\n\tisVibrationSupported,\n\tisIOS,\n\ttoVibrateSequence,\n\tschedulePattern,\n} from \"@haptics/core\";\nimport type { PresetName } from \"@haptics/core\";\nimport { getHapticsConfig } from \"./context\";\n\n/**\n * Create an imperative haptics controller.\n *\n * Can be called inside a Svelte component's <script> block (will use\n * setupHaptics context if available) or outside components (falls back\n * to built-in presets).\n *\n * @example\n * ```svelte\n * <script>\n * import { createHaptics } from '@haptics/svelte';\n * const haptics = createHaptics();\n * function handleSave() { haptics.trigger('success'); }\n * </script>\n * ```\n */\nexport function createHaptics() {\n\tconst config = getHapticsConfig();\n\tconst patterns = config?.patterns ?? PRESETS;\n\tconst respectReducedMotion = config?.respectReducedMotion ?? true;\n\n\tlet prefersReducedMotion = false;\n\tif (typeof window !== \"undefined\" && window.matchMedia) {\n\t\tconst mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\tprefersReducedMotion = mql.matches;\n\t\tmql.addEventListener(\"change\", (e) => {\n\t\t\tprefersReducedMotion = e.matches;\n\t\t});\n\t}\n\n\tconst trigger = (action: PresetName | (string & {})) => {\n\t\tif (respectReducedMotion && prefersReducedMotion) return;\n\n\t\tconst pattern = patterns[action as keyof typeof patterns];\n\t\tif (!pattern) return;\n\n\t\tif (isVibrationSupported()) {\n\t\t\tnavigator.vibrate(toVibrateSequence(pattern));\n\t\t} else if (isIOS()) {\n\t\t\tschedulePattern(pattern);\n\t\t}\n\t};\n\n\tconst cancel = () => {\n\t\tif (isVibrationSupported()) navigator.vibrate(0);\n\t};\n\n\treturn {\n\t\ttrigger,\n\t\tcancel,\n\t\tisSupported: isVibrationSupported(),\n\t\tisIOSSupported: isIOS(),\n\t};\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { HapticPattern, PresetName } from '@haptics/core';
|
|
2
|
+
|
|
3
|
+
interface HapticsConfig {
|
|
4
|
+
patterns: Record<string, HapticPattern>;
|
|
5
|
+
respectReducedMotion: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Initialize haptics context in a Svelte component tree.
|
|
9
|
+
* Call this in your root layout's <script> block.
|
|
10
|
+
*
|
|
11
|
+
* Registers a capture-phase click listener on document for iOS haptics
|
|
12
|
+
* and sets up context for child components using `use:haptic` and `createHaptics`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```svelte
|
|
16
|
+
* <script>
|
|
17
|
+
* import { setupHaptics } from '@haptics/svelte';
|
|
18
|
+
* setupHaptics({ patterns: { 'my-buzz': [{ duration: 30 }] } });
|
|
19
|
+
* </script>
|
|
20
|
+
* <slot />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare function setupHaptics(options?: {
|
|
24
|
+
patterns?: Record<string, HapticPattern>;
|
|
25
|
+
respectReducedMotion?: boolean;
|
|
26
|
+
}): void;
|
|
27
|
+
|
|
28
|
+
type HapticAction = string;
|
|
29
|
+
/**
|
|
30
|
+
* Svelte action for declarative haptic feedback.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```svelte
|
|
34
|
+
* <script>
|
|
35
|
+
* import { haptic } from '@haptics/svelte';
|
|
36
|
+
* </script>
|
|
37
|
+
* <button use:haptic={'success'}>Save</button>
|
|
38
|
+
* <button use:haptic={'impact-heavy'}>Delete</button>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* On iOS: sets data-haptic for the capture-phase listener from setupHaptics().
|
|
42
|
+
* On Android: calls navigator.vibrate() directly in the click handler.
|
|
43
|
+
* Svelte actions have native event access — no gesture chain issues.
|
|
44
|
+
*/
|
|
45
|
+
declare function haptic(node: HTMLElement, action: HapticAction): {
|
|
46
|
+
update: (action: HapticAction) => void;
|
|
47
|
+
destroy: () => void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create an imperative haptics controller.
|
|
52
|
+
*
|
|
53
|
+
* Can be called inside a Svelte component's <script> block (will use
|
|
54
|
+
* setupHaptics context if available) or outside components (falls back
|
|
55
|
+
* to built-in presets).
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```svelte
|
|
59
|
+
* <script>
|
|
60
|
+
* import { createHaptics } from '@haptics/svelte';
|
|
61
|
+
* const haptics = createHaptics();
|
|
62
|
+
* function handleSave() { haptics.trigger('success'); }
|
|
63
|
+
* </script>
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function createHaptics(): {
|
|
67
|
+
trigger: (action: PresetName | (string & {})) => void;
|
|
68
|
+
cancel: () => void;
|
|
69
|
+
isSupported: boolean;
|
|
70
|
+
isIOSSupported: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export { type HapticsConfig, createHaptics, haptic, setupHaptics };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { HapticPattern, PresetName } from '@haptics/core';
|
|
2
|
+
|
|
3
|
+
interface HapticsConfig {
|
|
4
|
+
patterns: Record<string, HapticPattern>;
|
|
5
|
+
respectReducedMotion: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Initialize haptics context in a Svelte component tree.
|
|
9
|
+
* Call this in your root layout's <script> block.
|
|
10
|
+
*
|
|
11
|
+
* Registers a capture-phase click listener on document for iOS haptics
|
|
12
|
+
* and sets up context for child components using `use:haptic` and `createHaptics`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```svelte
|
|
16
|
+
* <script>
|
|
17
|
+
* import { setupHaptics } from '@haptics/svelte';
|
|
18
|
+
* setupHaptics({ patterns: { 'my-buzz': [{ duration: 30 }] } });
|
|
19
|
+
* </script>
|
|
20
|
+
* <slot />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare function setupHaptics(options?: {
|
|
24
|
+
patterns?: Record<string, HapticPattern>;
|
|
25
|
+
respectReducedMotion?: boolean;
|
|
26
|
+
}): void;
|
|
27
|
+
|
|
28
|
+
type HapticAction = string;
|
|
29
|
+
/**
|
|
30
|
+
* Svelte action for declarative haptic feedback.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```svelte
|
|
34
|
+
* <script>
|
|
35
|
+
* import { haptic } from '@haptics/svelte';
|
|
36
|
+
* </script>
|
|
37
|
+
* <button use:haptic={'success'}>Save</button>
|
|
38
|
+
* <button use:haptic={'impact-heavy'}>Delete</button>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* On iOS: sets data-haptic for the capture-phase listener from setupHaptics().
|
|
42
|
+
* On Android: calls navigator.vibrate() directly in the click handler.
|
|
43
|
+
* Svelte actions have native event access — no gesture chain issues.
|
|
44
|
+
*/
|
|
45
|
+
declare function haptic(node: HTMLElement, action: HapticAction): {
|
|
46
|
+
update: (action: HapticAction) => void;
|
|
47
|
+
destroy: () => void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create an imperative haptics controller.
|
|
52
|
+
*
|
|
53
|
+
* Can be called inside a Svelte component's <script> block (will use
|
|
54
|
+
* setupHaptics context if available) or outside components (falls back
|
|
55
|
+
* to built-in presets).
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```svelte
|
|
59
|
+
* <script>
|
|
60
|
+
* import { createHaptics } from '@haptics/svelte';
|
|
61
|
+
* const haptics = createHaptics();
|
|
62
|
+
* function handleSave() { haptics.trigger('success'); }
|
|
63
|
+
* </script>
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function createHaptics(): {
|
|
67
|
+
trigger: (action: PresetName | (string & {})) => void;
|
|
68
|
+
cancel: () => void;
|
|
69
|
+
isSupported: boolean;
|
|
70
|
+
isIOSSupported: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export { type HapticsConfig, createHaptics, haptic, setupHaptics };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { PRESETS, isIOS, isVibrationSupported, toVibrateSequence, schedulePattern } from '@haptics/core';
|
|
2
|
+
|
|
3
|
+
// src/context.ts
|
|
4
|
+
var _config = null;
|
|
5
|
+
function setupHaptics(options = {}) {
|
|
6
|
+
const patterns = {
|
|
7
|
+
...PRESETS,
|
|
8
|
+
...options.patterns
|
|
9
|
+
};
|
|
10
|
+
const respectReducedMotion = options.respectReducedMotion ?? true;
|
|
11
|
+
const config = { patterns, respectReducedMotion };
|
|
12
|
+
_config = config;
|
|
13
|
+
if (typeof document !== "undefined" && isIOS()) {
|
|
14
|
+
let prefersReducedMotion = false;
|
|
15
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
16
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
17
|
+
prefersReducedMotion = mql.matches;
|
|
18
|
+
mql.addEventListener("change", (e) => {
|
|
19
|
+
prefersReducedMotion = e.matches;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const handler = (e) => {
|
|
23
|
+
if (respectReducedMotion && prefersReducedMotion) return;
|
|
24
|
+
const target = e.target?.closest("[data-haptic]");
|
|
25
|
+
if (!target) return;
|
|
26
|
+
const action = target.getAttribute("data-haptic");
|
|
27
|
+
if (action && action in patterns) {
|
|
28
|
+
schedulePattern(patterns[action]);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
document.addEventListener("click", handler, {
|
|
32
|
+
capture: true,
|
|
33
|
+
passive: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function getHapticsConfig() {
|
|
38
|
+
return _config;
|
|
39
|
+
}
|
|
40
|
+
function haptic(node, action) {
|
|
41
|
+
let currentAction = action;
|
|
42
|
+
const config = getHapticsConfig();
|
|
43
|
+
const patterns = config?.patterns ?? PRESETS;
|
|
44
|
+
const respectReducedMotion = config?.respectReducedMotion ?? true;
|
|
45
|
+
let prefersReducedMotion = false;
|
|
46
|
+
let mqlCleanup = null;
|
|
47
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
48
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
49
|
+
prefersReducedMotion = mql.matches;
|
|
50
|
+
const onChange = (e) => {
|
|
51
|
+
prefersReducedMotion = e.matches;
|
|
52
|
+
};
|
|
53
|
+
mql.addEventListener("change", onChange);
|
|
54
|
+
mqlCleanup = () => mql.removeEventListener("change", onChange);
|
|
55
|
+
}
|
|
56
|
+
node.setAttribute("data-haptic", currentAction);
|
|
57
|
+
const handler = () => {
|
|
58
|
+
if (respectReducedMotion && prefersReducedMotion) return;
|
|
59
|
+
const pattern = patterns[currentAction];
|
|
60
|
+
if (!pattern) return;
|
|
61
|
+
if (isVibrationSupported()) {
|
|
62
|
+
navigator.vibrate(toVibrateSequence(pattern));
|
|
63
|
+
} else if (isIOS() && !config) {
|
|
64
|
+
schedulePattern(pattern);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
node.addEventListener("click", handler);
|
|
68
|
+
return {
|
|
69
|
+
update(newAction) {
|
|
70
|
+
currentAction = newAction;
|
|
71
|
+
node.setAttribute("data-haptic", newAction);
|
|
72
|
+
},
|
|
73
|
+
destroy() {
|
|
74
|
+
node.removeEventListener("click", handler);
|
|
75
|
+
node.removeAttribute("data-haptic");
|
|
76
|
+
mqlCleanup?.();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function createHaptics() {
|
|
81
|
+
const config = getHapticsConfig();
|
|
82
|
+
const patterns = config?.patterns ?? PRESETS;
|
|
83
|
+
const respectReducedMotion = config?.respectReducedMotion ?? true;
|
|
84
|
+
let prefersReducedMotion = false;
|
|
85
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
86
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
87
|
+
prefersReducedMotion = mql.matches;
|
|
88
|
+
mql.addEventListener("change", (e) => {
|
|
89
|
+
prefersReducedMotion = e.matches;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const trigger = (action) => {
|
|
93
|
+
if (respectReducedMotion && prefersReducedMotion) return;
|
|
94
|
+
const pattern = patterns[action];
|
|
95
|
+
if (!pattern) return;
|
|
96
|
+
if (isVibrationSupported()) {
|
|
97
|
+
navigator.vibrate(toVibrateSequence(pattern));
|
|
98
|
+
} else if (isIOS()) {
|
|
99
|
+
schedulePattern(pattern);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const cancel = () => {
|
|
103
|
+
if (isVibrationSupported()) navigator.vibrate(0);
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
trigger,
|
|
107
|
+
cancel,
|
|
108
|
+
isSupported: isVibrationSupported(),
|
|
109
|
+
isIOSSupported: isIOS()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { createHaptics, haptic, setupHaptics };
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.ts","../src/action.ts","../src/create-haptics.ts"],"names":["PRESETS","isIOS","schedulePattern","isVibrationSupported","toVibrateSequence"],"mappings":"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","file":"index.js","sourcesContent":["import { PRESETS, isIOS, schedulePattern } from \"@haptics/core\";\nimport type { HapticPattern } from \"@haptics/core\";\n\nexport interface HapticsConfig {\n\tpatterns: Record<string, HapticPattern>;\n\trespectReducedMotion: boolean;\n}\n\nlet _config: HapticsConfig | null = null;\n\n/**\n * Initialize haptics context in a Svelte component tree.\n * Call this in your root layout's <script> block.\n *\n * Registers a capture-phase click listener on document for iOS haptics\n * and sets up context for child components using `use:haptic` and `createHaptics`.\n *\n * @example\n * ```svelte\n * <script>\n * import { setupHaptics } from '@haptics/svelte';\n * setupHaptics({ patterns: { 'my-buzz': [{ duration: 30 }] } });\n * </script>\n * <slot />\n * ```\n */\nexport function setupHaptics(\n\toptions: {\n\t\tpatterns?: Record<string, HapticPattern>;\n\t\trespectReducedMotion?: boolean;\n\t} = {},\n): void {\n\tconst patterns: Record<string, HapticPattern> = {\n\t\t...PRESETS,\n\t\t...options.patterns,\n\t};\n\tconst respectReducedMotion = options.respectReducedMotion ?? true;\n\tconst config: HapticsConfig = { patterns, respectReducedMotion };\n\t_config = config;\n\n\tif (typeof document !== \"undefined\" && isIOS()) {\n\t\tlet prefersReducedMotion = false;\n\n\t\tif (typeof window !== \"undefined\" && window.matchMedia) {\n\t\t\tconst mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\t\tprefersReducedMotion = mql.matches;\n\t\t\tmql.addEventListener(\"change\", (e) => {\n\t\t\t\tprefersReducedMotion = e.matches;\n\t\t\t});\n\t\t}\n\n\t\tconst handler = (e: MouseEvent) => {\n\t\t\tif (respectReducedMotion && prefersReducedMotion) return;\n\t\t\tconst target = (e.target as Element)?.closest(\"[data-haptic]\");\n\t\t\tif (!target) return;\n\t\t\tconst action = target.getAttribute(\"data-haptic\");\n\t\t\tif (action && action in patterns) {\n\t\t\t\tschedulePattern(patterns[action]);\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"click\", handler, {\n\t\t\tcapture: true,\n\t\t\tpassive: true,\n\t\t});\n\t}\n}\n\nexport function getHapticsConfig(): HapticsConfig | null {\n\treturn _config;\n}\n\n/** @internal Reset config for testing only. */\nexport function _resetConfig(): void {\n\t_config = null;\n}\n","import type { HapticPattern } from \"@haptics/core\";\nimport {\n\tPRESETS,\n\tisIOS,\n\tisVibrationSupported,\n\ttoVibrateSequence,\n\tschedulePattern,\n} from \"@haptics/core\";\nimport { getHapticsConfig } from \"./context\";\n\ntype HapticAction = string;\n\n/**\n * Svelte action for declarative haptic feedback.\n *\n * @example\n * ```svelte\n * <script>\n * import { haptic } from '@haptics/svelte';\n * </script>\n * <button use:haptic={'success'}>Save</button>\n * <button use:haptic={'impact-heavy'}>Delete</button>\n * ```\n *\n * On iOS: sets data-haptic for the capture-phase listener from setupHaptics().\n * On Android: calls navigator.vibrate() directly in the click handler.\n * Svelte actions have native event access — no gesture chain issues.\n */\nexport function haptic(\n\tnode: HTMLElement,\n\taction: HapticAction,\n): { update: (action: HapticAction) => void; destroy: () => void } {\n\tlet currentAction = action;\n\tconst config = getHapticsConfig();\n\tconst patterns: Record<string, HapticPattern> = config?.patterns ?? PRESETS;\n\tconst respectReducedMotion = config?.respectReducedMotion ?? true;\n\n\tlet prefersReducedMotion = false;\n\tlet mqlCleanup: (() => void) | null = null;\n\n\tif (typeof window !== \"undefined\" && window.matchMedia) {\n\t\tconst mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\tprefersReducedMotion = mql.matches;\n\t\tconst onChange = (e: MediaQueryListEvent) => {\n\t\t\tprefersReducedMotion = e.matches;\n\t\t};\n\t\tmql.addEventListener(\"change\", onChange);\n\t\tmqlCleanup = () => mql.removeEventListener(\"change\", onChange);\n\t}\n\n\tnode.setAttribute(\"data-haptic\", currentAction);\n\n\tconst handler = () => {\n\t\tif (respectReducedMotion && prefersReducedMotion) return;\n\t\tconst pattern = patterns[currentAction];\n\t\tif (!pattern) return;\n\n\t\tif (isVibrationSupported()) {\n\t\t\tnavigator.vibrate(toVibrateSequence(pattern));\n\t\t} else if (isIOS() && !config) {\n\t\t\tschedulePattern(pattern);\n\t\t}\n\t};\n\n\tnode.addEventListener(\"click\", handler);\n\n\treturn {\n\t\tupdate(newAction: HapticAction) {\n\t\t\tcurrentAction = newAction;\n\t\t\tnode.setAttribute(\"data-haptic\", newAction);\n\t\t},\n\t\tdestroy() {\n\t\t\tnode.removeEventListener(\"click\", handler);\n\t\t\tnode.removeAttribute(\"data-haptic\");\n\t\t\tmqlCleanup?.();\n\t\t},\n\t};\n}\n","import {\n\tPRESETS,\n\tisVibrationSupported,\n\tisIOS,\n\ttoVibrateSequence,\n\tschedulePattern,\n} from \"@haptics/core\";\nimport type { PresetName } from \"@haptics/core\";\nimport { getHapticsConfig } from \"./context\";\n\n/**\n * Create an imperative haptics controller.\n *\n * Can be called inside a Svelte component's <script> block (will use\n * setupHaptics context if available) or outside components (falls back\n * to built-in presets).\n *\n * @example\n * ```svelte\n * <script>\n * import { createHaptics } from '@haptics/svelte';\n * const haptics = createHaptics();\n * function handleSave() { haptics.trigger('success'); }\n * </script>\n * ```\n */\nexport function createHaptics() {\n\tconst config = getHapticsConfig();\n\tconst patterns = config?.patterns ?? PRESETS;\n\tconst respectReducedMotion = config?.respectReducedMotion ?? true;\n\n\tlet prefersReducedMotion = false;\n\tif (typeof window !== \"undefined\" && window.matchMedia) {\n\t\tconst mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\t\tprefersReducedMotion = mql.matches;\n\t\tmql.addEventListener(\"change\", (e) => {\n\t\t\tprefersReducedMotion = e.matches;\n\t\t});\n\t}\n\n\tconst trigger = (action: PresetName | (string & {})) => {\n\t\tif (respectReducedMotion && prefersReducedMotion) return;\n\n\t\tconst pattern = patterns[action as keyof typeof patterns];\n\t\tif (!pattern) return;\n\n\t\tif (isVibrationSupported()) {\n\t\t\tnavigator.vibrate(toVibrateSequence(pattern));\n\t\t} else if (isIOS()) {\n\t\t\tschedulePattern(pattern);\n\t\t}\n\t};\n\n\tconst cancel = () => {\n\t\tif (isVibrationSupported()) navigator.vibrate(0);\n\t};\n\n\treturn {\n\t\ttrigger,\n\t\tcancel,\n\t\tisSupported: isVibrationSupported(),\n\t\tisIOSSupported: isIOS(),\n\t};\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haptics/svelte",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Svelte actions and imperative API for haptic feedback. iOS Safari and Android Vibration API.",
|
|
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/svelte"
|
|
38
|
+
},
|
|
39
|
+
"author": "Howdoiusekeyboard",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"haptics",
|
|
43
|
+
"svelte",
|
|
44
|
+
"haptic-feedback",
|
|
45
|
+
"vibration",
|
|
46
|
+
"ios",
|
|
47
|
+
"android",
|
|
48
|
+
"action"
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@haptics/core": "^1.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"svelte": ">=4"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"jsdom": "^29.0.0",
|
|
58
|
+
"tsup": "^8.0.0",
|
|
59
|
+
"vitest": "^4.1.0"
|
|
60
|
+
}
|
|
61
|
+
}
|