From fb1e4ad09f2241d4fcb56676744cd9d20d97e76c Mon Sep 17 00:00:00 2001
From: David Di Biase <1168397+davedbase@users.noreply.github.com>
Date: Fri, 8 May 2026 18:37:40 -0400
Subject: [PATCH] Initial commit
---
.changeset/vibrate-initial.md | 16 ++
packages/vibrate/LICENSE | 21 ++
packages/vibrate/README.md | 167 +++++++++++
packages/vibrate/dev/index.tsx | 34 +++
packages/vibrate/package.json | 71 +++++
packages/vibrate/src/index.ts | 302 ++++++++++++++++++++
packages/vibrate/test/index.test.ts | 398 +++++++++++++++++++++++++++
packages/vibrate/test/server.test.ts | 30 ++
packages/vibrate/tsconfig.json | 16 ++
pnpm-lock.yaml | 16 +-
10 files changed, 1068 insertions(+), 3 deletions(-)
create mode 100644 .changeset/vibrate-initial.md
create mode 100644 packages/vibrate/LICENSE
create mode 100644 packages/vibrate/README.md
create mode 100644 packages/vibrate/dev/index.tsx
create mode 100644 packages/vibrate/package.json
create mode 100644 packages/vibrate/src/index.ts
create mode 100644 packages/vibrate/test/index.test.ts
create mode 100644 packages/vibrate/test/server.test.ts
create mode 100644 packages/vibrate/tsconfig.json
diff --git a/.changeset/vibrate-initial.md b/.changeset/vibrate-initial.md
new file mode 100644
index 000000000..dd1f23b7e
--- /dev/null
+++ b/.changeset/vibrate-initial.md
@@ -0,0 +1,16 @@
+---
+"@solid-primitives/vibrate": minor
+---
+
+Add `@solid-primitives/vibrate` package (Stage 0)
+
+New primitives for device haptic feedback via the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API).
+
+- **`isVibrationSupported()`** — runtime check for Vibration API availability.
+- **`makeVibrate(pattern, options?)`** — non-reactive helper returning `[start, stop]`. Supports optional `interval` for repeating patterns. No-ops when the API is unavailable.
+- **`createVibrate(pattern, options?)`** — reactive primitive returning `{ vibrating, start, stop, supported }`. Accepts a reactive accessor for `pattern`: changing it while vibrating restarts automatically. Stops and cleans up on owner disposal.
+- **`frequencyToPattern(hz, dutyCycle?)`** — converts a frequency in Hz to a single-cycle `[onMs, offMs]` pattern.
+- **`makePulse(hz, options?)`** — non-reactive helper that vibrates continuously at `hz` cycles per second using a repeating chunk strategy.
+- **`createPulse(hz, options?)`** — reactive pulse primitive returning `{ pulsing, start, stop, supported }`. Accepts a reactive `hz` accessor: changing frequency while pulsing restarts immediately at the new rhythm.
+
+Peer dependencies: `solid-js@^2.0.0-beta.10` and `@solidjs/web@^2.0.0-beta.10`.
diff --git a/packages/vibrate/LICENSE b/packages/vibrate/LICENSE
new file mode 100644
index 000000000..38b41d975
--- /dev/null
+++ b/packages/vibrate/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Solid Primitives Working Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/vibrate/README.md b/packages/vibrate/README.md
new file mode 100644
index 000000000..0d1565259
--- /dev/null
+++ b/packages/vibrate/README.md
@@ -0,0 +1,167 @@
+
+
+
+
+# @solid-primitives/vibrate
+
+[](https://bundlephobia.com/package/@solid-primitives/vibrate)
+[](https://www.npmjs.com/package/@solid-primitives/vibrate)
+[](https://github.com/solidjs-community/solid-primitives#contribution-process)
+
+Primitives for triggering and managing device haptic feedback via the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API).
+
+- [`isVibrationSupported`](#isvibrationSupported) — Check if the Vibration API is available.
+- [`makeVibrate`](#makevibrate) — Non-reactive helper; returns `[start, stop]` with no Solid lifecycle dependency.
+- [`createVibrate`](#createvibrate) — Reactive primitive; returns `{ vibrating, start, stop, supported }` with automatic cleanup and reactive pattern support.
+- [`frequencyToPattern`](#frequencytopattern) — Convert a frequency in Hz to a `[onMs, offMs]` pattern.
+- [`makePulse`](#makepulse) — Non-reactive pulse helper; vibrates continuously at a given frequency.
+- [`createPulse`](#createpulse) — Reactive pulse primitive; supports reactive `hz` that restarts on change.
+
+## Installation
+
+```bash
+npm install @solid-primitives/vibrate
+# or
+yarn add @solid-primitives/vibrate
+# or
+pnpm add @solid-primitives/vibrate
+```
+
+## `isVibrationSupported`
+
+Returns `true` when the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API) is available. Useful for conditionally rendering haptic UI or showing fallback content.
+
+```ts
+import { isVibrationSupported } from "@solid-primitives/vibrate";
+
+if (isVibrationSupported()) {
+ console.log("haptics available");
+}
+```
+
+## `makeVibrate`
+
+A non-reactive building block. Wraps `navigator.vibrate` with an optional repeating interval and returns `[start, stop]`. No Solid lifecycle dependency — both functions are no-ops when the API is unavailable.
+
+```ts
+import { makeVibrate } from "@solid-primitives/vibrate";
+
+// Single-shot vibration
+const [start, stop] = makeVibrate([200, 100, 200]);
+button.addEventListener("click", start);
+
+// Repeating vibration every 2 seconds
+const [start, stop] = makeVibrate(100, { interval: 2000 });
+start();
+// later:
+stop();
+```
+
+## `createVibrate`
+
+A reactive primitive tied to the current reactive owner. Returns `{ vibrating, start, stop, supported }`. Cleans up automatically on owner disposal.
+
+When `pattern` is a reactive accessor and changes **while vibrating**, vibration restarts with the new pattern automatically.
+
+```ts
+import { createVibrate } from "@solid-primitives/vibrate";
+import { createEffect } from "solid-js";
+
+const { vibrating, start, stop, supported } = createVibrate([200, 100, 200]);
+
+createEffect(() => {
+ console.log("vibrating:", vibrating());
+});
+```
+
+### Reactive pattern
+
+```ts
+import { createVibrate } from "@solid-primitives/vibrate";
+import { createSignal } from "solid-js";
+
+const [pattern, setPattern] = createSignal(200);
+const { vibrating, start, stop } = createVibrate(pattern);
+
+start();
+
+// Changing pattern while vibrating restarts automatically
+setPattern([100, 30, 100, 30, 100]);
+```
+
+### With interval
+
+```ts
+const { vibrating, start, stop } = createVibrate(200, { interval: 1000 });
+start(); // repeats every second
+stop(); // cancels interval + active vibration
+```
+
+## `frequencyToPattern`
+
+Converts a frequency in Hz and an optional duty cycle into a single-cycle `[onMs, offMs]` vibration pattern. Useful for previewing what `makePulse` / `createPulse` will produce.
+
+```ts
+import { frequencyToPattern } from "@solid-primitives/vibrate";
+
+frequencyToPattern(2) // [250, 250] — 2 Hz, equal on/off
+frequencyToPattern(4, 0.25) // ~[63, 188] — 4 Hz, short tap
+```
+
+## `makePulse`
+
+Non-reactive pulse helper. Vibrates continuously at `hz` cycles per second. No Solid lifecycle dependency; both functions are no-ops when the API is unavailable.
+
+```ts
+import { makePulse } from "@solid-primitives/vibrate";
+
+const [start, stop] = makePulse(4); // 4 taps per second
+button.addEventListener("pointerdown", start);
+button.addEventListener("pointerup", stop);
+```
+
+## `createPulse`
+
+Reactive pulse primitive tied to the current reactive owner. Returns `{ pulsing, start, stop, supported }`. Accepts a reactive `hz` accessor — changing the frequency while pulsing restarts the vibration immediately at the new rhythm.
+
+```ts
+import { createPulse } from "@solid-primitives/vibrate";
+import { createSignal, createMemo } from "solid-js";
+
+// Fixed frequency
+const { pulsing, start, stop } = createPulse(2);
+
+// Reactive frequency — escalates as a countdown nears zero
+const [seconds, setSeconds] = createSignal(10);
+const hz = createMemo(() => 1 + (10 - seconds()) * 0.5);
+const { start, stop } = createPulse(hz);
+```
+
+## Types
+
+```ts
+export type VibratePattern = number | number[];
+
+export interface VibrateOptions {
+ /** Milliseconds between pattern repetitions. Omit to vibrate once per call. */
+ interval?: number;
+}
+
+export interface PulseOptions {
+ /**
+ * Fraction of each cycle spent vibrating (0–1). Defaults to `0.5`.
+ * A higher value produces longer buzzes; a lower value produces shorter taps.
+ */
+ dutyCycle?: number;
+}
+```
+
+## Browser Support
+
+The Vibration API is supported on Chrome/Firefox for Android. It is **not** supported on iOS or most desktop browsers. Always check `isVibrationSupported()` or rely on `supported` from `createVibrate` before enabling haptic features in your UI.
+
+> Note: vibration requires a prior user interaction (sticky activation) and may be suppressed by silent/DND mode.
+
+## Changelog
+
+See [CHANGELOG.md](./CHANGELOG.md)
diff --git a/packages/vibrate/dev/index.tsx b/packages/vibrate/dev/index.tsx
new file mode 100644
index 000000000..ad50905cd
--- /dev/null
+++ b/packages/vibrate/dev/index.tsx
@@ -0,0 +1,34 @@
+import { type Component, createSignal } from "solid-js";
+import { createVibrate, isVibrationSupported } from "../src/index.js";
+
+const App: Component = () => {
+ const [pattern, setPattern] = createSignal([200, 100, 200]);
+ const { vibrating, start, stop, supported } = createVibrate(pattern);
+
+ return (
+
+
+
Vibration
+
+ {supported ? "Vibration API supported" : "Vibration API not supported in this browser"}
+