@digilogiclabs/saas-factory-ui 2.4.0 → 2.6.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/dist/index.d.mts +49 -10
- package/dist/index.d.ts +49 -10
- package/dist/index.js +194 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +194 -27
- package/dist/index.mjs.map +1 -1
- package/dist/web/index.d.mts +49 -10
- package/dist/web/index.d.ts +49 -10
- package/dist/web/index.js +194 -27
- package/dist/web/index.js.map +1 -1
- package/dist/web/index.mjs +194 -27
- package/dist/web/index.mjs.map +1 -1
- package/package.json +17 -17
package/dist/web/index.d.mts
CHANGED
|
@@ -5551,8 +5551,14 @@ interface RandomPlayerPickerProps {
|
|
|
5551
5551
|
darkMode?: boolean;
|
|
5552
5552
|
/** Wheel diameter in px. Default 380. */
|
|
5553
5553
|
size?: number;
|
|
5554
|
-
/**
|
|
5555
|
-
|
|
5554
|
+
/**
|
|
5555
|
+
* Called when a winner is locked in. `info.lastStanding` is true for
|
|
5556
|
+
* the automatic final pick of a draft — the survivor promoted when
|
|
5557
|
+
* pops drain the pool to one.
|
|
5558
|
+
*/
|
|
5559
|
+
onPick?: (winner: RandomPlayerParticipant, history: RandomPlayerParticipant[], info?: {
|
|
5560
|
+
lastStanding: boolean;
|
|
5561
|
+
}) => void;
|
|
5556
5562
|
/** Fired the moment a spin begins (before the animation). */
|
|
5557
5563
|
onSpinStart?: () => void;
|
|
5558
5564
|
/**
|
|
@@ -5586,14 +5592,37 @@ interface RandomPlayerPickerProps {
|
|
|
5586
5592
|
renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
|
|
5587
5593
|
/** Imperative handle. `spinRef.current?.()` triggers a spin. */
|
|
5588
5594
|
spinRef?: MutableRefObject<(() => void) | null>;
|
|
5595
|
+
/**
|
|
5596
|
+
* Imperative reset handle. `resetRef.current?.()` restores the pool to
|
|
5597
|
+
* the full `participants` list and clears history/winner — the escape
|
|
5598
|
+
* hatch for consumers that hide the built-in controls
|
|
5599
|
+
* (`showControls={false}`) but still need a "reset draft" affordance
|
|
5600
|
+
* once draft-mode pops drain the pool.
|
|
5601
|
+
*/
|
|
5602
|
+
resetRef?: MutableRefObject<(() => void) | null>;
|
|
5603
|
+
/**
|
|
5604
|
+
* Fired whenever the internal pool changes (mount, structural sync,
|
|
5605
|
+
* draft-mode pop, spin-again flush, reset). Draft-mode consumers use
|
|
5606
|
+
* this to mirror the live pool — e.g. to disable an external spin
|
|
5607
|
+
* trigger when the pool drains below 2 instead of presenting a dead
|
|
5608
|
+
* button, and to offer a reset once the draft completes.
|
|
5609
|
+
*/
|
|
5610
|
+
onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
|
|
5611
|
+
/**
|
|
5612
|
+
* Caption shown in place of `placeholderCaption` when draft-mode pops
|
|
5613
|
+
* drain the pool below 2 while the roster itself still has 2+ members.
|
|
5614
|
+
* Default: "Draft complete — reset to spin again".
|
|
5615
|
+
*/
|
|
5616
|
+
draftCompleteCaption?: string;
|
|
5589
5617
|
/**
|
|
5590
5618
|
* Visual intensity preset. Default "normal".
|
|
5591
|
-
* - "subtle" / "normal":
|
|
5592
|
-
*
|
|
5593
|
-
*
|
|
5594
|
-
*
|
|
5595
|
-
*
|
|
5596
|
-
* particles/glow/shake on reveal.
|
|
5619
|
+
* - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
|
|
5620
|
+
* - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
|
|
5621
|
+
* exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
|
|
5622
|
+
* visibly winding down through the FULL duration and lands exactly
|
|
5623
|
+
* at the end, +12 extra revolutions, ticks fired on actual
|
|
5624
|
+
* wedge/row crossings, plus more particles/glow/shake on reveal.
|
|
5625
|
+
* Falls back to a 250ms CSS settle under prefers-reduced-motion.
|
|
5597
5626
|
*/
|
|
5598
5627
|
intensity?: RandomPlayerIntensity;
|
|
5599
5628
|
/**
|
|
@@ -5636,7 +5665,7 @@ interface RandomPlayerPickerProps {
|
|
|
5636
5665
|
/** Accessible label for the wheel root. */
|
|
5637
5666
|
ariaLabel?: string;
|
|
5638
5667
|
}
|
|
5639
|
-
declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
|
|
5668
|
+
declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, resetRef, onPoolChange, draftCompleteCaption, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
|
|
5640
5669
|
|
|
5641
5670
|
type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
|
|
5642
5671
|
type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
|
|
@@ -6202,10 +6231,20 @@ interface TeamGeneratorProps {
|
|
|
6202
6231
|
maxPlayers?: number;
|
|
6203
6232
|
/** Initial roster. Uncontrolled — the component manages names internally. */
|
|
6204
6233
|
defaultNames?: string[];
|
|
6234
|
+
/** Initial generated teams (e.g. restored from a saved session).
|
|
6235
|
+
* Uncontrolled — the component takes over after mount. An empty array
|
|
6236
|
+
* is treated as "no teams". */
|
|
6237
|
+
defaultTeams?: string[][];
|
|
6205
6238
|
/** Called whenever the name pool changes. */
|
|
6206
6239
|
onNamesChange?: (names: string[]) => void;
|
|
6207
6240
|
/** Called whenever teams are (re)generated. */
|
|
6208
6241
|
onGenerate?: (teams: string[][]) => void;
|
|
6242
|
+
/** Called whenever the generated teams change for ANY reason — a
|
|
6243
|
+
* (re)generation, or invalidation to `null` when a player is removed,
|
|
6244
|
+
* the pool is cleared, or the team count changes. Unlike `onGenerate`,
|
|
6245
|
+
* this mirrors the full lifecycle of the on-screen split so hosts can
|
|
6246
|
+
* persist it and restore via `defaultTeams`. */
|
|
6247
|
+
onTeamsChange?: (teams: string[][] | null) => void;
|
|
6209
6248
|
/** Called whenever the team count selection changes. */
|
|
6210
6249
|
onTeamCountChange?: (count: number) => void;
|
|
6211
6250
|
/** Localized / customized labels. */
|
|
@@ -6218,7 +6257,7 @@ interface TeamGeneratorProps {
|
|
|
6218
6257
|
* CSV / TSV roster from a spreadsheet. Default `true`. */
|
|
6219
6258
|
enableImport?: boolean;
|
|
6220
6259
|
}
|
|
6221
|
-
declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
|
|
6260
|
+
declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, defaultTeams, onNamesChange, onGenerate, onTeamsChange, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
|
|
6222
6261
|
declare namespace TeamGenerator {
|
|
6223
6262
|
var displayName: string;
|
|
6224
6263
|
}
|
package/dist/web/index.d.ts
CHANGED
|
@@ -5551,8 +5551,14 @@ interface RandomPlayerPickerProps {
|
|
|
5551
5551
|
darkMode?: boolean;
|
|
5552
5552
|
/** Wheel diameter in px. Default 380. */
|
|
5553
5553
|
size?: number;
|
|
5554
|
-
/**
|
|
5555
|
-
|
|
5554
|
+
/**
|
|
5555
|
+
* Called when a winner is locked in. `info.lastStanding` is true for
|
|
5556
|
+
* the automatic final pick of a draft — the survivor promoted when
|
|
5557
|
+
* pops drain the pool to one.
|
|
5558
|
+
*/
|
|
5559
|
+
onPick?: (winner: RandomPlayerParticipant, history: RandomPlayerParticipant[], info?: {
|
|
5560
|
+
lastStanding: boolean;
|
|
5561
|
+
}) => void;
|
|
5556
5562
|
/** Fired the moment a spin begins (before the animation). */
|
|
5557
5563
|
onSpinStart?: () => void;
|
|
5558
5564
|
/**
|
|
@@ -5586,14 +5592,37 @@ interface RandomPlayerPickerProps {
|
|
|
5586
5592
|
renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
|
|
5587
5593
|
/** Imperative handle. `spinRef.current?.()` triggers a spin. */
|
|
5588
5594
|
spinRef?: MutableRefObject<(() => void) | null>;
|
|
5595
|
+
/**
|
|
5596
|
+
* Imperative reset handle. `resetRef.current?.()` restores the pool to
|
|
5597
|
+
* the full `participants` list and clears history/winner — the escape
|
|
5598
|
+
* hatch for consumers that hide the built-in controls
|
|
5599
|
+
* (`showControls={false}`) but still need a "reset draft" affordance
|
|
5600
|
+
* once draft-mode pops drain the pool.
|
|
5601
|
+
*/
|
|
5602
|
+
resetRef?: MutableRefObject<(() => void) | null>;
|
|
5603
|
+
/**
|
|
5604
|
+
* Fired whenever the internal pool changes (mount, structural sync,
|
|
5605
|
+
* draft-mode pop, spin-again flush, reset). Draft-mode consumers use
|
|
5606
|
+
* this to mirror the live pool — e.g. to disable an external spin
|
|
5607
|
+
* trigger when the pool drains below 2 instead of presenting a dead
|
|
5608
|
+
* button, and to offer a reset once the draft completes.
|
|
5609
|
+
*/
|
|
5610
|
+
onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
|
|
5611
|
+
/**
|
|
5612
|
+
* Caption shown in place of `placeholderCaption` when draft-mode pops
|
|
5613
|
+
* drain the pool below 2 while the roster itself still has 2+ members.
|
|
5614
|
+
* Default: "Draft complete — reset to spin again".
|
|
5615
|
+
*/
|
|
5616
|
+
draftCompleteCaption?: string;
|
|
5589
5617
|
/**
|
|
5590
5618
|
* Visual intensity preset. Default "normal".
|
|
5591
|
-
* - "subtle" / "normal":
|
|
5592
|
-
*
|
|
5593
|
-
*
|
|
5594
|
-
*
|
|
5595
|
-
*
|
|
5596
|
-
* particles/glow/shake on reveal.
|
|
5619
|
+
* - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
|
|
5620
|
+
* - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
|
|
5621
|
+
* exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
|
|
5622
|
+
* visibly winding down through the FULL duration and lands exactly
|
|
5623
|
+
* at the end, +12 extra revolutions, ticks fired on actual
|
|
5624
|
+
* wedge/row crossings, plus more particles/glow/shake on reveal.
|
|
5625
|
+
* Falls back to a 250ms CSS settle under prefers-reduced-motion.
|
|
5597
5626
|
*/
|
|
5598
5627
|
intensity?: RandomPlayerIntensity;
|
|
5599
5628
|
/**
|
|
@@ -5636,7 +5665,7 @@ interface RandomPlayerPickerProps {
|
|
|
5636
5665
|
/** Accessible label for the wheel root. */
|
|
5637
5666
|
ariaLabel?: string;
|
|
5638
5667
|
}
|
|
5639
|
-
declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
|
|
5668
|
+
declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, resetRef, onPoolChange, draftCompleteCaption, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
|
|
5640
5669
|
|
|
5641
5670
|
type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
|
|
5642
5671
|
type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
|
|
@@ -6202,10 +6231,20 @@ interface TeamGeneratorProps {
|
|
|
6202
6231
|
maxPlayers?: number;
|
|
6203
6232
|
/** Initial roster. Uncontrolled — the component manages names internally. */
|
|
6204
6233
|
defaultNames?: string[];
|
|
6234
|
+
/** Initial generated teams (e.g. restored from a saved session).
|
|
6235
|
+
* Uncontrolled — the component takes over after mount. An empty array
|
|
6236
|
+
* is treated as "no teams". */
|
|
6237
|
+
defaultTeams?: string[][];
|
|
6205
6238
|
/** Called whenever the name pool changes. */
|
|
6206
6239
|
onNamesChange?: (names: string[]) => void;
|
|
6207
6240
|
/** Called whenever teams are (re)generated. */
|
|
6208
6241
|
onGenerate?: (teams: string[][]) => void;
|
|
6242
|
+
/** Called whenever the generated teams change for ANY reason — a
|
|
6243
|
+
* (re)generation, or invalidation to `null` when a player is removed,
|
|
6244
|
+
* the pool is cleared, or the team count changes. Unlike `onGenerate`,
|
|
6245
|
+
* this mirrors the full lifecycle of the on-screen split so hosts can
|
|
6246
|
+
* persist it and restore via `defaultTeams`. */
|
|
6247
|
+
onTeamsChange?: (teams: string[][] | null) => void;
|
|
6209
6248
|
/** Called whenever the team count selection changes. */
|
|
6210
6249
|
onTeamCountChange?: (count: number) => void;
|
|
6211
6250
|
/** Localized / customized labels. */
|
|
@@ -6218,7 +6257,7 @@ interface TeamGeneratorProps {
|
|
|
6218
6257
|
* CSV / TSV roster from a spreadsheet. Default `true`. */
|
|
6219
6258
|
enableImport?: boolean;
|
|
6220
6259
|
}
|
|
6221
|
-
declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
|
|
6260
|
+
declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, defaultTeams, onNamesChange, onGenerate, onTeamsChange, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
|
|
6222
6261
|
declare namespace TeamGenerator {
|
|
6223
6262
|
var displayName: string;
|
|
6224
6263
|
}
|
package/dist/web/index.js
CHANGED
|
@@ -33381,6 +33381,11 @@ var WEDGE_PALETTE = [
|
|
|
33381
33381
|
];
|
|
33382
33382
|
var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
|
|
33383
33383
|
var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
|
|
33384
|
+
var PHYSICS_DECAY_K = 4;
|
|
33385
|
+
function physicsRampDown(t) {
|
|
33386
|
+
return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
|
|
33387
|
+
}
|
|
33388
|
+
var REEL_LEAD_IN = 32;
|
|
33384
33389
|
var INTENSITY_CONFIG = {
|
|
33385
33390
|
subtle: {
|
|
33386
33391
|
particles: 14,
|
|
@@ -33452,6 +33457,9 @@ function ReelStage({
|
|
|
33452
33457
|
winner,
|
|
33453
33458
|
spinning,
|
|
33454
33459
|
reelY,
|
|
33460
|
+
displayY,
|
|
33461
|
+
cssTransition,
|
|
33462
|
+
tapeRef,
|
|
33455
33463
|
reelVisibleRows,
|
|
33456
33464
|
reelRowHeight,
|
|
33457
33465
|
size,
|
|
@@ -33475,17 +33483,16 @@ function ReelStage({
|
|
|
33475
33483
|
const stripHeight = reelVisibleRows * reelRowHeight;
|
|
33476
33484
|
const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
|
|
33477
33485
|
const P = pool.length;
|
|
33478
|
-
const leadIn = 32;
|
|
33479
33486
|
const tapeRows = Math.max(
|
|
33480
|
-
reelVisibleRows +
|
|
33481
|
-
Math.ceil((reelY + stripHeight) / reelRowHeight) +
|
|
33487
|
+
reelVisibleRows + REEL_LEAD_IN + 1,
|
|
33488
|
+
Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
|
|
33482
33489
|
);
|
|
33483
33490
|
const tape = Array.from({ length: tapeRows }, (_, i) => {
|
|
33484
|
-
const poolIdx = P === 0 ? 0 : ((i -
|
|
33491
|
+
const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
|
|
33485
33492
|
return pool[poolIdx];
|
|
33486
33493
|
});
|
|
33487
|
-
const effectiveY =
|
|
33488
|
-
const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
|
|
33494
|
+
const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
|
|
33495
|
+
const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
|
|
33489
33496
|
return /* @__PURE__ */ (0, import_jsx_runtime116.jsxs)(
|
|
33490
33497
|
"div",
|
|
33491
33498
|
{
|
|
@@ -33530,6 +33537,7 @@ function ReelStage({
|
|
|
33530
33537
|
) : /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
|
|
33531
33538
|
"div",
|
|
33532
33539
|
{
|
|
33540
|
+
ref: tapeRef,
|
|
33533
33541
|
"aria-hidden": "true",
|
|
33534
33542
|
style: {
|
|
33535
33543
|
position: "absolute",
|
|
@@ -33745,6 +33753,9 @@ function RandomPlayerPicker({
|
|
|
33745
33753
|
showStats = true,
|
|
33746
33754
|
renderWinner,
|
|
33747
33755
|
spinRef,
|
|
33756
|
+
resetRef,
|
|
33757
|
+
onPoolChange,
|
|
33758
|
+
draftCompleteCaption = "Draft complete \u2014 reset to spin again",
|
|
33748
33759
|
intensity = "normal",
|
|
33749
33760
|
enableHaptics = false,
|
|
33750
33761
|
tickSoundUrl,
|
|
@@ -33759,6 +33770,7 @@ function RandomPlayerPicker({
|
|
|
33759
33770
|
const dark = useDarkMode(darkMode);
|
|
33760
33771
|
const reducedMotion = usePrefersReducedMotion();
|
|
33761
33772
|
const intensityCfg = INTENSITY_CONFIG[intensity];
|
|
33773
|
+
const physicsDriven = intensity === "dramatic" && !reducedMotion;
|
|
33762
33774
|
const [pool, setPool] = (0, import_react73.useState)(participants);
|
|
33763
33775
|
const [rotation, setRotation] = (0, import_react73.useState)(0);
|
|
33764
33776
|
const [reelY, setReelY] = (0, import_react73.useState)(0);
|
|
@@ -33777,6 +33789,11 @@ function RandomPlayerPicker({
|
|
|
33777
33789
|
const winAudioRef = (0, import_react73.useRef)(null);
|
|
33778
33790
|
const containerRef = (0, import_react73.useRef)(null);
|
|
33779
33791
|
const prevIdsRef = (0, import_react73.useRef)("");
|
|
33792
|
+
const wheelSvgRef = (0, import_react73.useRef)(null);
|
|
33793
|
+
const reelTapeRef = (0, import_react73.useRef)(null);
|
|
33794
|
+
const rafRef = (0, import_react73.useRef)(null);
|
|
33795
|
+
const visualRotationRef = (0, import_react73.useRef)(null);
|
|
33796
|
+
const visualReelYRef = (0, import_react73.useRef)(null);
|
|
33780
33797
|
(0, import_react73.useEffect)(() => {
|
|
33781
33798
|
if (typeof window === "undefined" || !tickSoundUrl) {
|
|
33782
33799
|
tickAudioRef.current = null;
|
|
@@ -33830,6 +33847,12 @@ function RandomPlayerPicker({
|
|
|
33830
33847
|
clearTimeout(popTimer.current);
|
|
33831
33848
|
popTimer.current = null;
|
|
33832
33849
|
}
|
|
33850
|
+
if (rafRef.current) {
|
|
33851
|
+
cancelAnimationFrame(rafRef.current);
|
|
33852
|
+
rafRef.current = null;
|
|
33853
|
+
}
|
|
33854
|
+
visualRotationRef.current = null;
|
|
33855
|
+
visualReelYRef.current = null;
|
|
33833
33856
|
pendingPopIdxRef.current = null;
|
|
33834
33857
|
setSpinning(false);
|
|
33835
33858
|
setPool(participants);
|
|
@@ -33902,7 +33925,20 @@ function RandomPlayerPicker({
|
|
|
33902
33925
|
}
|
|
33903
33926
|
}
|
|
33904
33927
|
const activeN = activePool.length;
|
|
33905
|
-
if (activeN < 2)
|
|
33928
|
+
if (activeN < 2) {
|
|
33929
|
+
if (removeWinnersRef.current && activePool.length === 1 && history.length > 0) {
|
|
33930
|
+
const survivor = activePool[0];
|
|
33931
|
+
if (survivor) {
|
|
33932
|
+
const promoted = [survivor, ...history].slice(0, 20);
|
|
33933
|
+
setPool([]);
|
|
33934
|
+
setWinner(survivor);
|
|
33935
|
+
setRevealKey((k) => k + 1);
|
|
33936
|
+
setHistory(promoted);
|
|
33937
|
+
onPick?.(survivor, promoted, { lastStanding: true });
|
|
33938
|
+
}
|
|
33939
|
+
}
|
|
33940
|
+
return;
|
|
33941
|
+
}
|
|
33906
33942
|
const activeAnglePerWedge = 2 * Math.PI / activeN;
|
|
33907
33943
|
const activeRotation = rotation;
|
|
33908
33944
|
const activeReelY = reelY;
|
|
@@ -33941,7 +33977,56 @@ function RandomPlayerPicker({
|
|
|
33941
33977
|
} else {
|
|
33942
33978
|
setRotation(finalRotation);
|
|
33943
33979
|
}
|
|
33944
|
-
|
|
33980
|
+
const fireTick = () => {
|
|
33981
|
+
onTick?.();
|
|
33982
|
+
const ta = tickAudioRef.current;
|
|
33983
|
+
if (ta) {
|
|
33984
|
+
try {
|
|
33985
|
+
ta.currentTime = 0;
|
|
33986
|
+
void ta.play().catch(() => {
|
|
33987
|
+
});
|
|
33988
|
+
} catch {
|
|
33989
|
+
}
|
|
33990
|
+
}
|
|
33991
|
+
};
|
|
33992
|
+
if (physicsDriven) {
|
|
33993
|
+
const from = mode === "reel" ? activeReelY : activeRotation;
|
|
33994
|
+
const to = mode === "reel" ? finalReelY : finalRotation;
|
|
33995
|
+
const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
|
|
33996
|
+
if (mode === "reel") visualReelYRef.current = from;
|
|
33997
|
+
else visualRotationRef.current = from;
|
|
33998
|
+
let crossings = 0;
|
|
33999
|
+
const t0 = performance.now();
|
|
34000
|
+
const step = (now) => {
|
|
34001
|
+
const t = Math.min(1, (now - t0) / effectiveDuration);
|
|
34002
|
+
const value = from + (to - from) * physicsRampDown(t);
|
|
34003
|
+
if (mode === "reel") {
|
|
34004
|
+
visualReelYRef.current = value;
|
|
34005
|
+
const el = reelTapeRef.current;
|
|
34006
|
+
if (el) {
|
|
34007
|
+
el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
|
|
34008
|
+
}
|
|
34009
|
+
} else {
|
|
34010
|
+
visualRotationRef.current = value;
|
|
34011
|
+
const el = wheelSvgRef.current;
|
|
34012
|
+
if (el) el.style.transform = `rotate(${value}deg)`;
|
|
34013
|
+
}
|
|
34014
|
+
const crossed = Math.floor(Math.abs(value - from) / crossUnit);
|
|
34015
|
+
if (crossed > crossings) {
|
|
34016
|
+
crossings = crossed;
|
|
34017
|
+
fireTick();
|
|
34018
|
+
}
|
|
34019
|
+
if (t < 1) {
|
|
34020
|
+
rafRef.current = requestAnimationFrame(step);
|
|
34021
|
+
} else {
|
|
34022
|
+
rafRef.current = null;
|
|
34023
|
+
visualRotationRef.current = null;
|
|
34024
|
+
visualReelYRef.current = null;
|
|
34025
|
+
}
|
|
34026
|
+
};
|
|
34027
|
+
rafRef.current = requestAnimationFrame(step);
|
|
34028
|
+
}
|
|
34029
|
+
if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
|
|
33945
34030
|
const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
|
|
33946
34031
|
const totalDelta = deltaMod + revolutions * 360;
|
|
33947
34032
|
const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
|
|
@@ -33959,16 +34044,7 @@ function RandomPlayerPicker({
|
|
|
33959
34044
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
33960
34045
|
const interval = startInterval + (endInterval - startInterval) * eased;
|
|
33961
34046
|
tickInterval.current = setTimeout(() => {
|
|
33962
|
-
|
|
33963
|
-
const ta = tickAudioRef.current;
|
|
33964
|
-
if (ta) {
|
|
33965
|
-
try {
|
|
33966
|
-
ta.currentTime = 0;
|
|
33967
|
-
void ta.play().catch(() => {
|
|
33968
|
-
});
|
|
33969
|
-
} catch {
|
|
33970
|
-
}
|
|
33971
|
-
}
|
|
34047
|
+
fireTick();
|
|
33972
34048
|
elapsed += interval;
|
|
33973
34049
|
schedule();
|
|
33974
34050
|
}, interval);
|
|
@@ -34027,8 +34103,19 @@ function RandomPlayerPicker({
|
|
|
34027
34103
|
popTimer.current = null;
|
|
34028
34104
|
pendingPopIdxRef.current = null;
|
|
34029
34105
|
if (!removeWinnersRef.current) return;
|
|
34030
|
-
|
|
34031
|
-
|
|
34106
|
+
const survivors = activePool.filter((_, i) => i !== finalWinnerIdx);
|
|
34107
|
+
const survivor = survivors.length === 1 ? survivors[0] : void 0;
|
|
34108
|
+
if (survivor) {
|
|
34109
|
+
const promoted = [survivor, ...nextHistory].slice(0, 20);
|
|
34110
|
+
setPool([]);
|
|
34111
|
+
setWinner(survivor);
|
|
34112
|
+
setRevealKey((k) => k + 1);
|
|
34113
|
+
setHistory(promoted);
|
|
34114
|
+
onPick?.(survivor, promoted, { lastStanding: true });
|
|
34115
|
+
} else {
|
|
34116
|
+
setPool(survivors);
|
|
34117
|
+
setWinner(null);
|
|
34118
|
+
}
|
|
34032
34119
|
setRotation(0);
|
|
34033
34120
|
setReelY(0);
|
|
34034
34121
|
}, 2800);
|
|
@@ -34051,6 +34138,7 @@ function RandomPlayerPicker({
|
|
|
34051
34138
|
maxRevolutions,
|
|
34052
34139
|
history,
|
|
34053
34140
|
reducedMotion,
|
|
34141
|
+
physicsDriven,
|
|
34054
34142
|
enableHaptics,
|
|
34055
34143
|
intensityCfg.particles,
|
|
34056
34144
|
intensityCfg.durationMult,
|
|
@@ -34062,12 +34150,20 @@ function RandomPlayerPicker({
|
|
|
34062
34150
|
if (spinRef && spinRef.current === spin) spinRef.current = null;
|
|
34063
34151
|
};
|
|
34064
34152
|
}, [spin, spinRef]);
|
|
34153
|
+
const onPoolChangeRef = (0, import_react73.useRef)(onPoolChange);
|
|
34154
|
+
(0, import_react73.useEffect)(() => {
|
|
34155
|
+
onPoolChangeRef.current = onPoolChange;
|
|
34156
|
+
});
|
|
34157
|
+
(0, import_react73.useEffect)(() => {
|
|
34158
|
+
onPoolChangeRef.current?.(pool);
|
|
34159
|
+
}, [pool]);
|
|
34065
34160
|
(0, import_react73.useEffect)(
|
|
34066
34161
|
() => () => {
|
|
34067
34162
|
if (winnerTimer.current) clearTimeout(winnerTimer.current);
|
|
34068
34163
|
if (particleTimer.current) clearTimeout(particleTimer.current);
|
|
34069
34164
|
if (popTimer.current) clearTimeout(popTimer.current);
|
|
34070
34165
|
if (tickInterval.current) clearTimeout(tickInterval.current);
|
|
34166
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
34071
34167
|
},
|
|
34072
34168
|
[]
|
|
34073
34169
|
);
|
|
@@ -34097,6 +34193,12 @@ function RandomPlayerPicker({
|
|
|
34097
34193
|
clearTimeout(popTimer.current);
|
|
34098
34194
|
popTimer.current = null;
|
|
34099
34195
|
}
|
|
34196
|
+
if (rafRef.current) {
|
|
34197
|
+
cancelAnimationFrame(rafRef.current);
|
|
34198
|
+
rafRef.current = null;
|
|
34199
|
+
}
|
|
34200
|
+
visualRotationRef.current = null;
|
|
34201
|
+
visualReelYRef.current = null;
|
|
34100
34202
|
pendingPopIdxRef.current = null;
|
|
34101
34203
|
setSpinning(false);
|
|
34102
34204
|
setPool(participants);
|
|
@@ -34106,9 +34208,17 @@ function RandomPlayerPicker({
|
|
|
34106
34208
|
setReelY(0);
|
|
34107
34209
|
setParticles([]);
|
|
34108
34210
|
}, [participants]);
|
|
34211
|
+
(0, import_react73.useEffect)(() => {
|
|
34212
|
+
if (resetRef) resetRef.current = reset;
|
|
34213
|
+
return () => {
|
|
34214
|
+
if (resetRef && resetRef.current === reset) resetRef.current = null;
|
|
34215
|
+
};
|
|
34216
|
+
}, [reset, resetRef]);
|
|
34109
34217
|
const isIdle = n < 2;
|
|
34218
|
+
const drainedByDraft = isIdle && participants.length >= 2;
|
|
34219
|
+
const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
|
|
34110
34220
|
const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
|
|
34111
|
-
if (isIdle && placeholderMode === "message") {
|
|
34221
|
+
if (isIdle && !drainedByDraft && placeholderMode === "message") {
|
|
34112
34222
|
return /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
|
|
34113
34223
|
"div",
|
|
34114
34224
|
{
|
|
@@ -34130,6 +34240,8 @@ function RandomPlayerPicker({
|
|
|
34130
34240
|
const shakeKeyframe = `rppShake_${intensity}`;
|
|
34131
34241
|
const glowKeyframe = `rppGlow_${intensity}`;
|
|
34132
34242
|
const pulseKeyframe = `rppPulse_${intensity}`;
|
|
34243
|
+
const displayRotation = visualRotationRef.current ?? rotation;
|
|
34244
|
+
const displayReelY = visualReelYRef.current ?? reelY;
|
|
34133
34245
|
return /* @__PURE__ */ (0, import_jsx_runtime116.jsxs)(
|
|
34134
34246
|
"div",
|
|
34135
34247
|
{
|
|
@@ -34176,13 +34288,33 @@ function RandomPlayerPicker({
|
|
|
34176
34288
|
[data-rpp-anim] { animation: none !important; }
|
|
34177
34289
|
}
|
|
34178
34290
|
` }),
|
|
34179
|
-
|
|
34291
|
+
isIdle && placeholderMode === "message" ? (
|
|
34292
|
+
/* Drained draft in "message" mode — compact box in the wheel's
|
|
34293
|
+
place; the winner card / stats / controls / history below stay
|
|
34294
|
+
mounted so the endgame isn't a dead end. */
|
|
34295
|
+
/* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
|
|
34296
|
+
"div",
|
|
34297
|
+
{
|
|
34298
|
+
style: {
|
|
34299
|
+
padding: "2rem 1rem",
|
|
34300
|
+
textAlign: "center",
|
|
34301
|
+
color: c.dim,
|
|
34302
|
+
borderRadius: 12,
|
|
34303
|
+
border: `1px dashed ${c.border}`
|
|
34304
|
+
},
|
|
34305
|
+
children: effectiveIdleCaption
|
|
34306
|
+
}
|
|
34307
|
+
)
|
|
34308
|
+
) : mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
|
|
34180
34309
|
ReelStage,
|
|
34181
34310
|
{
|
|
34182
34311
|
pool,
|
|
34183
34312
|
winner,
|
|
34184
34313
|
spinning,
|
|
34185
34314
|
reelY,
|
|
34315
|
+
displayY: displayReelY,
|
|
34316
|
+
cssTransition: !physicsDriven,
|
|
34317
|
+
tapeRef: reelTapeRef,
|
|
34186
34318
|
reelVisibleRows,
|
|
34187
34319
|
reelRowHeight,
|
|
34188
34320
|
size,
|
|
@@ -34191,7 +34323,7 @@ function RandomPlayerPicker({
|
|
|
34191
34323
|
dark,
|
|
34192
34324
|
isIdle,
|
|
34193
34325
|
placeholderMode,
|
|
34194
|
-
placeholderCaption,
|
|
34326
|
+
placeholderCaption: effectiveIdleCaption,
|
|
34195
34327
|
placeholderDiscColor,
|
|
34196
34328
|
spinDuration,
|
|
34197
34329
|
intensityCfg,
|
|
@@ -34248,6 +34380,7 @@ function RandomPlayerPicker({
|
|
|
34248
34380
|
/* @__PURE__ */ (0, import_jsx_runtime116.jsxs)(
|
|
34249
34381
|
"svg",
|
|
34250
34382
|
{
|
|
34383
|
+
ref: wheelSvgRef,
|
|
34251
34384
|
width: size,
|
|
34252
34385
|
height: size,
|
|
34253
34386
|
"aria-hidden": "true",
|
|
@@ -34256,8 +34389,8 @@ function RandomPlayerPicker({
|
|
|
34256
34389
|
top: 36,
|
|
34257
34390
|
left: 0,
|
|
34258
34391
|
transformOrigin: `${cx}px ${cy}px`,
|
|
34259
|
-
transform: `rotate(${
|
|
34260
|
-
transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
|
|
34392
|
+
transform: `rotate(${displayRotation}deg)`,
|
|
34393
|
+
transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
|
|
34261
34394
|
filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
|
|
34262
34395
|
},
|
|
34263
34396
|
children: [
|
|
@@ -34410,7 +34543,7 @@ function RandomPlayerPicker({
|
|
|
34410
34543
|
pointerEvents: "none",
|
|
34411
34544
|
zIndex: 3
|
|
34412
34545
|
},
|
|
34413
|
-
children:
|
|
34546
|
+
children: effectiveIdleCaption
|
|
34414
34547
|
}
|
|
34415
34548
|
),
|
|
34416
34549
|
particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime116.jsx)(
|
|
@@ -38584,6 +38717,33 @@ function GameTimer({
|
|
|
38584
38717
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
38585
38718
|
};
|
|
38586
38719
|
}, [running, tick]);
|
|
38720
|
+
(0, import_react76.useEffect)(() => {
|
|
38721
|
+
if (!running || mode !== "countdown") return;
|
|
38722
|
+
let timeoutId = null;
|
|
38723
|
+
const disarm = () => {
|
|
38724
|
+
if (timeoutId !== null) {
|
|
38725
|
+
clearTimeout(timeoutId);
|
|
38726
|
+
timeoutId = null;
|
|
38727
|
+
}
|
|
38728
|
+
};
|
|
38729
|
+
const arm = () => {
|
|
38730
|
+
disarm();
|
|
38731
|
+
const remainingUntilDone = durationMs - elapsedRef.current;
|
|
38732
|
+
if (remainingUntilDone > 0) {
|
|
38733
|
+
timeoutId = setTimeout(tick, remainingUntilDone + 50);
|
|
38734
|
+
}
|
|
38735
|
+
};
|
|
38736
|
+
const onVisibility = () => {
|
|
38737
|
+
if (document.hidden) arm();
|
|
38738
|
+
else disarm();
|
|
38739
|
+
};
|
|
38740
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
38741
|
+
if (document.hidden) arm();
|
|
38742
|
+
return () => {
|
|
38743
|
+
disarm();
|
|
38744
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
38745
|
+
};
|
|
38746
|
+
}, [running, mode, durationMs, tick]);
|
|
38587
38747
|
const start = (0, import_react76.useCallback)(() => {
|
|
38588
38748
|
if (completed && mode === "countdown") return;
|
|
38589
38749
|
setRunning(true);
|
|
@@ -39890,8 +40050,10 @@ function TeamGenerator({
|
|
|
39890
40050
|
minPlayersPerTeam = 2,
|
|
39891
40051
|
maxPlayers = Number.POSITIVE_INFINITY,
|
|
39892
40052
|
defaultNames = [],
|
|
40053
|
+
defaultTeams,
|
|
39893
40054
|
onNamesChange,
|
|
39894
40055
|
onGenerate,
|
|
40056
|
+
onTeamsChange,
|
|
39895
40057
|
onTeamCountChange,
|
|
39896
40058
|
labels,
|
|
39897
40059
|
ariaLabel = "Team generator",
|
|
@@ -39906,12 +40068,17 @@ function TeamGenerator({
|
|
|
39906
40068
|
const [names, setNames] = (0, import_react78.useState)(defaultNames);
|
|
39907
40069
|
const [input, setInput] = (0, import_react78.useState)("");
|
|
39908
40070
|
const [teamCount, setTeamCount] = (0, import_react78.useState)(initialCount);
|
|
39909
|
-
const [teams, setTeams] = (0, import_react78.useState)(
|
|
40071
|
+
const [teams, setTeams] = (0, import_react78.useState)(
|
|
40072
|
+
defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
|
|
40073
|
+
);
|
|
39910
40074
|
const inputRef = (0, import_react78.useRef)(null);
|
|
39911
40075
|
const minNames = teamCount * minPlayersPerTeam;
|
|
39912
40076
|
(0, import_react78.useEffect)(() => {
|
|
39913
40077
|
onNamesChange?.(names);
|
|
39914
40078
|
}, [names, onNamesChange]);
|
|
40079
|
+
(0, import_react78.useEffect)(() => {
|
|
40080
|
+
onTeamsChange?.(teams);
|
|
40081
|
+
}, [teams, onTeamsChange]);
|
|
39915
40082
|
const addName = (0, import_react78.useCallback)(() => {
|
|
39916
40083
|
const trimmed = input.trim();
|
|
39917
40084
|
if (!trimmed) return;
|