@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 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"]}
@@ -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 };
@@ -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
+ }