ripple-text 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.
Files changed (3) hide show
  1. package/README.md +120 -0
  2. package/dist/index.js +320 -0
  3. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # ripple-text
2
+
3
+ Physics-driven text animation engine. Characters react to mouse and touch
4
+ interactions through expanding ripple waves and continuous field effects like
5
+ water caustics.
6
+
7
+ [Demo & homepage](https://ilia.to)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install ripple-text
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import {
19
+ RippleTextEngine,
20
+ WaterField,
21
+ WaveRipple,
22
+ layoutText,
23
+ } from "ripple-text";
24
+
25
+ const canvas = document.getElementById("canvas") as HTMLCanvasElement;
26
+ const ctx = canvas.getContext("2d")!;
27
+
28
+ const field = new WaterField();
29
+ const ripple = new WaveRipple();
30
+
31
+ const engine = new RippleTextEngine(canvas, field, ripple, {
32
+ bgColor: "#020814",
33
+ });
34
+
35
+ const letters = layoutText(ctx, "Hello world", { fontSize: 24, margin: 40 });
36
+ engine.setLetters(letters);
37
+ engine.start();
38
+ ```
39
+
40
+ ### Tuning settings at runtime
41
+
42
+ ```typescript
43
+ engine.updateSettings({
44
+ gradientForce: 25000,
45
+ springForce: 5,
46
+ damping: 0.92,
47
+ maxDisplacement: 150,
48
+ });
49
+ ```
50
+
51
+ ### Custom colors
52
+
53
+ ```typescript
54
+ const engine = new RippleTextEngine(canvas, field, ripple, {
55
+ colorBuckets: 10,
56
+ buildColors(n) {
57
+ return Array.from({ length: n }, (_, i) => {
58
+ const t = i / (n - 1);
59
+ return `rgba(${100 + t * 155}, ${180 + t * 75}, 255, ${0.8 + t * 0.2})`;
60
+ });
61
+ },
62
+ });
63
+ ```
64
+
65
+ ### Extracting text from the DOM
66
+
67
+ ```typescript
68
+ import { extractTextFromDOM } from "ripple-text";
69
+
70
+ const letters = extractTextFromDOM(document.body, 5000);
71
+ engine.setLetters(letters);
72
+ ```
73
+
74
+ ## API
75
+
76
+ ### `RippleTextEngine(canvas, field, ripple, settings?)`
77
+
78
+ Main class. Handles physics simulation, rendering, and pointer interaction.
79
+
80
+ - **`setLetters(letters)`** — set the characters to animate
81
+ - **`updateSettings(patch)`** — update physics/rendering settings live
82
+ - **`resize(w, h)`** — call on window resize
83
+ - **`start()`** — begin the animation loop and attach event listeners
84
+ - **`stop()`** — stop the loop and remove listeners
85
+
86
+ ### `layoutText(ctx, text, options)`
87
+
88
+ Lays out plain text on a canvas with word wrapping. Returns an array of letter
89
+ positions.
90
+
91
+ Options: `fontSize`, `margin`, `lineHeight`, `font`.
92
+
93
+ ### `extractTextFromDOM(root, maxChars?)`
94
+
95
+ Walks the DOM tree and extracts visible characters with their screen positions
96
+ and computed fonts.
97
+
98
+ ### `WaterField` / `WaveRipple`
99
+
100
+ Built-in implementations of the `FieldEffect` and `RippleSource` interfaces.
101
+ Provide your own to create custom effects.
102
+
103
+ ## Settings
104
+
105
+ | Setting | Default | Description |
106
+ | ----------------- | ----------- | ----------------------------------------------- |
107
+ | `gradientForce` | `18000` | How strongly field gradients push letters |
108
+ | `springForce` | `3.0` | Restoration force toward original position |
109
+ | `darkSpringBoost` | `8.0` | Extra spring force in dark field regions |
110
+ | `damping` | `0.88` | Velocity damping per frame |
111
+ | `maxDisplacement` | `120` | Max pixel distance from origin |
112
+ | `fieldScale` | `3` | Field simulation downscale factor |
113
+ | `rippleInterval` | `90` | Milliseconds between ripples while pointer held |
114
+ | `colorBuckets` | `10` | Number of displacement-based color steps |
115
+ | `bgColor` | `"#020814"` | Canvas background color |
116
+ | `showFps` | `false` | Show FPS counter |
117
+
118
+ ## License
119
+
120
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,320 @@
1
+ const M = 0.016666666666666666, P = {
2
+ gradientForce: 18e3,
3
+ springForce: 3,
4
+ darkSpringBoost: 8,
5
+ damping: 0.88,
6
+ maxDisplacement: 120,
7
+ fieldScale: 3,
8
+ rippleInterval: 90,
9
+ colorBuckets: 10,
10
+ bgColor: "rgb(255,254,250)",
11
+ showFps: !1,
12
+ buildColors(p) {
13
+ const e = [];
14
+ for (let t = 0; t < p; t++) {
15
+ const i = t / (p - 1), s = Math.round(50 - i * 40);
16
+ e.push(`rgba(${s},${s},${s},${(0.75 + i * 0.25).toFixed(2)})`);
17
+ }
18
+ return e;
19
+ }
20
+ };
21
+ class R {
22
+ constructor(e, t, i, s) {
23
+ this.letters = [], this.colors = [], this.rafId = 0, this.lastFrameTime = 0, this.running = !1, this.fpsFrames = 0, this.fpsLastTime = 0, this.fpsDisplay = 0, this.pointerDown = !1, this.pointerX = 0, this.pointerY = 0, this.prevPointerX = 0, this.prevPointerY = 0, this.pointerSpeed = 0, this.lastRippleTime = 0, this.canvas = e, this.ctx = e.getContext("2d"), this.field = t, this.ripple = i, this.settings = { ...P, ...s }, this.colors = this.settings.buildColors(this.settings.colorBuckets);
24
+ const r = Math.ceil(e.width / this.settings.fieldScale), a = Math.ceil(e.height / this.settings.fieldScale);
25
+ this.field.resize(r, a), this.ripple.resize(e.width, e.height), this._onDown = (h) => {
26
+ this.pointerDown = !0, this.pointerX = h.clientX, this.pointerY = h.clientY, this.prevPointerX = this.pointerX, this.prevPointerY = this.pointerY, this.pointerSpeed = 0, this.ripple.addRipple(this.pointerX, this.pointerY, this.lastFrameTime), this.lastRippleTime = this.lastFrameTime;
27
+ }, this._onMove = (h) => {
28
+ if (!this.pointerDown) return;
29
+ this.prevPointerX = this.pointerX, this.prevPointerY = this.pointerY, this.pointerX = h.clientX, this.pointerY = h.clientY;
30
+ const o = this.pointerX - this.prevPointerX, c = this.pointerY - this.prevPointerY;
31
+ this.pointerSpeed = Math.sqrt(o * o + c * c);
32
+ }, this._onUp = () => {
33
+ this.pointerDown = !1, this.pointerSpeed = 0;
34
+ }, this._onTouchStart = (h) => {
35
+ const o = h.touches[0];
36
+ o && (this.pointerDown = !0, this.pointerX = o.clientX, this.pointerY = o.clientY, this.prevPointerX = this.pointerX, this.prevPointerY = this.pointerY, this.pointerSpeed = 0, this.ripple.addRipple(this.pointerX, this.pointerY, this.lastFrameTime), this.lastRippleTime = this.lastFrameTime);
37
+ }, this._onTouchMove = (h) => {
38
+ const o = h.touches[0];
39
+ if (!o || !this.pointerDown) return;
40
+ this.prevPointerX = this.pointerX, this.prevPointerY = this.pointerY, this.pointerX = o.clientX, this.pointerY = o.clientY;
41
+ const c = this.pointerX - this.prevPointerX, n = this.pointerY - this.prevPointerY;
42
+ this.pointerSpeed = Math.sqrt(c * c + n * n);
43
+ }, this._onTouchEnd = () => {
44
+ this.pointerDown = !1, this.pointerSpeed = 0;
45
+ };
46
+ }
47
+ // ——— Public API ———
48
+ setLetters(e) {
49
+ this.letters = e.map((t) => ({
50
+ char: t.char,
51
+ ox: t.x,
52
+ oy: t.y,
53
+ x: t.x,
54
+ y: t.y,
55
+ vx: 0,
56
+ vy: 0,
57
+ font: t.font
58
+ }));
59
+ }
60
+ updateSettings(e) {
61
+ Object.assign(this.settings, e), (e.buildColors || e.colorBuckets) && (this.colors = this.settings.buildColors(this.settings.colorBuckets));
62
+ }
63
+ resize(e, t) {
64
+ this.canvas.width = e, this.canvas.height = t;
65
+ const i = Math.ceil(e / this.settings.fieldScale), s = Math.ceil(t / this.settings.fieldScale);
66
+ this.field.resize(i, s), this.ripple.resize(e, t);
67
+ }
68
+ start() {
69
+ this.running && this.stop(), this.running = !0, this.canvas.addEventListener("mousedown", this._onDown), this.canvas.addEventListener("mousemove", this._onMove), window.addEventListener("mouseup", this._onUp), this.canvas.addEventListener("touchstart", this._onTouchStart, {
70
+ passive: !0
71
+ }), this.canvas.addEventListener("touchmove", this._onTouchMove, {
72
+ passive: !0
73
+ }), window.addEventListener("touchend", this._onTouchEnd);
74
+ const e = (t) => {
75
+ this.running && (this.lastFrameTime = t, this.fpsFrames++, t - this.fpsLastTime >= 1e3 && (this.fpsDisplay = this.fpsFrames, this.fpsFrames = 0, this.fpsLastTime = t), this.emitRipples(t), this.updatePhysics(t), this.render(), this.rafId = requestAnimationFrame(e));
76
+ };
77
+ this.rafId = requestAnimationFrame(e);
78
+ }
79
+ stop() {
80
+ this.running = !1, cancelAnimationFrame(this.rafId), this.rafId = 0, this.pointerDown = !1, this.pointerSpeed = 0, this.canvas.removeEventListener("mousedown", this._onDown), this.canvas.removeEventListener("mousemove", this._onMove), window.removeEventListener("mouseup", this._onUp), this.canvas.removeEventListener("touchstart", this._onTouchStart), this.canvas.removeEventListener("touchmove", this._onTouchMove), window.removeEventListener("touchend", this._onTouchEnd);
81
+ }
82
+ // ——— Private ———
83
+ emitRipples(e) {
84
+ if (!this.pointerDown) return;
85
+ const t = Math.min(this.pointerSpeed * 0.1, 1), i = 800 * (1 - t) + this.settings.rippleInterval * t;
86
+ e - this.lastRippleTime >= i && (this.lastRippleTime = e, this.ripple.addRipple(this.pointerX, this.pointerY, e)), this.pointerSpeed *= 0.85;
87
+ }
88
+ updatePhysics(e) {
89
+ const t = this.settings, i = t.fieldScale;
90
+ this.field.update(e), this.ripple.update(e), this.ripple.prune(e);
91
+ for (const s of this.letters) {
92
+ const r = s.x / i, a = s.y / i, { brightness: h, gradX: o, gradY: c } = this.field.sample(r, a), n = h * h;
93
+ s.vx -= o * t.gradientForce * n * M, s.vy -= c * t.gradientForce * n * M;
94
+ const [l, d, f] = this.ripple.computeForce(
95
+ s.x,
96
+ s.y,
97
+ e
98
+ );
99
+ s.vx += l * M, s.vy += d * M;
100
+ const m = s.ox - s.x, g = s.oy - s.y, v = 1 - h, F = 1 - f * 0.85, S = (t.springForce + v * t.darkSpringBoost) * F;
101
+ s.vx += m * S * M, s.vy += g * S * M, s.vx *= t.damping, s.vy *= t.damping, s.x += s.vx * M, s.y += s.vy * M;
102
+ const x = s.x - s.ox, w = s.y - s.oy, u = Math.sqrt(x * x + w * w);
103
+ if (u > t.maxDisplacement) {
104
+ const y = t.maxDisplacement / u;
105
+ s.x = s.ox + x * y, s.y = s.oy + w * y, s.vx *= 0.5, s.vy *= 0.5;
106
+ }
107
+ }
108
+ }
109
+ render() {
110
+ const { ctx: e, canvas: t, settings: i } = this, s = t.width, r = t.height;
111
+ e.fillStyle = i.bgColor, e.fillRect(0, 0, s, r);
112
+ const a = 1 / i.maxDisplacement, h = i.colorBuckets - 1;
113
+ e.textBaseline = "alphabetic";
114
+ let o = "";
115
+ for (const c of this.letters) {
116
+ const n = c.x - c.ox, l = c.y - c.oy, f = Math.min(Math.sqrt(n * n + l * l) * a, 1) * h + 0.5 | 0;
117
+ c.font !== o && (e.font = c.font, o = c.font), e.fillStyle = this.colors[f], e.fillText(c.char, c.x, c.y);
118
+ }
119
+ if (i.showFps) {
120
+ e.font = "11px monospace", e.textBaseline = "bottom", e.fillStyle = "rgba(100,180,255,0.4)";
121
+ const c = `${this.fpsDisplay} fps | ${this.letters.length} chars | ${this.ripple.activeCount()} ripples`, n = e.measureText(c).width;
122
+ e.fillText(c, s - n - 10, r - 10);
123
+ }
124
+ }
125
+ }
126
+ const B = 0.5 * (Math.sqrt(3) - 1), _ = (3 - Math.sqrt(3)) / 6, X = [1, -1, 1, -1, 1, -1, 0, 0], D = [1, 1, -1, -1, 0, 0, 1, -1], b = new Uint8Array(512), Y = new Uint8Array(512), T = new Uint8Array(256);
127
+ for (let p = 0; p < 256; p++) T[p] = p;
128
+ for (let p = 255; p > 0; p--) {
129
+ const e = Math.floor(Math.random() * (p + 1));
130
+ [T[p], T[e]] = [T[e], T[p]];
131
+ }
132
+ for (let p = 0; p < 512; p++)
133
+ b[p] = T[p & 255], Y[p] = b[p] & 7;
134
+ function E(p, e) {
135
+ const t = (p + e) * B, i = Math.floor(p + t), s = Math.floor(e + t), r = (i + s) * _, a = p - (i - r), h = e - (s - r), o = a > h ? 1 : 0, c = 1 - o, n = a - o + _, l = h - c + _, d = a - 1 + 2 * _, f = h - 1 + 2 * _, m = i & 255, g = s & 255;
136
+ let v = 0, F = 0, S = 0, x = 0.5 - a * a - h * h;
137
+ if (x >= 0) {
138
+ x *= x;
139
+ const y = Y[m + b[g]];
140
+ v = x * x * (X[y] * a + D[y] * h);
141
+ }
142
+ let w = 0.5 - n * n - l * l;
143
+ if (w >= 0) {
144
+ w *= w;
145
+ const y = Y[m + o + b[g + c]];
146
+ F = w * w * (X[y] * n + D[y] * l);
147
+ }
148
+ let u = 0.5 - d * d - f * f;
149
+ if (u >= 0) {
150
+ u *= u;
151
+ const y = Y[m + 1 + b[g + 1]];
152
+ S = u * u * (X[y] * d + D[y] * f);
153
+ }
154
+ return 70 * (v + F + S);
155
+ }
156
+ const A = {
157
+ noiseScale: 4e-3,
158
+ timeSpeed: 3e-4,
159
+ sharpness: 0.6
160
+ };
161
+ class $ {
162
+ constructor(e) {
163
+ this.w = 0, this.h = 0, this.brightness = new Float32Array(0), this.gradXBuf = new Float32Array(0), this.gradYBuf = new Float32Array(0), this.frameCount = 0, this.settings = { ...A, ...e };
164
+ }
165
+ resize(e, t) {
166
+ this.w = e, this.h = t;
167
+ const i = e * t;
168
+ this.brightness = new Float32Array(i), this.gradXBuf = new Float32Array(i), this.gradYBuf = new Float32Array(i);
169
+ }
170
+ update(e) {
171
+ this.frameCount++;
172
+ const { w: t, h: i, brightness: s } = this;
173
+ if ((this.frameCount & 1) === 0) {
174
+ const r = this.settings.noiseScale, a = e * this.settings.timeSpeed, h = this.settings.sharpness, o = Math.abs(h - 1) > 0.01 && Math.abs(h - 0.5) > 0.01, c = Math.abs(h - 0.5) < 0.01, n = a * 0.7, l = a * 0.5, d = a * 0.4, f = a * 0.3, m = a * 0.2, g = a * 0.6;
175
+ for (let v = 0; v < i; v++) {
176
+ const F = v * r, S = v * t;
177
+ for (let x = 0; x < t; x++) {
178
+ const w = x * r;
179
+ let u = 0;
180
+ u += E(w + n, F + l) * 0.5, u += E(w * 2 - d, F * 2 + f) * 0.25, u += E(w * 4 + m, F * 4 - g) * 0.125, u = u < 0 ? -u : u, c ? u = Math.sqrt(u) : o && (u = Math.pow(u, h)), s[S + x] = 1 - u;
181
+ }
182
+ }
183
+ }
184
+ this.computeGradients();
185
+ }
186
+ sample(e, t) {
187
+ const { w: i, h: s, brightness: r, gradXBuf: a, gradYBuf: h } = this, o = i - 1.001, c = s - 1.001;
188
+ e < 0 ? e = 0 : e > o && (e = o), t < 0 ? t = 0 : t > c && (t = c);
189
+ const n = e | 0, l = t | 0, d = e - n, f = t - l, m = l * i + n, g = 1 - d, v = 1 - f, F = r[m] * g * v + r[m + 1] * d * v + r[m + i] * g * f + r[m + i + 1] * d * f, S = (t | 0) * i + (e | 0);
190
+ return { brightness: F, gradX: a[S], gradY: h[S] };
191
+ }
192
+ computeGradients() {
193
+ const { w: e, h: t, brightness: i, gradXBuf: s, gradYBuf: r } = this;
194
+ for (let a = 0; a < t; a++) {
195
+ const h = a * e, o = a > 1 ? (a - 2) * e : 0, c = a < t - 2 ? (a + 2) * e : (t - 1) * e;
196
+ for (let n = 0; n < e; n++) {
197
+ const l = n > 1 ? n - 2 : 0, d = n < e - 2 ? n + 2 : e - 1;
198
+ s[h + n] = (i[h + d] - i[h + l]) * 0.25, r[h + n] = (i[c + n] - i[o + n]) * 0.25;
199
+ }
200
+ }
201
+ }
202
+ }
203
+ const C = {
204
+ amplitude: 1.25,
205
+ wavelength: 800,
206
+ decay: 48e-4,
207
+ force: 2350,
208
+ waveCenterY: -0.05,
209
+ waveAmpMul: 1.4
210
+ };
211
+ class k {
212
+ constructor(e) {
213
+ this.ripples = [], this.settings = { ...C, ...e };
214
+ }
215
+ resize(e, t) {
216
+ }
217
+ addRipple(e, t, i) {
218
+ const s = this.settings;
219
+ this.ripples.push({
220
+ cx: e,
221
+ cy: t,
222
+ startTime: i,
223
+ wavelength: s.wavelength,
224
+ amplitude: s.amplitude,
225
+ decay: s.decay,
226
+ _radius: 0,
227
+ _timeFade: 1,
228
+ _ringWidth: s.wavelength * 0.15,
229
+ _baseSpeed: s.wavelength * 2e-3
230
+ });
231
+ }
232
+ update(e) {
233
+ for (const t of this.ripples) {
234
+ const i = e - t.startTime;
235
+ t._timeFade = Math.exp(-i * t.decay), t._ringWidth = t.wavelength * 0.15, t._baseSpeed = t.wavelength * 2e-3;
236
+ const s = 2 * Math.PI * i / t.wavelength, r = (1 - Math.cos(s + this.settings.waveCenterY * Math.PI)) * this.settings.waveAmpMul, a = t._baseSpeed * i * 0.5 * r;
237
+ t._radius = t._baseSpeed * i * 0.3 + a * 0.7;
238
+ }
239
+ }
240
+ prune(e) {
241
+ for (let t = this.ripples.length - 1; t >= 0; t--)
242
+ this.ripples[t]._timeFade < 0.01 && this.ripples.splice(t, 1);
243
+ }
244
+ computeForce(e, t, i) {
245
+ let s = 0, r = 0, a = 0;
246
+ const h = this.settings.force;
247
+ for (const o of this.ripples) {
248
+ const c = e - o.cx, n = t - o.cy, l = c * c + n * n;
249
+ if (l < 1) continue;
250
+ const d = Math.sqrt(l), f = d - o._radius, m = o._ringWidth, g = m * 3;
251
+ if (f > g || f < -g) continue;
252
+ const v = -f / m, F = f / m, S = Math.exp(-F * F), x = f < 0 ? Math.exp(f / m) : Math.exp(-f * 2 / m), w = v * S * h * 1.5, u = x * h * o._baseSpeed * 0.8, y = (w + u) * o._timeFade * o.amplitude, L = 1 / d;
253
+ s += c * L * y, r += n * L * y, a = Math.max(a, S * o._timeFade);
254
+ }
255
+ return [s, r, a];
256
+ }
257
+ activeCount() {
258
+ return this.ripples.length;
259
+ }
260
+ }
261
+ function W(p, e = 4e3) {
262
+ const t = [], i = document.createTreeWalker(p, NodeFilter.SHOW_TEXT), s = document.createRange();
263
+ let r = i.nextNode();
264
+ for (; r && t.length < e; ) {
265
+ const a = r.textContent ?? "";
266
+ if (!a.trim()) {
267
+ r = i.nextNode();
268
+ continue;
269
+ }
270
+ const h = r.parentElement;
271
+ if (!h) {
272
+ r = i.nextNode();
273
+ continue;
274
+ }
275
+ const o = getComputedStyle(h);
276
+ if (o.display === "none" || o.visibility === "hidden" || o.opacity === "0") {
277
+ r = i.nextNode();
278
+ continue;
279
+ }
280
+ const c = `${o.fontStyle} ${o.fontWeight} ${o.fontSize} ${o.fontFamily}`;
281
+ for (let n = 0; n < a.length && t.length < e; n++) {
282
+ const l = a[n];
283
+ if (/\s/.test(l)) continue;
284
+ s.setStart(r, n), s.setEnd(r, n + 1);
285
+ const d = s.getBoundingClientRect();
286
+ d.width === 0 || d.height === 0 || t.push({
287
+ char: l,
288
+ x: d.left,
289
+ y: d.top + d.height * 0.75,
290
+ font: c
291
+ });
292
+ }
293
+ r = i.nextNode();
294
+ }
295
+ return s.detach(), t;
296
+ }
297
+ function q(p, e, t = {}) {
298
+ const i = t.fontSize ?? 16, s = t.font ?? `${i}px "Courier New", monospace`, r = t.margin ?? 40, a = (t.maxWidth ?? p.canvas.width) - r * 2, h = t.maxHeight ?? p.canvas.height, o = t.lineHeight ?? i * 1.6;
299
+ p.font = s;
300
+ const c = [];
301
+ let n = r, l = r + i;
302
+ const d = e.split(" ");
303
+ for (const f of d) {
304
+ const m = p.measureText(f + " ").width;
305
+ if (n + m > r + a && n > r && (n = r, l += o), l > h - r) break;
306
+ for (const g of f) {
307
+ const v = p.measureText(g).width;
308
+ c.push({ char: g, x: n, y: l, font: s }), n += v;
309
+ }
310
+ n += p.measureText(" ").width;
311
+ }
312
+ return c;
313
+ }
314
+ export {
315
+ R as RippleTextEngine,
316
+ $ as WaterField,
317
+ k as WaveRipple,
318
+ W as extractTextFromDOM,
319
+ q as layoutText
320
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "ripple-text",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Physics-driven text effect with pluggable field and ripple algorithms",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "dev": "vite demo",
13
+ "build": "tsc && vite build",
14
+ "typecheck": "tsc --noEmit",
15
+ "lint": "eslint .",
16
+ "lint:fix": "eslint . --fix",
17
+ "format": "prettier --write .",
18
+ "format:check": "prettier --check .",
19
+ "prepare": "husky"
20
+ },
21
+ "lint-staged": {
22
+ "*.{ts,js}": [
23
+ "eslint --fix",
24
+ "prettier --write"
25
+ ],
26
+ "*.{json,md,yml,yaml}": [
27
+ "prettier --write"
28
+ ]
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^10.0.1",
32
+ "eslint": "^10.1.0",
33
+ "eslint-config-prettier": "^10.1.8",
34
+ "husky": "^9.1.7",
35
+ "lint-staged": "^16.4.0",
36
+ "prettier": "^3.8.1",
37
+ "typescript": "^5.7.0",
38
+ "typescript-eslint": "^8.58.0",
39
+ "vite": "^6.3.0"
40
+ }
41
+ }