@jepepa/like-button 0.8.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/LICENSE +22 -0
- package/README.md +373 -0
- package/dist/chunk-WTGBDIZW.js +841 -0
- package/dist/chunk-WTGBDIZW.js.map +1 -0
- package/dist/index.d.ts +580 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/vanilla.d.ts +2 -0
- package/dist/vanilla.js +22 -0
- package/dist/vanilla.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
// src/LikeButton/DefaultHeartIcon.tsx
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
function DefaultHeartIcon({ size, className }) {
|
|
4
|
+
return /* @__PURE__ */ jsx(
|
|
5
|
+
"svg",
|
|
6
|
+
{
|
|
7
|
+
className,
|
|
8
|
+
style: {
|
|
9
|
+
width: size,
|
|
10
|
+
height: size,
|
|
11
|
+
stroke: "#111827",
|
|
12
|
+
strokeWidth: 2,
|
|
13
|
+
fill: "transparent"
|
|
14
|
+
},
|
|
15
|
+
viewBox: "0 0 24 24",
|
|
16
|
+
"aria-hidden": "true",
|
|
17
|
+
children: /* @__PURE__ */ jsx(
|
|
18
|
+
"path",
|
|
19
|
+
{
|
|
20
|
+
strokeLinecap: "round",
|
|
21
|
+
strokeLinejoin: "round",
|
|
22
|
+
d: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/LikeButton/LikeButton.tsx
|
|
30
|
+
import { useId, useMemo } from "react";
|
|
31
|
+
|
|
32
|
+
// src/Particle/shapes/CircleShape.tsx
|
|
33
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
34
|
+
function CircleShape({ size, color, className = "" }) {
|
|
35
|
+
return /* @__PURE__ */ jsx2(
|
|
36
|
+
"svg",
|
|
37
|
+
{
|
|
38
|
+
width: size,
|
|
39
|
+
height: size,
|
|
40
|
+
viewBox: "0 0 24 24",
|
|
41
|
+
className: `fill-current ${className}`,
|
|
42
|
+
style: { color },
|
|
43
|
+
"aria-hidden": "true",
|
|
44
|
+
children: /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" })
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/Particle/shapes/HeartShape.tsx
|
|
50
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
51
|
+
function HeartShape({ size, color, className = "" }) {
|
|
52
|
+
return /* @__PURE__ */ jsx3(
|
|
53
|
+
"svg",
|
|
54
|
+
{
|
|
55
|
+
width: size,
|
|
56
|
+
height: size,
|
|
57
|
+
viewBox: "0 0 24 24",
|
|
58
|
+
className: `fill-current ${className}`,
|
|
59
|
+
style: { color },
|
|
60
|
+
"aria-hidden": "true",
|
|
61
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" })
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/Particle/shapes/SparkleShape.tsx
|
|
67
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
68
|
+
function SparkleShape({ size, color, className = "" }) {
|
|
69
|
+
return /* @__PURE__ */ jsx4(
|
|
70
|
+
"svg",
|
|
71
|
+
{
|
|
72
|
+
width: size,
|
|
73
|
+
height: size,
|
|
74
|
+
viewBox: "0 0 24 24",
|
|
75
|
+
className: `fill-current ${className}`,
|
|
76
|
+
style: { color },
|
|
77
|
+
"aria-hidden": "true",
|
|
78
|
+
children: /* @__PURE__ */ jsx4("path", { d: "M12 2l2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5L12 2z" })
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/Particle/shapes/SquareShape.tsx
|
|
84
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
85
|
+
function SquareShape({ size, color, className = "" }) {
|
|
86
|
+
return /* @__PURE__ */ jsx5(
|
|
87
|
+
"svg",
|
|
88
|
+
{
|
|
89
|
+
width: size,
|
|
90
|
+
height: size,
|
|
91
|
+
viewBox: "0 0 24 24",
|
|
92
|
+
className: `fill-current ${className}`,
|
|
93
|
+
style: { color },
|
|
94
|
+
"aria-hidden": "true",
|
|
95
|
+
children: /* @__PURE__ */ jsx5("rect", { x: "3", y: "3", width: "18", height: "18", rx: "3" })
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/Particle/shapes/StarShape.tsx
|
|
101
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
102
|
+
function StarShape({ size, color, className = "" }) {
|
|
103
|
+
return /* @__PURE__ */ jsx6(
|
|
104
|
+
"svg",
|
|
105
|
+
{
|
|
106
|
+
width: size,
|
|
107
|
+
height: size,
|
|
108
|
+
viewBox: "0 0 24 24",
|
|
109
|
+
className: `fill-current ${className}`,
|
|
110
|
+
style: { color },
|
|
111
|
+
"aria-hidden": "true",
|
|
112
|
+
children: /* @__PURE__ */ jsx6("path", { d: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" })
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/Particle/shapes/utils.ts
|
|
118
|
+
function getParticleShape(shape) {
|
|
119
|
+
if (typeof shape === "object" && "render" in shape) {
|
|
120
|
+
return ({ size, color, className }) => shape.render({ size, color, className });
|
|
121
|
+
}
|
|
122
|
+
switch (shape) {
|
|
123
|
+
case "heart":
|
|
124
|
+
return HeartShape;
|
|
125
|
+
case "star":
|
|
126
|
+
return StarShape;
|
|
127
|
+
case "circle":
|
|
128
|
+
return CircleShape;
|
|
129
|
+
case "square":
|
|
130
|
+
return SquareShape;
|
|
131
|
+
case "sparkle":
|
|
132
|
+
return SparkleShape;
|
|
133
|
+
default:
|
|
134
|
+
return HeartShape;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/Particle/useParticle.ts
|
|
139
|
+
import { useEffect, useState } from "react";
|
|
140
|
+
function useParticle({
|
|
141
|
+
angle,
|
|
142
|
+
distance,
|
|
143
|
+
scale,
|
|
144
|
+
speed,
|
|
145
|
+
easing,
|
|
146
|
+
fadeOut
|
|
147
|
+
}) {
|
|
148
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const timer = requestAnimationFrame(() => setIsAnimating(true));
|
|
151
|
+
return () => cancelAnimationFrame(timer);
|
|
152
|
+
}, []);
|
|
153
|
+
const x = Math.cos(angle * Math.PI / 180) * distance;
|
|
154
|
+
const y = Math.sin(angle * Math.PI / 180) * distance;
|
|
155
|
+
return {
|
|
156
|
+
isAnimating,
|
|
157
|
+
x,
|
|
158
|
+
y,
|
|
159
|
+
transform: isAnimating ? `translate(${x}px, ${y}px) scale(${scale})` : "translate(0, 0) scale(0)",
|
|
160
|
+
opacity: isAnimating ? 0 : 1,
|
|
161
|
+
speed,
|
|
162
|
+
easing,
|
|
163
|
+
fadeOut
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/Particle/Particle.tsx
|
|
168
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
169
|
+
function Particle({
|
|
170
|
+
angle,
|
|
171
|
+
distance,
|
|
172
|
+
scale,
|
|
173
|
+
color,
|
|
174
|
+
shape,
|
|
175
|
+
speed,
|
|
176
|
+
easing,
|
|
177
|
+
fadeOut
|
|
178
|
+
}) {
|
|
179
|
+
const { transform, opacity } = useParticle({
|
|
180
|
+
angle,
|
|
181
|
+
distance,
|
|
182
|
+
scale,
|
|
183
|
+
speed,
|
|
184
|
+
easing,
|
|
185
|
+
fadeOut
|
|
186
|
+
});
|
|
187
|
+
const ShapeComponent = getParticleShape(shape);
|
|
188
|
+
return /* @__PURE__ */ jsx7(
|
|
189
|
+
"div",
|
|
190
|
+
{
|
|
191
|
+
className: "absolute w-10 h-10 transition-all",
|
|
192
|
+
style: {
|
|
193
|
+
color,
|
|
194
|
+
transform,
|
|
195
|
+
opacity: fadeOut ? opacity : 1,
|
|
196
|
+
transitionDuration: `${speed}ms`,
|
|
197
|
+
transitionTimingFunction: easing
|
|
198
|
+
},
|
|
199
|
+
children: /* @__PURE__ */ jsx7(ShapeComponent, { size: 40, color, className: "w-full h-full" })
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/LikeButton/useLikeButton.ts
|
|
205
|
+
import { useCallback, useState as useState2 } from "react";
|
|
206
|
+
|
|
207
|
+
// src/Particle/presets.ts
|
|
208
|
+
var DEFAULT_PARTICLE_CONFIG = {
|
|
209
|
+
shape: "heart",
|
|
210
|
+
speed: 500,
|
|
211
|
+
distance: { min: 60, max: 100 },
|
|
212
|
+
spread: 360,
|
|
213
|
+
spreadOffset: 0,
|
|
214
|
+
size: { min: 1, max: 1.5 },
|
|
215
|
+
colors: ["#EF4444", "#B9FF14", "#3B82F6"],
|
|
216
|
+
count: 8,
|
|
217
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
218
|
+
fadeOut: true
|
|
219
|
+
};
|
|
220
|
+
var PARTICLE_PRESETS = {
|
|
221
|
+
/**
|
|
222
|
+
* Burst - Fast, enthusiastic explosion effect
|
|
223
|
+
* - Wide 360° spread
|
|
224
|
+
* - Fast animation (400ms)
|
|
225
|
+
* - Multiple colors
|
|
226
|
+
* - 12 particles
|
|
227
|
+
* - Perfect for: Likes, favorites, celebrations
|
|
228
|
+
*/
|
|
229
|
+
burst: {
|
|
230
|
+
shape: "heart",
|
|
231
|
+
speed: 400,
|
|
232
|
+
distance: { min: 80, max: 120 },
|
|
233
|
+
spread: 360,
|
|
234
|
+
spreadOffset: 0,
|
|
235
|
+
size: { min: 1, max: 1.5 },
|
|
236
|
+
colors: ["#EF4444", "#F59E0B", "#3B82F6"],
|
|
237
|
+
count: 12,
|
|
238
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
239
|
+
fadeOut: true
|
|
240
|
+
},
|
|
241
|
+
/**
|
|
242
|
+
* Fountain - Upward spray effect
|
|
243
|
+
* - 120° upward spread
|
|
244
|
+
* - Medium animation (600ms)
|
|
245
|
+
* - Cool colors (blue, purple, pink)
|
|
246
|
+
* - 10 particles
|
|
247
|
+
* - Perfect for: Achievements, upgrades, success
|
|
248
|
+
*/
|
|
249
|
+
fountain: {
|
|
250
|
+
shape: "circle",
|
|
251
|
+
speed: 600,
|
|
252
|
+
distance: { min: 60, max: 100 },
|
|
253
|
+
spread: 120,
|
|
254
|
+
spreadOffset: -90,
|
|
255
|
+
// Upward
|
|
256
|
+
size: { min: 0.8, max: 1.2 },
|
|
257
|
+
colors: ["#3B82F6", "#8B5CF6", "#EC4899"],
|
|
258
|
+
count: 10,
|
|
259
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
260
|
+
fadeOut: true
|
|
261
|
+
},
|
|
262
|
+
/**
|
|
263
|
+
* Confetti - Colorful celebration effect
|
|
264
|
+
* - Full 360° spread
|
|
265
|
+
* - Slow animation (800ms)
|
|
266
|
+
* - Rainbow colors
|
|
267
|
+
* - 15 particles
|
|
268
|
+
* - Perfect for: Milestones, victories, special events
|
|
269
|
+
*/
|
|
270
|
+
confetti: {
|
|
271
|
+
shape: "square",
|
|
272
|
+
speed: 800,
|
|
273
|
+
distance: { min: 70, max: 110 },
|
|
274
|
+
spread: 360,
|
|
275
|
+
spreadOffset: 0,
|
|
276
|
+
size: { min: 0.6, max: 1.4 },
|
|
277
|
+
colors: ["#EF4444", "#F59E0B", "#10B981", "#3B82F6", "#8B5CF6", "#EC4899"],
|
|
278
|
+
count: 15,
|
|
279
|
+
easing: "ease-out",
|
|
280
|
+
fadeOut: true
|
|
281
|
+
},
|
|
282
|
+
/**
|
|
283
|
+
* Gentle - Subtle, calm effect
|
|
284
|
+
* - 180° upward spread
|
|
285
|
+
* - Slow animation (700ms)
|
|
286
|
+
* - Soft red tones
|
|
287
|
+
* - 6 particles
|
|
288
|
+
* - Perfect for: Subtle interactions, quiet appreciation
|
|
289
|
+
*/
|
|
290
|
+
gentle: {
|
|
291
|
+
shape: "heart",
|
|
292
|
+
speed: 700,
|
|
293
|
+
distance: { min: 40, max: 60 },
|
|
294
|
+
spread: 180,
|
|
295
|
+
spreadOffset: -90,
|
|
296
|
+
size: { min: 0.8, max: 1 },
|
|
297
|
+
colors: ["#EF4444", "#F87171"],
|
|
298
|
+
count: 6,
|
|
299
|
+
easing: "ease-in-out",
|
|
300
|
+
fadeOut: true
|
|
301
|
+
},
|
|
302
|
+
/**
|
|
303
|
+
* Fireworks - Explosive sparkle effect
|
|
304
|
+
* - Full 360° spread
|
|
305
|
+
* - Medium-fast animation (500ms)
|
|
306
|
+
* - Warm sparkle colors
|
|
307
|
+
* - 16 particles
|
|
308
|
+
* - Large size range
|
|
309
|
+
* - Perfect for: Big celebrations, major achievements
|
|
310
|
+
*/
|
|
311
|
+
fireworks: {
|
|
312
|
+
shape: "sparkle",
|
|
313
|
+
speed: 500,
|
|
314
|
+
distance: { min: 100, max: 150 },
|
|
315
|
+
spread: 360,
|
|
316
|
+
spreadOffset: 0,
|
|
317
|
+
size: { min: 1.2, max: 2 },
|
|
318
|
+
colors: ["#FBBF24", "#F59E0B", "#EF4444", "#EC4899"],
|
|
319
|
+
count: 16,
|
|
320
|
+
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
321
|
+
fadeOut: true
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
function getParticlePreset(preset) {
|
|
325
|
+
return PARTICLE_PRESETS[preset];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/Particle/utils.ts
|
|
329
|
+
function normalizeRange(value) {
|
|
330
|
+
return typeof value === "number" ? { min: value, max: value } : value;
|
|
331
|
+
}
|
|
332
|
+
function randomInRange(range) {
|
|
333
|
+
return range.min + Math.random() * (range.max - range.min);
|
|
334
|
+
}
|
|
335
|
+
function normalizeAngle(angle) {
|
|
336
|
+
const normalized = angle % 360;
|
|
337
|
+
const result = normalized < 0 ? normalized + 360 : normalized;
|
|
338
|
+
return result === 0 ? 0 : result;
|
|
339
|
+
}
|
|
340
|
+
function randomAngle(spread, offset) {
|
|
341
|
+
const angle = offset + Math.random() * spread;
|
|
342
|
+
return normalizeAngle(angle);
|
|
343
|
+
}
|
|
344
|
+
function mergeParticleConfig(base, override) {
|
|
345
|
+
if (!override) {
|
|
346
|
+
return base;
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
...base,
|
|
350
|
+
...override
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function resolveParticleConfig(preset, config) {
|
|
354
|
+
let resolved = { ...DEFAULT_PARTICLE_CONFIG };
|
|
355
|
+
if (preset) {
|
|
356
|
+
resolved = mergeParticleConfig(resolved, getParticlePreset(preset));
|
|
357
|
+
}
|
|
358
|
+
if (config) {
|
|
359
|
+
resolved = mergeParticleConfig(resolved, config);
|
|
360
|
+
}
|
|
361
|
+
return resolved;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/LikeButton/useLikeButton.ts
|
|
365
|
+
var LIKE_BUTTON_DEFAULTS = {
|
|
366
|
+
maxClicks: 14,
|
|
367
|
+
size: 96,
|
|
368
|
+
fillColor: "#EF4444",
|
|
369
|
+
waveColor: "#B91C1C"
|
|
370
|
+
};
|
|
371
|
+
function useLikeButton(options = {}) {
|
|
372
|
+
const {
|
|
373
|
+
localClicks: externalLocalClicks,
|
|
374
|
+
maxClicks = LIKE_BUTTON_DEFAULTS.maxClicks,
|
|
375
|
+
onClick,
|
|
376
|
+
onRightClick,
|
|
377
|
+
disabled: externalDisabled,
|
|
378
|
+
showParticles = true,
|
|
379
|
+
particlePreset,
|
|
380
|
+
particleConfig,
|
|
381
|
+
ariaLabel: customAriaLabel
|
|
382
|
+
} = options;
|
|
383
|
+
const [internalLocalClicks, setInternalLocalClicks] = useState2(0);
|
|
384
|
+
const [particles, setParticles] = useState2([]);
|
|
385
|
+
const localClicks = externalLocalClicks ?? internalLocalClicks;
|
|
386
|
+
const isMaxed = localClicks >= maxClicks;
|
|
387
|
+
const disabled = externalDisabled ?? isMaxed;
|
|
388
|
+
const spawnParticles = useCallback(() => {
|
|
389
|
+
if (!showParticles) return;
|
|
390
|
+
const config = resolveParticleConfig(particlePreset, particleConfig);
|
|
391
|
+
const distanceRange = normalizeRange(config.distance);
|
|
392
|
+
const sizeRange = normalizeRange(config.size);
|
|
393
|
+
const id = Date.now();
|
|
394
|
+
const newParticles = Array.from({ length: config.count }).map((_, i) => ({
|
|
395
|
+
id: `${id}-${i}`,
|
|
396
|
+
angle: randomAngle(config.spread, config.spreadOffset),
|
|
397
|
+
distance: randomInRange(distanceRange),
|
|
398
|
+
scale: randomInRange(sizeRange),
|
|
399
|
+
color: config.colors[Math.floor(Math.random() * config.colors.length)],
|
|
400
|
+
shape: config.shape,
|
|
401
|
+
speed: config.speed,
|
|
402
|
+
easing: config.easing,
|
|
403
|
+
fadeOut: config.fadeOut
|
|
404
|
+
}));
|
|
405
|
+
setParticles((prev) => [...prev, ...newParticles]);
|
|
406
|
+
const cleanupDelay = config.speed + 100;
|
|
407
|
+
setTimeout(() => {
|
|
408
|
+
setParticles((prev) => prev.filter((p) => !newParticles.find((np) => np.id === p.id)));
|
|
409
|
+
}, cleanupDelay);
|
|
410
|
+
}, [showParticles, particlePreset, particleConfig]);
|
|
411
|
+
const handleClick = useCallback(() => {
|
|
412
|
+
if (disabled) return;
|
|
413
|
+
const newLocalClicks = localClicks + 1;
|
|
414
|
+
if (externalLocalClicks === void 0) {
|
|
415
|
+
setInternalLocalClicks(newLocalClicks);
|
|
416
|
+
}
|
|
417
|
+
spawnParticles();
|
|
418
|
+
onClick?.(newLocalClicks);
|
|
419
|
+
}, [disabled, localClicks, externalLocalClicks, spawnParticles, onClick]);
|
|
420
|
+
const handleRightClick = useCallback(
|
|
421
|
+
(e) => {
|
|
422
|
+
e.preventDefault();
|
|
423
|
+
if (disabled) return;
|
|
424
|
+
onRightClick?.(localClicks);
|
|
425
|
+
},
|
|
426
|
+
[disabled, localClicks, onRightClick]
|
|
427
|
+
);
|
|
428
|
+
const fillPercentage = localClicks / maxClicks * 100;
|
|
429
|
+
const defaultAriaLabel = isMaxed ? "Thank you for your likes!" : `Like this content. ${maxClicks - localClicks} clicks remaining`;
|
|
430
|
+
return {
|
|
431
|
+
localClicks,
|
|
432
|
+
isMaxed,
|
|
433
|
+
disabled,
|
|
434
|
+
fillPercentage,
|
|
435
|
+
particles,
|
|
436
|
+
handleClick,
|
|
437
|
+
handleRightClick,
|
|
438
|
+
ariaLabel: customAriaLabel ?? defaultAriaLabel,
|
|
439
|
+
isPressed: localClicks > 0
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/LikeButton/utils.ts
|
|
444
|
+
var DEFAULT_STYLES = {
|
|
445
|
+
borderWidth: 4,
|
|
446
|
+
borderColor: "#111827",
|
|
447
|
+
shadowOffset: 8,
|
|
448
|
+
shadowColor: "#111827",
|
|
449
|
+
backgroundColor: "white"
|
|
450
|
+
};
|
|
451
|
+
function getShapeStyles(shape = "circle") {
|
|
452
|
+
if (typeof shape === "string") {
|
|
453
|
+
switch (shape) {
|
|
454
|
+
case "circle":
|
|
455
|
+
return { borderRadius: "9999px" };
|
|
456
|
+
case "rounded":
|
|
457
|
+
return { borderRadius: "1rem" };
|
|
458
|
+
case "square":
|
|
459
|
+
return { borderRadius: "0" };
|
|
460
|
+
default:
|
|
461
|
+
return { borderRadius: "9999px" };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const styles = {};
|
|
465
|
+
if (shape.clipPath) {
|
|
466
|
+
styles.clipPath = shape.clipPath;
|
|
467
|
+
styles.borderRadius = shape.borderRadius ?? "0";
|
|
468
|
+
} else if (shape.borderRadius) {
|
|
469
|
+
styles.borderRadius = shape.borderRadius;
|
|
470
|
+
}
|
|
471
|
+
return styles;
|
|
472
|
+
}
|
|
473
|
+
function computeHoverOffset(shadowOffset) {
|
|
474
|
+
return Math.max(shadowOffset / 2, 2);
|
|
475
|
+
}
|
|
476
|
+
function computeButtonStyles(size, mergedStyles, shapeStyles) {
|
|
477
|
+
return {
|
|
478
|
+
width: size,
|
|
479
|
+
height: size,
|
|
480
|
+
borderWidth: mergedStyles.borderWidth,
|
|
481
|
+
borderColor: mergedStyles.borderColor,
|
|
482
|
+
backgroundColor: mergedStyles.backgroundColor,
|
|
483
|
+
boxShadow: `${mergedStyles.shadowOffset}px ${mergedStyles.shadowOffset}px 0px ${mergedStyles.shadowColor}`,
|
|
484
|
+
...shapeStyles
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function generateDynamicStyles(buttonSelector, hoverShadowOffset, mergedStyles) {
|
|
488
|
+
const translateOnHover = mergedStyles.shadowOffset - hoverShadowOffset;
|
|
489
|
+
return `
|
|
490
|
+
${buttonSelector}:hover:not(:disabled) {
|
|
491
|
+
box-shadow: ${hoverShadowOffset}px ${hoverShadowOffset}px 0px ${mergedStyles.shadowColor};
|
|
492
|
+
transform: translate(${translateOnHover}px, ${translateOnHover}px);
|
|
493
|
+
}
|
|
494
|
+
${buttonSelector}:active:not(:disabled) {
|
|
495
|
+
box-shadow: none;
|
|
496
|
+
transform: translate(${mergedStyles.shadowOffset}px, ${mergedStyles.shadowOffset}px) scale(0.9);
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
}
|
|
500
|
+
var CURSOR_SVGS = {
|
|
501
|
+
heart: `data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='%23EF4444' stroke='%23111827' stroke-width='1.5'><path d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/></svg>`,
|
|
502
|
+
star: `data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='%23FBBF24' stroke='%23111827' stroke-width='1.5'><path d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/></svg>`,
|
|
503
|
+
"thumbs-up": `data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='%233B82F6' stroke='%23111827' stroke-width='1.5'><path d='M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3'/></svg>`
|
|
504
|
+
};
|
|
505
|
+
var DEFAULT_HOTSPOT = 16;
|
|
506
|
+
function getCursorStyle(cursor = "heart") {
|
|
507
|
+
if (typeof cursor === "string") {
|
|
508
|
+
switch (cursor) {
|
|
509
|
+
case "pointer":
|
|
510
|
+
return "pointer";
|
|
511
|
+
case "none":
|
|
512
|
+
return "none";
|
|
513
|
+
case "heart":
|
|
514
|
+
case "star":
|
|
515
|
+
case "thumbs-up":
|
|
516
|
+
return `url("${CURSOR_SVGS[cursor]}") ${DEFAULT_HOTSPOT} ${DEFAULT_HOTSPOT}, pointer`;
|
|
517
|
+
default:
|
|
518
|
+
return "pointer";
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const {
|
|
522
|
+
url,
|
|
523
|
+
hotspotX = DEFAULT_HOTSPOT,
|
|
524
|
+
hotspotY = DEFAULT_HOTSPOT,
|
|
525
|
+
fallback = "pointer"
|
|
526
|
+
} = cursor;
|
|
527
|
+
return `url("${url}") ${hotspotX} ${hotspotY}, ${fallback}`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/LikeButton/LikeButton.tsx
|
|
531
|
+
import { jsx as jsx8, jsxs } from "react/jsx-runtime";
|
|
532
|
+
var MAX_FILL_HEIGHT = 85;
|
|
533
|
+
function LikeButton({
|
|
534
|
+
size = LIKE_BUTTON_DEFAULTS.size,
|
|
535
|
+
fillColor = LIKE_BUTTON_DEFAULTS.fillColor,
|
|
536
|
+
waveColor = LIKE_BUTTON_DEFAULTS.waveColor,
|
|
537
|
+
className = "",
|
|
538
|
+
showParticles = true,
|
|
539
|
+
renderIcon,
|
|
540
|
+
shape = "circle",
|
|
541
|
+
styles = {},
|
|
542
|
+
cursor = "heart",
|
|
543
|
+
minFillPercent = 0,
|
|
544
|
+
...hookOptions
|
|
545
|
+
}) {
|
|
546
|
+
const clampedMinFill = Math.max(0, Math.min(MAX_FILL_HEIGHT, minFillPercent));
|
|
547
|
+
const reactId = useId();
|
|
548
|
+
const buttonId = `like-button${reactId.replace(/:/g, "-")}`;
|
|
549
|
+
const {
|
|
550
|
+
handleClick,
|
|
551
|
+
handleRightClick,
|
|
552
|
+
disabled,
|
|
553
|
+
ariaLabel,
|
|
554
|
+
isPressed,
|
|
555
|
+
isMaxed,
|
|
556
|
+
fillPercentage,
|
|
557
|
+
particles
|
|
558
|
+
} = useLikeButton({ showParticles, ...hookOptions });
|
|
559
|
+
const mergedStyles = useMemo(() => ({ ...DEFAULT_STYLES, ...styles }), [styles]);
|
|
560
|
+
const shapeStyles = useMemo(() => getShapeStyles(shape), [shape]);
|
|
561
|
+
const buttonStyle = useMemo(
|
|
562
|
+
() => computeButtonStyles(size, mergedStyles, shapeStyles),
|
|
563
|
+
[size, mergedStyles, shapeStyles]
|
|
564
|
+
);
|
|
565
|
+
const hoverShadowOffset = useMemo(
|
|
566
|
+
() => computeHoverOffset(mergedStyles.shadowOffset),
|
|
567
|
+
[mergedStyles.shadowOffset]
|
|
568
|
+
);
|
|
569
|
+
const dynamicStyles = useMemo(
|
|
570
|
+
() => `
|
|
571
|
+
@keyframes wave-scroll-left {
|
|
572
|
+
from { transform: translateX(0); }
|
|
573
|
+
to { transform: translateX(-50%); }
|
|
574
|
+
}
|
|
575
|
+
@keyframes wave-scroll-right {
|
|
576
|
+
from { transform: translateX(-50%); }
|
|
577
|
+
to { transform: translateX(0); }
|
|
578
|
+
}
|
|
579
|
+
${generateDynamicStyles(`#${buttonId}`, hoverShadowOffset, mergedStyles)}
|
|
580
|
+
`,
|
|
581
|
+
[buttonId, hoverShadowOffset, mergedStyles]
|
|
582
|
+
);
|
|
583
|
+
const cursorStyle = useMemo(
|
|
584
|
+
() => disabled ? "not-allowed" : getCursorStyle(cursor),
|
|
585
|
+
[cursor, disabled]
|
|
586
|
+
);
|
|
587
|
+
const iconSize = size * 0.5;
|
|
588
|
+
const iconRenderProps = {
|
|
589
|
+
size: iconSize,
|
|
590
|
+
className: "relative z-20 transition-colors duration-300 pointer-events-none",
|
|
591
|
+
isMaxed,
|
|
592
|
+
fillPercentage
|
|
593
|
+
};
|
|
594
|
+
const renderedIcon = renderIcon === null ? null : renderIcon === void 0 ? /* @__PURE__ */ jsx8(DefaultHeartIcon, { ...iconRenderProps }) : renderIcon(iconRenderProps);
|
|
595
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative inline-block", children: [
|
|
596
|
+
/* @__PURE__ */ jsx8("style", { children: dynamicStyles }),
|
|
597
|
+
/* @__PURE__ */ jsxs(
|
|
598
|
+
"button",
|
|
599
|
+
{
|
|
600
|
+
id: buttonId,
|
|
601
|
+
type: "button",
|
|
602
|
+
onClick: handleClick,
|
|
603
|
+
onContextMenu: handleRightClick,
|
|
604
|
+
disabled,
|
|
605
|
+
"aria-label": ariaLabel,
|
|
606
|
+
"aria-pressed": isPressed,
|
|
607
|
+
"aria-disabled": disabled,
|
|
608
|
+
style: { ...buttonStyle, cursor: cursorStyle },
|
|
609
|
+
className: `relative overflow-hidden z-10 border-solid flex items-center justify-center group transition-all focus:outline-none focus-visible:ring-4 focus-visible:ring-primary-dark focus-visible:ring-offset-2 ${className}`,
|
|
610
|
+
children: [
|
|
611
|
+
/* @__PURE__ */ jsxs(
|
|
612
|
+
"div",
|
|
613
|
+
{
|
|
614
|
+
className: "absolute bottom-0 left-0 right-0 z-0 transition-[height] duration-500 ease-out",
|
|
615
|
+
style: {
|
|
616
|
+
backgroundColor: fillColor,
|
|
617
|
+
height: isMaxed ? "100%" : `${clampedMinFill + fillPercentage / 100 * (MAX_FILL_HEIGHT - clampedMinFill)}%`
|
|
618
|
+
},
|
|
619
|
+
children: [
|
|
620
|
+
/* @__PURE__ */ jsx8(
|
|
621
|
+
"div",
|
|
622
|
+
{
|
|
623
|
+
className: "absolute bottom-full left-0 w-[200%] h-4 flex",
|
|
624
|
+
style: { animation: "wave-scroll-left 3s linear infinite" },
|
|
625
|
+
children: [0, 1].map((i) => /* @__PURE__ */ jsx8(
|
|
626
|
+
"svg",
|
|
627
|
+
{
|
|
628
|
+
className: "w-1/2 h-full fill-current",
|
|
629
|
+
style: { color: waveColor },
|
|
630
|
+
viewBox: "0 0 100 20",
|
|
631
|
+
preserveAspectRatio: "none",
|
|
632
|
+
"aria-hidden": "true",
|
|
633
|
+
children: /* @__PURE__ */ jsx8("path", { d: "M0,10 Q25,0 50,10 T100,10 V20 H0 Z" })
|
|
634
|
+
},
|
|
635
|
+
i
|
|
636
|
+
))
|
|
637
|
+
}
|
|
638
|
+
),
|
|
639
|
+
/* @__PURE__ */ jsx8(
|
|
640
|
+
"div",
|
|
641
|
+
{
|
|
642
|
+
className: "absolute bottom-full left-0 w-[200%] h-4 flex",
|
|
643
|
+
style: { animation: "wave-scroll-right 1.5s linear infinite" },
|
|
644
|
+
children: [0, 1].map((i) => /* @__PURE__ */ jsx8(
|
|
645
|
+
"svg",
|
|
646
|
+
{
|
|
647
|
+
className: "w-1/2 h-full fill-current",
|
|
648
|
+
style: { color: fillColor },
|
|
649
|
+
viewBox: "0 0 100 20",
|
|
650
|
+
preserveAspectRatio: "none",
|
|
651
|
+
"aria-hidden": "true",
|
|
652
|
+
children: /* @__PURE__ */ jsx8("path", { d: "M0,10 Q25,5 50,10 T100,10 V20 H0 Z" })
|
|
653
|
+
},
|
|
654
|
+
i
|
|
655
|
+
))
|
|
656
|
+
}
|
|
657
|
+
)
|
|
658
|
+
]
|
|
659
|
+
}
|
|
660
|
+
),
|
|
661
|
+
renderedIcon
|
|
662
|
+
]
|
|
663
|
+
}
|
|
664
|
+
),
|
|
665
|
+
showParticles && /* @__PURE__ */ jsx8(
|
|
666
|
+
"div",
|
|
667
|
+
{
|
|
668
|
+
className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none",
|
|
669
|
+
"aria-hidden": "true",
|
|
670
|
+
children: particles.map((p) => /* @__PURE__ */ jsx8(Particle, { ...p }, p.id))
|
|
671
|
+
}
|
|
672
|
+
)
|
|
673
|
+
] });
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/LikeButton/LikeButton.vanilla.tsx
|
|
677
|
+
import { useId as useId2, useMemo as useMemo2 } from "react";
|
|
678
|
+
|
|
679
|
+
// src/Particle/Particle.vanilla.tsx
|
|
680
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
681
|
+
function ParticleVanilla({
|
|
682
|
+
angle,
|
|
683
|
+
distance,
|
|
684
|
+
scale,
|
|
685
|
+
color,
|
|
686
|
+
shape,
|
|
687
|
+
speed,
|
|
688
|
+
easing,
|
|
689
|
+
fadeOut
|
|
690
|
+
}) {
|
|
691
|
+
const { transform, opacity } = useParticle({
|
|
692
|
+
angle,
|
|
693
|
+
distance,
|
|
694
|
+
scale,
|
|
695
|
+
speed,
|
|
696
|
+
easing,
|
|
697
|
+
fadeOut
|
|
698
|
+
});
|
|
699
|
+
const ShapeComponent = getParticleShape(shape);
|
|
700
|
+
return /* @__PURE__ */ jsx9(
|
|
701
|
+
"div",
|
|
702
|
+
{
|
|
703
|
+
className: "particle",
|
|
704
|
+
style: {
|
|
705
|
+
color,
|
|
706
|
+
transform,
|
|
707
|
+
opacity: fadeOut ? opacity : 1,
|
|
708
|
+
transitionDuration: `${speed}ms`,
|
|
709
|
+
transitionTimingFunction: easing
|
|
710
|
+
},
|
|
711
|
+
children: /* @__PURE__ */ jsx9(ShapeComponent, { size: 40, color, className: "particle__icon" })
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/LikeButton/LikeButton.vanilla.tsx
|
|
717
|
+
import { jsx as jsx10, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
718
|
+
var MAX_FILL_HEIGHT2 = 85;
|
|
719
|
+
function LikeButtonVanilla({
|
|
720
|
+
size = LIKE_BUTTON_DEFAULTS.size,
|
|
721
|
+
fillColor = LIKE_BUTTON_DEFAULTS.fillColor,
|
|
722
|
+
waveColor = LIKE_BUTTON_DEFAULTS.waveColor,
|
|
723
|
+
className = "",
|
|
724
|
+
showParticles = true,
|
|
725
|
+
renderIcon,
|
|
726
|
+
shape = "circle",
|
|
727
|
+
styles = {},
|
|
728
|
+
cursor = "heart",
|
|
729
|
+
minFillPercent = 0,
|
|
730
|
+
...hookOptions
|
|
731
|
+
}) {
|
|
732
|
+
const clampedMinFill = Math.max(0, Math.min(MAX_FILL_HEIGHT2, minFillPercent));
|
|
733
|
+
const reactId = useId2();
|
|
734
|
+
const buttonId = `like-button${reactId.replace(/:/g, "-")}`;
|
|
735
|
+
const {
|
|
736
|
+
handleClick,
|
|
737
|
+
handleRightClick,
|
|
738
|
+
disabled,
|
|
739
|
+
ariaLabel,
|
|
740
|
+
isPressed,
|
|
741
|
+
isMaxed,
|
|
742
|
+
fillPercentage,
|
|
743
|
+
particles
|
|
744
|
+
} = useLikeButton({ showParticles, ...hookOptions });
|
|
745
|
+
const mergedStyles = useMemo2(() => ({ ...DEFAULT_STYLES, ...styles }), [styles]);
|
|
746
|
+
const shapeStyles = useMemo2(() => getShapeStyles(shape), [shape]);
|
|
747
|
+
const buttonStyle = useMemo2(
|
|
748
|
+
() => computeButtonStyles(size, mergedStyles, shapeStyles),
|
|
749
|
+
[size, mergedStyles, shapeStyles]
|
|
750
|
+
);
|
|
751
|
+
const hoverShadowOffset = useMemo2(
|
|
752
|
+
() => computeHoverOffset(mergedStyles.shadowOffset),
|
|
753
|
+
[mergedStyles.shadowOffset]
|
|
754
|
+
);
|
|
755
|
+
const dynamicStyles = useMemo2(
|
|
756
|
+
() => generateDynamicStyles(`#${buttonId}`, hoverShadowOffset, mergedStyles),
|
|
757
|
+
[buttonId, hoverShadowOffset, mergedStyles]
|
|
758
|
+
);
|
|
759
|
+
const cursorStyle = useMemo2(
|
|
760
|
+
() => disabled ? "not-allowed" : getCursorStyle(cursor),
|
|
761
|
+
[cursor, disabled]
|
|
762
|
+
);
|
|
763
|
+
const iconSize = size * 0.5;
|
|
764
|
+
const iconRenderProps = {
|
|
765
|
+
size: iconSize,
|
|
766
|
+
className: "like-button__icon",
|
|
767
|
+
isMaxed,
|
|
768
|
+
fillPercentage
|
|
769
|
+
};
|
|
770
|
+
const renderedIcon = renderIcon === null ? null : renderIcon === void 0 ? /* @__PURE__ */ jsx10(DefaultHeartIcon, { ...iconRenderProps }) : renderIcon(iconRenderProps);
|
|
771
|
+
return /* @__PURE__ */ jsxs2("div", { className: "like-button-container", children: [
|
|
772
|
+
/* @__PURE__ */ jsx10("style", { children: dynamicStyles }),
|
|
773
|
+
/* @__PURE__ */ jsxs2(
|
|
774
|
+
"button",
|
|
775
|
+
{
|
|
776
|
+
id: buttonId,
|
|
777
|
+
type: "button",
|
|
778
|
+
onClick: handleClick,
|
|
779
|
+
onContextMenu: handleRightClick,
|
|
780
|
+
disabled,
|
|
781
|
+
"aria-label": ariaLabel,
|
|
782
|
+
"aria-pressed": isPressed,
|
|
783
|
+
"aria-disabled": disabled,
|
|
784
|
+
style: { ...buttonStyle, cursor: cursorStyle },
|
|
785
|
+
className: `like-button ${className}`.trim(),
|
|
786
|
+
children: [
|
|
787
|
+
/* @__PURE__ */ jsxs2(
|
|
788
|
+
"div",
|
|
789
|
+
{
|
|
790
|
+
className: "like-button__fill",
|
|
791
|
+
style: {
|
|
792
|
+
backgroundColor: fillColor,
|
|
793
|
+
height: isMaxed ? "100%" : `${clampedMinFill + fillPercentage / 100 * (MAX_FILL_HEIGHT2 - clampedMinFill)}%`
|
|
794
|
+
},
|
|
795
|
+
children: [
|
|
796
|
+
/* @__PURE__ */ jsx10("div", { className: "like-button__wave like-button__wave--back", children: [0, 1].map((i) => /* @__PURE__ */ jsx10(
|
|
797
|
+
"svg",
|
|
798
|
+
{
|
|
799
|
+
className: "like-button__wave-svg",
|
|
800
|
+
style: { color: waveColor },
|
|
801
|
+
viewBox: "0 0 100 20",
|
|
802
|
+
preserveAspectRatio: "none",
|
|
803
|
+
"aria-hidden": "true",
|
|
804
|
+
children: /* @__PURE__ */ jsx10("path", { d: "M0,10 Q25,0 50,10 T100,10 V20 H0 Z" })
|
|
805
|
+
},
|
|
806
|
+
i
|
|
807
|
+
)) }),
|
|
808
|
+
/* @__PURE__ */ jsx10("div", { className: "like-button__wave like-button__wave--front", children: [0, 1].map((i) => /* @__PURE__ */ jsx10(
|
|
809
|
+
"svg",
|
|
810
|
+
{
|
|
811
|
+
className: "like-button__wave-svg",
|
|
812
|
+
style: { color: fillColor },
|
|
813
|
+
viewBox: "0 0 100 20",
|
|
814
|
+
preserveAspectRatio: "none",
|
|
815
|
+
"aria-hidden": "true",
|
|
816
|
+
children: /* @__PURE__ */ jsx10("path", { d: "M0,10 Q25,5 50,10 T100,10 V20 H0 Z" })
|
|
817
|
+
},
|
|
818
|
+
i
|
|
819
|
+
)) })
|
|
820
|
+
]
|
|
821
|
+
}
|
|
822
|
+
),
|
|
823
|
+
renderedIcon
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
),
|
|
827
|
+
showParticles && /* @__PURE__ */ jsx10("div", { className: "like-button__particles", "aria-hidden": "true", children: particles.map((p) => /* @__PURE__ */ jsx10(ParticleVanilla, { ...p }, p.id)) })
|
|
828
|
+
] });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export {
|
|
832
|
+
DefaultHeartIcon,
|
|
833
|
+
useParticle,
|
|
834
|
+
Particle,
|
|
835
|
+
LIKE_BUTTON_DEFAULTS,
|
|
836
|
+
useLikeButton,
|
|
837
|
+
LikeButton,
|
|
838
|
+
ParticleVanilla,
|
|
839
|
+
LikeButtonVanilla
|
|
840
|
+
};
|
|
841
|
+
//# sourceMappingURL=chunk-WTGBDIZW.js.map
|