@joknoll/svelte-attach-haptic 0.1.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/README.md +129 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# svelte-attach-haptic
|
|
2
|
+
|
|
3
|
+
A Svelte attachment for adding haptic feedback to elements using the [Svelte 5 attachments API](https://svelte.dev/docs/svelte/attachments).
|
|
4
|
+
|
|
5
|
+
Uses the Web Vibration API. Silently no-ops on unsupported platforms (desktop).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install @joknoll/svelte-attach-haptic
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
```svelte
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import { haptic, useHaptic } from "@joknoll/svelte-attach-haptic";
|
|
18
|
+
|
|
19
|
+
const tap = useHaptic("medium", ["pointerdown"]);
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<!-- Default: medium intensity on click -->
|
|
23
|
+
<button {@attach haptic()}>Default</button>
|
|
24
|
+
|
|
25
|
+
<!-- Built-in presets -->
|
|
26
|
+
<button {@attach haptic({ pattern: "success" })}>Success</button>
|
|
27
|
+
<button {@attach haptic({ pattern: "error" })}>Error</button>
|
|
28
|
+
|
|
29
|
+
<!-- Custom event trigger -->
|
|
30
|
+
<button {@attach haptic({ pattern: "heavy", events: ["pointerdown"] })}>Heavy on pointerdown</button>
|
|
31
|
+
|
|
32
|
+
<!-- Factory: create a reusable haptic with shared defaults -->
|
|
33
|
+
<button {@attach tap()}>Factory default</button>
|
|
34
|
+
<button {@attach tap({ pattern: "light" })}>Factory with override</button>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
[Demo](https://joknoll.github.io/svelte-attach-haptic/) | [npm](https://www.npmjs.com/package/svelte-attach-haptic)
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `haptic(options?)`
|
|
42
|
+
|
|
43
|
+
Svelte attachment that triggers haptic feedback on an element.
|
|
44
|
+
|
|
45
|
+
| Option | Type | Default | Description |
|
|
46
|
+
| ----------- | ------------------------------ | ----------- | ------------------------------------ |
|
|
47
|
+
| `pattern` | `HapticInput` | `"medium"` | Vibration pattern or preset name |
|
|
48
|
+
| `events` | `[triggerEvent, cancelEvent?]` | `["click"]` | DOM events to trigger/cancel haptics |
|
|
49
|
+
| `intensity` | `number` | `0.5` | Global intensity override (0–1) |
|
|
50
|
+
|
|
51
|
+
### `useHaptic(pattern?, events?, intensity?)`
|
|
52
|
+
|
|
53
|
+
Factory function that returns a reusable attachment with preset defaults. The returned function accepts optional overrides per element.
|
|
54
|
+
|
|
55
|
+
```svelte
|
|
56
|
+
<script lang="ts">
|
|
57
|
+
import { useHaptic } from "svelte-attach-haptic";
|
|
58
|
+
const tap = useHaptic("medium", ["pointerdown"]);
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<button {@attach tap()}>Uses defaults</button>
|
|
62
|
+
<button {@attach tap({ pattern: "light" })}>Override pattern</button>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `Haptic` class
|
|
66
|
+
|
|
67
|
+
For manual control outside of attachments:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { Haptic } from "svelte-attach-haptic";
|
|
71
|
+
|
|
72
|
+
const h = new Haptic("success");
|
|
73
|
+
h.trigger();
|
|
74
|
+
h.cancel();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `isSupported`
|
|
78
|
+
|
|
79
|
+
Boolean indicating whether the Web Vibration API is available.
|
|
80
|
+
|
|
81
|
+
### Built-in presets
|
|
82
|
+
|
|
83
|
+
| Preset | Category | Description |
|
|
84
|
+
| ------------- | ------------ | -------------------------------------------------------- |
|
|
85
|
+
| `"light"` | Impact | Short, subtle tap (small toggle, minor interaction) |
|
|
86
|
+
| `"medium"` | Impact | Standard tap (button press, card snap-to-position) |
|
|
87
|
+
| `"heavy"` | Impact | Strong impact (major state change, force press) |
|
|
88
|
+
| `"soft"` | Impact | Soft impact (gentle interaction) |
|
|
89
|
+
| `"rigid"` | Impact | Crisp impact (sharp, precise feedback) |
|
|
90
|
+
| `"selection"` | Selection | Selection feedback (picker scroll, slider detent) |
|
|
91
|
+
| `"success"` | Notification | Success notification (form saved, payment confirmed) |
|
|
92
|
+
| `"warning"` | Notification | Warning notification (destructive action, limit reached) |
|
|
93
|
+
| `"error"` | Notification | Error notification (validation failure, network error) |
|
|
94
|
+
| `"nudge"` | Other | Double bump (attention grab) |
|
|
95
|
+
| `"buzz"` | Other | Long buzz |
|
|
96
|
+
|
|
97
|
+
### Custom patterns
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// Single duration (ms)
|
|
101
|
+
haptic({ pattern: 200 });
|
|
102
|
+
|
|
103
|
+
// Array of durations (vibrate, pause, vibrate, ...)
|
|
104
|
+
haptic({ pattern: [100, 50, 100] });
|
|
105
|
+
|
|
106
|
+
// Vibration objects with per-step intensity
|
|
107
|
+
haptic({
|
|
108
|
+
pattern: [
|
|
109
|
+
{ duration: 50, intensity: 0.8 },
|
|
110
|
+
{ delay: 100, duration: 30, intensity: 0.4 },
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Design Guidelines
|
|
116
|
+
|
|
117
|
+
1. **Haptics supplement, never replace.** Always pair with visual feedback.
|
|
118
|
+
2. **Match intensity to significance.** Light interactions → `"light"`/`"selection"`. Standard → `"medium"`/`"success"`. Major → `"heavy"`/`"error"`/`"warning"`.
|
|
119
|
+
3. **Do not overuse.** Reserve for meaningful moments.
|
|
120
|
+
4. **Synchronize perfectly.** Fire haptic at the exact instant the visual change occurs.
|
|
121
|
+
5. **For async ops**, trigger when the result arrives:
|
|
122
|
+
```ts
|
|
123
|
+
try {
|
|
124
|
+
await submit();
|
|
125
|
+
new Haptic("success").trigger();
|
|
126
|
+
} catch {
|
|
127
|
+
new Haptic("error").trigger();
|
|
128
|
+
}
|
|
129
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Attachment } from "svelte/attachments";
|
|
2
|
+
|
|
3
|
+
//#region src/haptic.d.ts
|
|
4
|
+
interface Vibration {
|
|
5
|
+
duration: number;
|
|
6
|
+
intensity?: number;
|
|
7
|
+
delay?: number;
|
|
8
|
+
}
|
|
9
|
+
type HapticPattern = number[] | Vibration[];
|
|
10
|
+
type HapticInput = number | string | HapticPattern;
|
|
11
|
+
type HapticEvents = [keyof HTMLElementEventMap, (keyof HTMLElementEventMap)?];
|
|
12
|
+
interface HapticOptions {
|
|
13
|
+
pattern?: HapticInput;
|
|
14
|
+
events?: HapticEvents;
|
|
15
|
+
intensity?: number;
|
|
16
|
+
}
|
|
17
|
+
type HapticPresetName = "success" | "warning" | "error" | "light" | "medium" | "heavy" | "soft" | "rigid" | "selection" | "nudge" | "buzz";
|
|
18
|
+
declare const defaultPatterns: Record<HapticPresetName, Vibration[]>;
|
|
19
|
+
/** Whether the Web Vibration API is available in the current environment. */
|
|
20
|
+
declare const isSupported: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a haptic input into a flat vibrate pattern (number[]).
|
|
23
|
+
* Returns an empty array if the input is invalid or unknown.
|
|
24
|
+
*/
|
|
25
|
+
declare function createPattern(input?: HapticInput, intensity?: number): number[];
|
|
26
|
+
/** Fire a pre-built vibrate pattern. No-op if the Vibration API is unavailable. */
|
|
27
|
+
declare function triggerHaptic(pattern: number[]): void;
|
|
28
|
+
/** Cancel any active vibration. No-op if the Vibration API is unavailable. */
|
|
29
|
+
declare function cancelHaptic(): void;
|
|
30
|
+
/** Convenience class for manual haptic control. */
|
|
31
|
+
declare class Haptic {
|
|
32
|
+
static readonly isSupported: boolean;
|
|
33
|
+
private readonly pattern;
|
|
34
|
+
constructor(input?: HapticInput, intensity?: number);
|
|
35
|
+
trigger(): void;
|
|
36
|
+
cancel(): void;
|
|
37
|
+
}
|
|
38
|
+
/** Svelte attachment that triggers haptic feedback on DOM events. */
|
|
39
|
+
declare function haptic(options?: HapticOptions): Attachment<HTMLElement>;
|
|
40
|
+
/** Create a reusable haptic attachment factory with preset defaults. */
|
|
41
|
+
declare function useHaptic(pattern?: HapticInput, events?: HapticEvents, intensity?: number): (overrides?: Partial<HapticOptions>) => Attachment<HTMLElement>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { Haptic, type HapticEvents, type HapticInput, type HapticOptions, type HapticPattern, type Vibration, cancelHaptic, createPattern, defaultPatterns, haptic, isSupported, triggerHaptic, useHaptic };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{on as e}from"svelte/events";const t={success:[{duration:30,intensity:.5},{delay:60,duration:40,intensity:1}],warning:[{duration:40,intensity:.8},{delay:100,duration:40,intensity:.6}],error:[{duration:40,intensity:.9},{delay:40,duration:40,intensity:.9},{delay:40,duration:40,intensity:.9}],light:[{duration:15,intensity:.4}],medium:[{duration:25,intensity:.7}],heavy:[{duration:35,intensity:1}],soft:[{duration:40,intensity:.5}],rigid:[{duration:10,intensity:1}],selection:[{duration:8,intensity:.3}],nudge:[{duration:80,intensity:.8},{delay:80,duration:50,intensity:.3}],buzz:[{duration:1e3,intensity:1}]},n=1e3;function r(e){return Math.max(0,Math.min(1,e))}function i(e){if(typeof e==`number`)return[{duration:e}];if(typeof e==`string`){let n=t[e];return n?n.map(e=>({...e})):(console.warn(`[svelte-attach-haptic] Unknown preset: "${e}"`),null)}if(e.length===0)return[];if(typeof e[0]==`number`){let t=e,n=[];for(let e=0;e<t.length;e+=2){let r=e>0?t[e-1]:0;n.push({...r>0&&{delay:r},duration:t[e]})}return n}return e.map(e=>({...e}))}function a(e,t){if(t>=1)return[e];if(t<=0)return[];let n=Math.max(1,Math.round(20*t)),r=20-n,i=[],a=e;for(;a>=20;)i.push(n,r),a-=20;if(a>0){let e=Math.max(1,Math.round(a*t));i.push(e);let n=a-e;n>0&&i.push(n)}return i}function o(e,t){t<=0||(e.length>0&&e.length%2==0?e[e.length-1]+=t:(e.length===0&&e.push(0),e.push(t)))}function s(e,t){let n=[];for(let i of e){let e=r(i.intensity??t);o(n,i.delay??0);let s=a(i.duration,e);if(s.length===0){o(n,i.duration);continue}for(let e of s)n.push(e)}return n}const c=typeof navigator<`u`&&typeof navigator.vibrate==`function`;function l(e=`medium`,t=.5){let a=i(e);if(!a||a.length===0)return[];for(let e of a)e.duration>n&&(e.duration=n);return s(a,r(t))}function u(e){c&&e.length>0&&navigator.vibrate(e)}function d(){c&&navigator.vibrate(0)}var f=class{static isSupported=c;pattern;constructor(e=`medium`,t=.5){this.pattern=l(e,t)}trigger(){u(this.pattern)}cancel(){d()}};function p(t={}){return n=>{let{pattern:r=`medium`,events:i=[`click`],intensity:a=.5}=t,[o,s]=i,c=l(r,a),f=e(n,o,()=>u(c)),p=s?e(n,s,()=>d()):null;return()=>{f(),p?.()}}}function m(e,t,n){return r=>p({pattern:e,events:t,intensity:n,...r})}export{f as Haptic,d as cancelHaptic,l as createPattern,t as defaultPatterns,p as haptic,c as isSupported,u as triggerHaptic,m as useHaptic};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joknoll/svelte-attach-haptic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Svelte attachment for adding haptics to elements.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"attachment",
|
|
7
|
+
"haptics",
|
|
8
|
+
"svelte",
|
|
9
|
+
"svelte5"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/joknoll/svelte-attach/tree/main/packages/haptic#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/joknoll/svelte-attach/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "joknoll",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/joknoll/svelte-attach.git",
|
|
20
|
+
"directory": "packages/haptic"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./dist/index.js",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.6.2",
|
|
36
|
+
"@typescript/native-preview": "7.0.0-dev.20260509.2",
|
|
37
|
+
"bumpp": "^11.1.0",
|
|
38
|
+
"jsdom": "^28.1.0",
|
|
39
|
+
"svelte": "^5.55.0",
|
|
40
|
+
"typescript": "^6.0.3",
|
|
41
|
+
"vite-plus": "latest"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"svelte": ">=5.55.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "vp pack",
|
|
48
|
+
"dev": "vp pack --watch",
|
|
49
|
+
"test": "vp test",
|
|
50
|
+
"check": "vp check"
|
|
51
|
+
}
|
|
52
|
+
}
|