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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="https://i.ibb.co/k2MRQCqT/logo-nbg.png" alt="react-raffle-picker logo placeholder" width="160" />
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
- <RafflePick min={1} max={100} interval={100} inertia onSelect={(v) => console.log(v)}>
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 *is* required to make the column animate. Two ways to load it:
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 | 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. |
108
- | `finalValue` | `number \| string` | — | Forces settle to land on this value. Cycle still appears random; only final freeze is rigged. |
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. |
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;height:1em;line-height:1em;vertical-align:baseline}
429
- .rrp-slot__col{display:flex;flex-direction:column;transform:translateY(-1em);animation:rrp-slot-reel var(--rrp-tick,80ms) linear infinite}
430
- .rrp-slot__cell{display:block;height:1em;line-height:1em}
431
- .rrp-slot[data-stopped] .rrp-slot__col{animation:none;transform:translateY(-1em)}
432
- @keyframes rrp-slot-reel{from{transform:translateY(-1em)}to{transform:translateY(-2em)}}
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
- let slotStylesInjected = false;
438
+ const SLOT_STYLES_VERSION = "4";
435
439
  const injectSlotStyles = () => {
436
- if (slotStylesInjected) return;
437
440
  if (typeof document === "undefined") return;
438
- if (document.querySelector("style[data-rrp-slot-base]")) {
439
- slotStylesInjected = true;
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 c = pool[0] ?? "";
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.removeAttribute("data-stopped");
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;height:1em;line-height:1em;vertical-align:baseline}
428
- .rrp-slot__col{display:flex;flex-direction:column;transform:translateY(-1em);animation:rrp-slot-reel var(--rrp-tick,80ms) linear infinite}
429
- .rrp-slot__cell{display:block;height:1em;line-height:1em}
430
- .rrp-slot[data-stopped] .rrp-slot__col{animation:none;transform:translateY(-1em)}
431
- @keyframes rrp-slot-reel{from{transform:translateY(-1em)}to{transform:translateY(-2em)}}
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
- let slotStylesInjected = false;
437
+ const SLOT_STYLES_VERSION = "4";
434
438
  const injectSlotStyles = () => {
435
- if (slotStylesInjected) return;
436
439
  if (typeof document === "undefined") return;
437
- if (document.querySelector("style[data-rrp-slot-base]")) {
438
- slotStylesInjected = true;
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 c = pool[0] ?? "";
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.removeAttribute("data-stopped");
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
- display: flex;
14
- flex-direction: column;
15
- transform: translateY(-1em);
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
- display: block;
20
- height: 1em;
21
- line-height: 1em;
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: translateY(-1em);
32
+ transform: translate3d(0, -33.3333%, 0);
26
33
  }
27
34
  @keyframes rrp-slot-reel {
28
- from { transform: translateY(-1em); }
29
- to { transform: translateY(-2em); }
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.0",
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
  }