@base-ripple/core 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/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @base-ripple/core
2
+
3
+ Framework-agnostic ripple effect for DOM elements. Use this core package for
4
+ vanilla JS or to build your own wrapper. If you are using a framework, prefer
5
+ the framework-specific package (for example `@base-ripple/react`).
6
+
7
+ Demo: <https://base-ripple.vercel.app/>
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @base-ripple/core
13
+ ```
14
+
15
+ ## Quick start (vanilla)
16
+
17
+ ```html
18
+ <button class="base-ripple-container">Click me</button>
19
+ ```
20
+
21
+ ```ts
22
+ import "@base-ripple/core/styles.css";
23
+ import { attachBaseRipple } from "@base-ripple/core";
24
+
25
+ const button = document.querySelector<HTMLButtonElement>(
26
+ ".base-ripple-container",
27
+ );
28
+ if (!button) throw new Error("Missing button");
29
+
30
+ const dispose = attachBaseRipple(button, {
31
+ origin: "pointer",
32
+ sizeOffset: 96,
33
+ attributes: { "data-ripple": "true" },
34
+ });
35
+
36
+ // later
37
+ dispose();
38
+ ```
39
+
40
+ The ripple attaches listeners to the target element and global pointer events.
41
+ Call the returned `dispose` function on teardown.
42
+
43
+ ## Styles (required)
44
+
45
+ The ripple uses two class names:
46
+
47
+ - `.base-ripple-container` on the target element
48
+ - `.base-ripple` on the injected `<span>`
49
+
50
+ Option A: Import the default styles (recommended)
51
+
52
+ ```css
53
+ @import "@base-ripple/core/styles.css";
54
+ ```
55
+
56
+ The default stylesheet handles layout and animation. You still need to set a
57
+ fill color and opacity, for example:
58
+
59
+ ```css
60
+ .base-ripple {
61
+ background: currentColor;
62
+ opacity: 0.12;
63
+ }
64
+ ```
65
+
66
+ Option B: Provide your own styles (advanced)
67
+
68
+ If you do not import `styles.css`, create your own CSS using the same class
69
+ names. Make sure the container is positioned and clips overflow. Here is the
70
+ default `styles.css` you can copy and customize:
71
+
72
+ ```css
73
+ .base-ripple-container {
74
+ position: relative;
75
+ overflow: hidden;
76
+ touch-action: manipulation;
77
+ -webkit-tap-highlight-color: transparent;
78
+ }
79
+
80
+ .base-ripple {
81
+ position: absolute;
82
+ top: 0;
83
+ left: 0;
84
+ border-radius: 50%;
85
+ pointer-events: none;
86
+ animation: baseRipple 600ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
87
+ transition: opacity 600ms cubic-bezier(0.4, 0, 0.2, 1);
88
+ will-change: transform, opacity;
89
+ }
90
+
91
+ @keyframes baseRipple {
92
+ from {
93
+ transform: translate3d(
94
+ calc(var(--base-ripple-keyframes-from-x) - 50%),
95
+ calc(var(--base-ripple-keyframes-from-y) - 50%),
96
+ 0
97
+ )
98
+ scale(0);
99
+ }
100
+ to {
101
+ transform: translate3d(
102
+ calc(var(--base-ripple-keyframes-to-x) - 50%),
103
+ calc(var(--base-ripple-keyframes-to-y) - 50%),
104
+ 0
105
+ )
106
+ scale(1);
107
+ }
108
+ }
109
+ ```
110
+
111
+ IMPORTANT: If you write your own `.base-ripple` styles, it MUST include a
112
+ `transition` for `opacity`. Cleanup relies on the `transitionend` event for
113
+ `opacity`. Without it, ripple elements never remove themselves. If you want no
114
+ transition, set a `transition` with 0 duration, for example:
115
+
116
+ ```css
117
+ .base-ripple {
118
+ transition: opacity 0ms linear;
119
+ }
120
+ ```
121
+
122
+ ## API
123
+
124
+ ```ts
125
+ import type { AttachBaseRippleOptions } from "@base-ripple/core";
126
+
127
+ attachBaseRipple(
128
+ container: HTMLElement,
129
+ options?: AttachBaseRippleOptions
130
+ ): () => void;
131
+ ```
132
+
133
+ ### Options
134
+
135
+ | Prop | Type | Default | Description |
136
+ | ------------ | ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------ |
137
+ | `origin` | `"pointer" \| "center"` | `pointer` | Where the ripple starts. `pointer` uses the pointer position; `center` starts from the element center. |
138
+ | `sizeOffset` | `number` | `0` | Extra pixels added to the ripple diameter, useful when adding blur or glow. |
139
+ | `attributes` | `Record<string, string>` | `undefined` | Extra attributes added to each ripple `<span>`. |
140
+
141
+ ## Behavior notes
142
+
143
+ - Uses pointer and keyboard interactions (Space, Enter, NumpadEnter).
144
+ - Respects `prefers-reduced-motion` by disabling ripples and clearing existing.
145
+ - Skips ripples for disabled elements (`disabled` or `aria-disabled="true"`).
146
+ - Adds `aria-hidden="true"` to the ripple spans for accessibility.
147
+ - Browser-only API; call it after the DOM is available.
148
+
149
+ ## Framework packages
150
+
151
+ Use the framework-specific package when available:
152
+
153
+ - React: `@base-ripple/react`
154
+
155
+ This core package is ideal for vanilla usage or custom integrations.
156
+
157
+ ## Planned framework wrappers (not implemented yet)
158
+
159
+ - Preact
160
+ - Vue
161
+ - Svelte
162
+ - Solid
163
+ - Angular
164
+ - Lit
165
+ - Qwik
package/dist/index.js ADDED
@@ -0,0 +1,179 @@
1
+ const f = /* @__PURE__ */ new Set();
2
+ let m = !1;
3
+ function S() {
4
+ m || (m = !0, addEventListener("blur", c), addEventListener("pagehide", c), addEventListener("beforeunload", c), addEventListener("pointerup", h, { passive: !0 }), addEventListener("pointercancel", h, { passive: !0 }), document.addEventListener("visibilitychange", B));
5
+ }
6
+ function N() {
7
+ !m || f.size > 0 || (m = !1, removeEventListener("blur", c), removeEventListener("pagehide", c), removeEventListener("beforeunload", c), removeEventListener("pointerup", h), removeEventListener("pointercancel", h), document.removeEventListener("visibilitychange", B));
8
+ }
9
+ function c() {
10
+ for (const e of f)
11
+ e.fadeAll();
12
+ }
13
+ function h(e) {
14
+ for (const i of f)
15
+ i.fadeFromPointer(e);
16
+ }
17
+ function B() {
18
+ document.visibilityState === "hidden" && c();
19
+ }
20
+ function _(e, {
21
+ origin: i = "pointer",
22
+ sizeOffset: s = 0,
23
+ attributes: n
24
+ } = {}) {
25
+ const a = /* @__PURE__ */ new Set(), o = /* @__PURE__ */ new Map(), d = /* @__PURE__ */ new Map(), b = i === "pointer";
26
+ let v = !1, u = O();
27
+ const g = (t, r, l) => ({
28
+ from: {
29
+ size: 0,
30
+ x: r,
31
+ y: l
32
+ },
33
+ to: {
34
+ size: (b ? Q(
35
+ t.width,
36
+ t.height,
37
+ r,
38
+ l
39
+ ) : K(
40
+ t.width,
41
+ t.height
42
+ )) + s,
43
+ x: b ? r : t.width / 2,
44
+ y: b ? l : t.height / 2
45
+ }
46
+ }), x = (t) => {
47
+ if (t.defaultPrevented || I(e) || u) return;
48
+ const r = e.getBoundingClientRect(), l = g(
49
+ r,
50
+ t.clientX - r.x,
51
+ t.clientY - r.y
52
+ ), p = D({
53
+ rippleKeyframes: l,
54
+ removeHandler: R,
55
+ attributes: n
56
+ });
57
+ a.add(p), o.set(t.pointerId, p), e.appendChild(p);
58
+ }, w = (t) => {
59
+ if (t.defaultPrevented || I(e) || u || t.repeat || d.has(t.code) || !F(t.code) || q(t.target)) return;
60
+ const r = e.getBoundingClientRect(), l = g(
61
+ r,
62
+ r.width / 2,
63
+ r.height / 2
64
+ ), p = D({
65
+ rippleKeyframes: l,
66
+ removeHandler: R,
67
+ attributes: n
68
+ });
69
+ a.add(p), d.set(t.code, p), e.appendChild(p);
70
+ }, y = (t) => {
71
+ t.style.opacity = "0";
72
+ }, E = (t) => {
73
+ const r = o.get(t.pointerId);
74
+ r && (y(r), o.delete(t.pointerId));
75
+ }, z = (t) => {
76
+ const r = d.get(t.code);
77
+ r && (y(r), d.delete(t.code));
78
+ }, L = () => {
79
+ for (const t of o.values()) y(t);
80
+ o.clear();
81
+ for (const t of d.values()) y(t);
82
+ d.clear();
83
+ }, R = (t) => {
84
+ a.delete(t);
85
+ for (const [r, l] of o)
86
+ if (l === t) {
87
+ o.delete(r);
88
+ break;
89
+ }
90
+ for (const [r, l] of d)
91
+ if (l === t) {
92
+ d.delete(r);
93
+ break;
94
+ }
95
+ t.remove();
96
+ }, P = () => {
97
+ for (const t of a)
98
+ t.remove();
99
+ a.clear(), o.clear(), d.clear();
100
+ }, M = () => {
101
+ v || (e.addEventListener("pointerdown", x, {
102
+ passive: !0
103
+ }), v = !0);
104
+ }, k = () => {
105
+ v && (e.removeEventListener("pointerdown", x), v = !1);
106
+ }, G = T((t) => {
107
+ u !== t && (u = t, u ? (k(), P()) : M());
108
+ }), A = (t) => {
109
+ F(t.code) && z(t);
110
+ };
111
+ u || M(), e.addEventListener("keyup", A), e.addEventListener("keydown", w), e.addEventListener("contextmenu", L), e.addEventListener("pointerleave", E, {
112
+ passive: !0
113
+ }), S();
114
+ const C = {
115
+ fadeAll: L,
116
+ fadeFromPointer: E
117
+ };
118
+ return f.add(C), () => {
119
+ G(), k(), e.removeEventListener("keyup", A), e.removeEventListener("keydown", w), e.removeEventListener("contextmenu", L), e.removeEventListener("pointerleave", E), f.delete(C), N(), P();
120
+ };
121
+ }
122
+ const H = "(prefers-reduced-motion: reduce)";
123
+ function O() {
124
+ return typeof matchMedia != "function" ? !1 : matchMedia(H).matches;
125
+ }
126
+ function T(e) {
127
+ if (typeof matchMedia != "function") return () => {
128
+ };
129
+ const i = matchMedia(H), s = (n) => e(n.matches);
130
+ return typeof i.addEventListener == "function" ? (i.addEventListener("change", s), () => i.removeEventListener("change", s)) : typeof i.addListener == "function" ? (i.addListener(s), () => i.removeListener(s)) : () => {
131
+ };
132
+ }
133
+ function D({
134
+ rippleKeyframes: e,
135
+ removeHandler: i,
136
+ attributes: s
137
+ }) {
138
+ const n = document.createElement("span");
139
+ if (n.setAttribute("aria-hidden", "true"), n.className = "base-ripple", n.style.width = e.to.size + "px", n.style.height = e.to.size + "px", n.style.setProperty(
140
+ "--base-ripple-keyframes-from-x",
141
+ e.from.x + "px"
142
+ ), n.style.setProperty(
143
+ "--base-ripple-keyframes-from-y",
144
+ e.from.y + "px"
145
+ ), n.style.setProperty(
146
+ "--base-ripple-keyframes-to-x",
147
+ e.to.x + "px"
148
+ ), n.style.setProperty(
149
+ "--base-ripple-keyframes-to-y",
150
+ e.to.y + "px"
151
+ ), s)
152
+ for (const [o, d] of Object.entries(s))
153
+ n.setAttribute(o, d);
154
+ const a = (o) => {
155
+ o.propertyName === "opacity" && (i(n), n.removeEventListener("transitionend", a));
156
+ };
157
+ return n.addEventListener("transitionend", a), n;
158
+ }
159
+ function I(e) {
160
+ const i = e.getAttribute("aria-disabled");
161
+ return i !== null ? i.toLowerCase() === "true" : "disabled" in e && !!e.disabled;
162
+ }
163
+ function F(e) {
164
+ return e === "Space" || e === "Enter" || e === "NumpadEnter";
165
+ }
166
+ const U = 'input:not([type]),input[type="text"],input[type="search"],input[type="url"],input[type="tel"],input[type="email"],input[type="password"],input[type="number"],input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"],input[type="week"],textarea,[contenteditable]';
167
+ function q(e) {
168
+ return e instanceof Element ? !!e.closest(U) : !1;
169
+ }
170
+ function K(e, i) {
171
+ return Math.sqrt(e * e + i * i);
172
+ }
173
+ function Q(e, i, s, n) {
174
+ const a = Math.max(s, e - s), o = Math.max(n, i - n);
175
+ return 2 * Math.sqrt(a * a + o * o);
176
+ }
177
+ export {
178
+ _ as attachBaseRipple
179
+ };
@@ -0,0 +1 @@
1
+ .base-ripple-container{touch-action:manipulation;-webkit-tap-highlight-color:transparent;position:relative;overflow:hidden}.base-ripple{pointer-events:none;will-change:transform,opacity;border-radius:50%;transition:opacity .6s cubic-bezier(.4,0,.2,1);animation:.6s cubic-bezier(.4,0,.2,1) forwards baseRipple;position:absolute;top:0;left:0}@keyframes baseRipple{0%{transform:translate3d(calc(var(--base-ripple-keyframes-from-x) - 50%),calc(var(--base-ripple-keyframes-from-y) - 50%),0)scale(0)}to{transform:translate3d(calc(var(--base-ripple-keyframes-to-x) - 50%),calc(var(--base-ripple-keyframes-to-y) - 50%),0)scale(1)}}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@base-ripple/core",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ ".": {
11
+ "@base-ripple/source": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./styles.css": {
17
+ "@base-ripple/source": "./src/styles.css",
18
+ "default": "./dist/styles.css"
19
+ }
20
+ },
21
+ "sideEffects": [
22
+ "./dist/styles.css"
23
+ ],
24
+ "files": [
25
+ "dist",
26
+ "!**/*.tsbuildinfo"
27
+ ],
28
+ "dependencies": {
29
+ "tslib": "^2.3.0"
30
+ },
31
+ "nx": {
32
+ "tags": [
33
+ "scope:core"
34
+ ]
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }