react-raffle-picker 0.2.0 → 0.2.2
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 +20 -18
- package/dist/index.cjs +36 -15
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +36 -15
- package/dist/styles.css +18 -11
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="https://i.ibb.co/
|
|
2
|
+
<img src="https://i.ibb.co/yB50hJTv/logo-nbg.png" alt="react-raffle-picker logo placeholder" width="160" />
|
|
3
3
|
|
|
4
4
|
<h1>react-raffle-picker</h1>
|
|
5
5
|
|
|
@@ -69,7 +69,7 @@ This means:
|
|
|
69
69
|
```tsx
|
|
70
70
|
import { RafflePick } from 'react-raffle-picker'
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
;<RafflePick min={1} max={100} interval={100} inertia onSelect={(v) => console.log(v)}>
|
|
73
73
|
<RafflePick.Value animation="roll" className="my-value" />
|
|
74
74
|
<RafflePick.Button startLabel="Pick" stopLabel="Stop" />
|
|
75
75
|
</RafflePick>
|
|
@@ -79,7 +79,7 @@ import { RafflePick } from 'react-raffle-picker'
|
|
|
79
79
|
|
|
80
80
|
The library is headless — no global stylesheet is required for `Value`, `Button`, or `Countdown`. You bring your own CSS.
|
|
81
81
|
|
|
82
|
-
For the **slot reel** (`<RafflePick.Slots>`), a minimal stylesheet
|
|
82
|
+
For the **slot reel** (`<RafflePick.Slots>`), a minimal stylesheet _is_ required to make the column animate. Two ways to load it:
|
|
83
83
|
|
|
84
84
|
1. **Auto-injected at runtime** — `<RafflePick.Slots>` injects a `<style data-rrp-slot-base>` tag into `document.head` on first mount. No action needed in CSR apps.
|
|
85
85
|
2. **Static import** (recommended for SSR / strict CSP / full control):
|
|
@@ -96,21 +96,21 @@ For the **slot reel** (`<RafflePick.Slots>`), a minimal stylesheet *is* required
|
|
|
96
96
|
|
|
97
97
|
Provides context. Renders an optional wrapper element (`as` prop, default `'div'`).
|
|
98
98
|
|
|
99
|
-
| Prop
|
|
100
|
-
|
|
|
101
|
-
| `min`, `max`
|
|
102
|
-
| `items`
|
|
103
|
-
| `interval`
|
|
104
|
-
| `random`
|
|
105
|
-
| `inertia`
|
|
106
|
-
| `autoStart`
|
|
107
|
-
| `initialValue` | `number \| string`
|
|
108
|
-
| `finalValue` | `number \| string`
|
|
109
|
-
| `onSelect`
|
|
110
|
-
| `as`
|
|
111
|
-
| `className`
|
|
112
|
-
| `style`
|
|
113
|
-
| `children`
|
|
99
|
+
| Prop | Type | Default | Notes |
|
|
100
|
+
| -------------- | --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
101
|
+
| `min`, `max` | `number` | `1`, `100` | Numeric range. Ignored if `items` provided. |
|
|
102
|
+
| `items` | `string[]` | — | Switches to item mode (cycles through names). |
|
|
103
|
+
| `interval` | `number` (ms, clamped ≥ 50) | `100` | Tick speed. |
|
|
104
|
+
| `random` | `boolean` | `true` | Random pick vs sequential. |
|
|
105
|
+
| `inertia` | `boolean` | `false` | Soft start / soft stop ramp. |
|
|
106
|
+
| `autoStart` | `boolean` | `true` | Begin cycling on mount. |
|
|
107
|
+
| `initialValue` | `number \| string` | — | Starting display before first run. Number for `min`/`max` mode, string for `items` mode. For `<Slots>`, each character seeds the corresponding reel. |
|
|
108
|
+
| `finalValue` | `number \| string` | — | Forces settle to land on this value. Cycle still appears random; only final freeze is rigged. For `<Slots>`, each character is the final char of the corresponding reel. |
|
|
109
|
+
| `onSelect` | `(value) => void` | — | Fires once per round on freeze. |
|
|
110
|
+
| `as` | `ElementType` | `'div'` | Wrapper tag. |
|
|
111
|
+
| `className` | `string` | — | Wrapper class. |
|
|
112
|
+
| `style` | `CSSProperties` | — | Wrapper style. |
|
|
113
|
+
| `children` | `ReactNode` | — | Sub-components. |
|
|
114
114
|
|
|
115
115
|
### `<RafflePick.Value>`
|
|
116
116
|
|
|
@@ -153,6 +153,8 @@ Schedules auto-freeze after `seconds`. Renders an SVG ring + numeric label by de
|
|
|
153
153
|
|
|
154
154
|
Independent multi-reel slot machine. Each reel ticks on its own and stops with a stagger.
|
|
155
155
|
|
|
156
|
+
`initialValue` (root prop) seeds reels char-by-char before the first run. `finalValue` rigs the freeze so each reel lands on the corresponding character — useful for predetermined winners or scripted demos.
|
|
157
|
+
|
|
156
158
|
| Prop | Type | Default | Notes |
|
|
157
159
|
| -------------------------------------------------- | -------------------------- | -------------- | -------------------------------------------- |
|
|
158
160
|
| `length` | `number` | `3` | Number of reels. |
|
package/dist/index.cjs
CHANGED
|
@@ -258,6 +258,8 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
|
|
|
258
258
|
inertia,
|
|
259
259
|
hasItems,
|
|
260
260
|
initialIndex,
|
|
261
|
+
initialValue,
|
|
262
|
+
finalValue,
|
|
261
263
|
valueRef,
|
|
262
264
|
displayValue,
|
|
263
265
|
subscribe,
|
|
@@ -272,6 +274,8 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
|
|
|
272
274
|
inertia,
|
|
273
275
|
hasItems,
|
|
274
276
|
initialIndex,
|
|
277
|
+
initialValue,
|
|
278
|
+
finalValue,
|
|
275
279
|
displayValue,
|
|
276
280
|
subscribe,
|
|
277
281
|
start,
|
|
@@ -425,25 +429,26 @@ function CountdownRunning({ seconds, className, style, children }) {
|
|
|
425
429
|
//#region src/components/RafflePick/RafflePickSlots.tsx
|
|
426
430
|
const pickRandom = (pool) => pool[Math.floor(Math.random() * pool.length)] ?? "";
|
|
427
431
|
const SLOT_BASE_CSS = `
|
|
428
|
-
.rrp-slot{display:inline-block;overflow:hidden;
|
|
429
|
-
.rrp-slot__col{
|
|
430
|
-
.rrp-slot__cell{display:
|
|
431
|
-
.rrp-slot[data-stopped] .rrp-slot__col{animation:none;transform:
|
|
432
|
-
@keyframes rrp-slot-reel{from{transform:
|
|
432
|
+
.rrp-slot{display:inline-block;position:relative;overflow:hidden;vertical-align:baseline}
|
|
433
|
+
.rrp-slot__col{position:absolute;top:0;left:0;right:0;height:300%;transform:translate3d(0,-33.3333%,0);animation:rrp-slot-reel var(--rrp-tick,80ms) linear infinite;will-change:transform}
|
|
434
|
+
.rrp-slot__cell{height:33.3333%;display:grid;place-items:center;padding:0;margin:0;box-sizing:border-box;text-align:center}
|
|
435
|
+
.rrp-slot[data-stopped] .rrp-slot__col{animation:none;transform:translate3d(0,-33.3333%,0)}
|
|
436
|
+
@keyframes rrp-slot-reel{from{transform:translate3d(0,-33.3333%,0)}to{transform:translate3d(0,-66.6666%,0)}}
|
|
433
437
|
`;
|
|
434
|
-
|
|
438
|
+
const SLOT_STYLES_VERSION = "4";
|
|
435
439
|
const injectSlotStyles = () => {
|
|
436
|
-
if (slotStylesInjected) return;
|
|
437
440
|
if (typeof document === "undefined") return;
|
|
438
|
-
|
|
439
|
-
|
|
441
|
+
const existing = document.querySelector("style[data-rrp-slot-base]");
|
|
442
|
+
if (existing) {
|
|
443
|
+
if (existing.getAttribute("data-rrp-slot-base") === SLOT_STYLES_VERSION) return;
|
|
444
|
+
existing.textContent = SLOT_BASE_CSS;
|
|
445
|
+
existing.setAttribute("data-rrp-slot-base", SLOT_STYLES_VERSION);
|
|
440
446
|
return;
|
|
441
447
|
}
|
|
442
448
|
const tag = document.createElement("style");
|
|
443
|
-
tag.setAttribute("data-rrp-slot-base",
|
|
449
|
+
tag.setAttribute("data-rrp-slot-base", SLOT_STYLES_VERSION);
|
|
444
450
|
tag.textContent = SLOT_BASE_CSS;
|
|
445
451
|
document.head.appendChild(tag);
|
|
446
|
-
slotStylesInjected = true;
|
|
447
452
|
};
|
|
448
453
|
const makeRefs = () => ({
|
|
449
454
|
root: null,
|
|
@@ -456,7 +461,19 @@ const makeRefs = () => ({
|
|
|
456
461
|
});
|
|
457
462
|
function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80, staggerMs = 220, className, slotClassName, style, slotStyle, onResult }) {
|
|
458
463
|
injectSlotStyles();
|
|
459
|
-
const { phase } = useRaffleContext("RafflePick.Slots");
|
|
464
|
+
const { phase, initialValue, finalValue } = useRaffleContext("RafflePick.Slots");
|
|
465
|
+
const initialChars = (0, react.useMemo)(() => {
|
|
466
|
+
const seed = typeof initialValue === "string" ? initialValue : "";
|
|
467
|
+
return Array.from({ length }, (_, i) => seed[i] ?? "");
|
|
468
|
+
}, [initialValue, length]);
|
|
469
|
+
const finalChars = (0, react.useMemo)(() => {
|
|
470
|
+
if (typeof finalValue !== "string") return null;
|
|
471
|
+
return Array.from({ length }, (_, i) => finalValue[i] ?? "");
|
|
472
|
+
}, [finalValue, length]);
|
|
473
|
+
const finalCharsRef = (0, react.useRef)(finalChars);
|
|
474
|
+
(0, react.useEffect)(() => {
|
|
475
|
+
finalCharsRef.current = finalChars;
|
|
476
|
+
}, [finalChars]);
|
|
460
477
|
const slotsRef = (0, react.useRef)([]);
|
|
461
478
|
const stopTimersRef = (0, react.useRef)([]);
|
|
462
479
|
const finalRef = (0, react.useRef)([]);
|
|
@@ -514,6 +531,8 @@ function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80,
|
|
|
514
531
|
for (let i = 0; i < length; i++) {
|
|
515
532
|
const id = setTimeout(() => {
|
|
516
533
|
const s = slotsRef.current[i];
|
|
534
|
+
const forced = finalCharsRef.current?.[i];
|
|
535
|
+
if (forced) writeSlot(s, s.currChar, forced, forced);
|
|
517
536
|
s.stopped = true;
|
|
518
537
|
if (s.root) s.root.setAttribute("data-stopped", "");
|
|
519
538
|
finalRef.current[i] = s.currChar;
|
|
@@ -529,17 +548,19 @@ function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80,
|
|
|
529
548
|
]);
|
|
530
549
|
(0, react.useEffect)(() => {
|
|
531
550
|
if (phase !== "idle") return;
|
|
532
|
-
const
|
|
551
|
+
const fallback = pool[0] ?? "";
|
|
533
552
|
for (let i = 0; i < length; i++) {
|
|
534
553
|
const s = slotsRef.current[i];
|
|
554
|
+
const c = initialChars[i] || fallback;
|
|
535
555
|
s.stopped = true;
|
|
536
556
|
writeSlot(s, c, c, c);
|
|
537
|
-
if (s.root) s.root.
|
|
557
|
+
if (s.root) s.root.setAttribute("data-stopped", "");
|
|
538
558
|
}
|
|
539
559
|
}, [
|
|
540
560
|
phase,
|
|
541
561
|
length,
|
|
542
|
-
pool
|
|
562
|
+
pool,
|
|
563
|
+
initialChars
|
|
543
564
|
]);
|
|
544
565
|
const cls = (0, react.useMemo)(() => joinClassNames("rrp-slots", className), [className]);
|
|
545
566
|
const slotCls = (0, react.useMemo)(() => joinClassNames("rrp-slot", slotClassName), [slotClassName]);
|
package/dist/index.d.cts
CHANGED
|
@@ -135,6 +135,8 @@ interface RaffleContextValue {
|
|
|
135
135
|
inertia: boolean;
|
|
136
136
|
hasItems: boolean;
|
|
137
137
|
initialIndex: number;
|
|
138
|
+
initialValue?: RafflePickValue$1;
|
|
139
|
+
finalValue?: RafflePickValue$1;
|
|
138
140
|
valueRef: RefObject<number>;
|
|
139
141
|
displayValue: (index: number) => RafflePickValue$1;
|
|
140
142
|
subscribe: (fn: (value: number) => void) => () => void;
|
package/dist/index.d.mts
CHANGED
|
@@ -135,6 +135,8 @@ interface RaffleContextValue {
|
|
|
135
135
|
inertia: boolean;
|
|
136
136
|
hasItems: boolean;
|
|
137
137
|
initialIndex: number;
|
|
138
|
+
initialValue?: RafflePickValue$1;
|
|
139
|
+
finalValue?: RafflePickValue$1;
|
|
138
140
|
valueRef: RefObject<number>;
|
|
139
141
|
displayValue: (index: number) => RafflePickValue$1;
|
|
140
142
|
subscribe: (fn: (value: number) => void) => () => void;
|
package/dist/index.mjs
CHANGED
|
@@ -257,6 +257,8 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
|
|
|
257
257
|
inertia,
|
|
258
258
|
hasItems,
|
|
259
259
|
initialIndex,
|
|
260
|
+
initialValue,
|
|
261
|
+
finalValue,
|
|
260
262
|
valueRef,
|
|
261
263
|
displayValue,
|
|
262
264
|
subscribe,
|
|
@@ -271,6 +273,8 @@ function RafflePickRoot({ items, min = 1, max = 100, interval = 100, random = tr
|
|
|
271
273
|
inertia,
|
|
272
274
|
hasItems,
|
|
273
275
|
initialIndex,
|
|
276
|
+
initialValue,
|
|
277
|
+
finalValue,
|
|
274
278
|
displayValue,
|
|
275
279
|
subscribe,
|
|
276
280
|
start,
|
|
@@ -424,25 +428,26 @@ function CountdownRunning({ seconds, className, style, children }) {
|
|
|
424
428
|
//#region src/components/RafflePick/RafflePickSlots.tsx
|
|
425
429
|
const pickRandom = (pool) => pool[Math.floor(Math.random() * pool.length)] ?? "";
|
|
426
430
|
const SLOT_BASE_CSS = `
|
|
427
|
-
.rrp-slot{display:inline-block;overflow:hidden;
|
|
428
|
-
.rrp-slot__col{
|
|
429
|
-
.rrp-slot__cell{display:
|
|
430
|
-
.rrp-slot[data-stopped] .rrp-slot__col{animation:none;transform:
|
|
431
|
-
@keyframes rrp-slot-reel{from{transform:
|
|
431
|
+
.rrp-slot{display:inline-block;position:relative;overflow:hidden;vertical-align:baseline}
|
|
432
|
+
.rrp-slot__col{position:absolute;top:0;left:0;right:0;height:300%;transform:translate3d(0,-33.3333%,0);animation:rrp-slot-reel var(--rrp-tick,80ms) linear infinite;will-change:transform}
|
|
433
|
+
.rrp-slot__cell{height:33.3333%;display:grid;place-items:center;padding:0;margin:0;box-sizing:border-box;text-align:center}
|
|
434
|
+
.rrp-slot[data-stopped] .rrp-slot__col{animation:none;transform:translate3d(0,-33.3333%,0)}
|
|
435
|
+
@keyframes rrp-slot-reel{from{transform:translate3d(0,-33.3333%,0)}to{transform:translate3d(0,-66.6666%,0)}}
|
|
432
436
|
`;
|
|
433
|
-
|
|
437
|
+
const SLOT_STYLES_VERSION = "4";
|
|
434
438
|
const injectSlotStyles = () => {
|
|
435
|
-
if (slotStylesInjected) return;
|
|
436
439
|
if (typeof document === "undefined") return;
|
|
437
|
-
|
|
438
|
-
|
|
440
|
+
const existing = document.querySelector("style[data-rrp-slot-base]");
|
|
441
|
+
if (existing) {
|
|
442
|
+
if (existing.getAttribute("data-rrp-slot-base") === SLOT_STYLES_VERSION) return;
|
|
443
|
+
existing.textContent = SLOT_BASE_CSS;
|
|
444
|
+
existing.setAttribute("data-rrp-slot-base", SLOT_STYLES_VERSION);
|
|
439
445
|
return;
|
|
440
446
|
}
|
|
441
447
|
const tag = document.createElement("style");
|
|
442
|
-
tag.setAttribute("data-rrp-slot-base",
|
|
448
|
+
tag.setAttribute("data-rrp-slot-base", SLOT_STYLES_VERSION);
|
|
443
449
|
tag.textContent = SLOT_BASE_CSS;
|
|
444
450
|
document.head.appendChild(tag);
|
|
445
|
-
slotStylesInjected = true;
|
|
446
451
|
};
|
|
447
452
|
const makeRefs = () => ({
|
|
448
453
|
root: null,
|
|
@@ -455,7 +460,19 @@ const makeRefs = () => ({
|
|
|
455
460
|
});
|
|
456
461
|
function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80, staggerMs = 220, className, slotClassName, style, slotStyle, onResult }) {
|
|
457
462
|
injectSlotStyles();
|
|
458
|
-
const { phase } = useRaffleContext("RafflePick.Slots");
|
|
463
|
+
const { phase, initialValue, finalValue } = useRaffleContext("RafflePick.Slots");
|
|
464
|
+
const initialChars = useMemo(() => {
|
|
465
|
+
const seed = typeof initialValue === "string" ? initialValue : "";
|
|
466
|
+
return Array.from({ length }, (_, i) => seed[i] ?? "");
|
|
467
|
+
}, [initialValue, length]);
|
|
468
|
+
const finalChars = useMemo(() => {
|
|
469
|
+
if (typeof finalValue !== "string") return null;
|
|
470
|
+
return Array.from({ length }, (_, i) => finalValue[i] ?? "");
|
|
471
|
+
}, [finalValue, length]);
|
|
472
|
+
const finalCharsRef = useRef(finalChars);
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
finalCharsRef.current = finalChars;
|
|
475
|
+
}, [finalChars]);
|
|
459
476
|
const slotsRef = useRef([]);
|
|
460
477
|
const stopTimersRef = useRef([]);
|
|
461
478
|
const finalRef = useRef([]);
|
|
@@ -513,6 +530,8 @@ function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80,
|
|
|
513
530
|
for (let i = 0; i < length; i++) {
|
|
514
531
|
const id = setTimeout(() => {
|
|
515
532
|
const s = slotsRef.current[i];
|
|
533
|
+
const forced = finalCharsRef.current?.[i];
|
|
534
|
+
if (forced) writeSlot(s, s.currChar, forced, forced);
|
|
516
535
|
s.stopped = true;
|
|
517
536
|
if (s.root) s.root.setAttribute("data-stopped", "");
|
|
518
537
|
finalRef.current[i] = s.currChar;
|
|
@@ -528,17 +547,19 @@ function RafflePickSlots({ length = 3, chars = "0123456789", spinInterval = 80,
|
|
|
528
547
|
]);
|
|
529
548
|
useEffect(() => {
|
|
530
549
|
if (phase !== "idle") return;
|
|
531
|
-
const
|
|
550
|
+
const fallback = pool[0] ?? "";
|
|
532
551
|
for (let i = 0; i < length; i++) {
|
|
533
552
|
const s = slotsRef.current[i];
|
|
553
|
+
const c = initialChars[i] || fallback;
|
|
534
554
|
s.stopped = true;
|
|
535
555
|
writeSlot(s, c, c, c);
|
|
536
|
-
if (s.root) s.root.
|
|
556
|
+
if (s.root) s.root.setAttribute("data-stopped", "");
|
|
537
557
|
}
|
|
538
558
|
}, [
|
|
539
559
|
phase,
|
|
540
560
|
length,
|
|
541
|
-
pool
|
|
561
|
+
pool,
|
|
562
|
+
initialChars
|
|
542
563
|
]);
|
|
543
564
|
const cls = useMemo(() => joinClassNames("rrp-slots", className), [className]);
|
|
544
565
|
const slotCls = useMemo(() => joinClassNames("rrp-slot", slotClassName), [slotClassName]);
|
package/dist/styles.css
CHANGED
|
@@ -4,29 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
.rrp-slot {
|
|
6
6
|
display: inline-block;
|
|
7
|
+
position: relative;
|
|
7
8
|
overflow: hidden;
|
|
8
|
-
height: 1em;
|
|
9
|
-
line-height: 1em;
|
|
10
9
|
vertical-align: baseline;
|
|
11
10
|
}
|
|
12
11
|
.rrp-slot__col {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 0;
|
|
14
|
+
left: 0;
|
|
15
|
+
right: 0;
|
|
16
|
+
height: 300%;
|
|
17
|
+
transform: translate3d(0, -33.3333%, 0);
|
|
16
18
|
animation: rrp-slot-reel var(--rrp-tick, 80ms) linear infinite;
|
|
19
|
+
will-change: transform;
|
|
17
20
|
}
|
|
18
21
|
.rrp-slot__cell {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
height: 33.3333%;
|
|
23
|
+
display: grid;
|
|
24
|
+
place-items: center;
|
|
25
|
+
padding: 0;
|
|
26
|
+
margin: 0;
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
text-align: center;
|
|
22
29
|
}
|
|
23
30
|
.rrp-slot[data-stopped] .rrp-slot__col {
|
|
24
31
|
animation: none;
|
|
25
|
-
transform:
|
|
32
|
+
transform: translate3d(0, -33.3333%, 0);
|
|
26
33
|
}
|
|
27
34
|
@keyframes rrp-slot-reel {
|
|
28
|
-
from { transform:
|
|
29
|
-
to { transform:
|
|
35
|
+
from { transform: translate3d(0, -33.3333%, 0); }
|
|
36
|
+
to { transform: translate3d(0, -66.6666%, 0); }
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
/* Value animations: opt-in via <RafflePick.Value animation="..." /> */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-raffle-picker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Headless React raffle picker for giveaways, winner draws, countdowns, and slot-machine UIs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -111,5 +111,8 @@
|
|
|
111
111
|
"typescript-eslint": "^8.58.2",
|
|
112
112
|
"vite": "^8.0.8",
|
|
113
113
|
"vitest": "^4.1.4"
|
|
114
|
+
},
|
|
115
|
+
"volta": {
|
|
116
|
+
"node": "22.12.0"
|
|
114
117
|
}
|
|
115
118
|
}
|