@digilogiclabs/saas-factory-ui 2.4.0 → 2.5.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 +41 -8
- package/dist/index.d.ts +41 -8
- package/dist/index.js +141 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +141 -25
- package/dist/index.mjs.map +1 -1
- package/dist/web/index.d.mts +41 -8
- package/dist/web/index.d.ts +41 -8
- package/dist/web/index.js +141 -25
- package/dist/web/index.js.map +1 -1
- package/dist/web/index.mjs +141 -25
- package/dist/web/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -5050,14 +5050,37 @@ interface RandomPlayerPickerProps {
|
|
|
5050
5050
|
renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
|
|
5051
5051
|
/** Imperative handle. `spinRef.current?.()` triggers a spin. */
|
|
5052
5052
|
spinRef?: MutableRefObject<(() => void) | null>;
|
|
5053
|
+
/**
|
|
5054
|
+
* Imperative reset handle. `resetRef.current?.()` restores the pool to
|
|
5055
|
+
* the full `participants` list and clears history/winner — the escape
|
|
5056
|
+
* hatch for consumers that hide the built-in controls
|
|
5057
|
+
* (`showControls={false}`) but still need a "reset draft" affordance
|
|
5058
|
+
* once draft-mode pops drain the pool.
|
|
5059
|
+
*/
|
|
5060
|
+
resetRef?: MutableRefObject<(() => void) | null>;
|
|
5061
|
+
/**
|
|
5062
|
+
* Fired whenever the internal pool changes (mount, structural sync,
|
|
5063
|
+
* draft-mode pop, spin-again flush, reset). Draft-mode consumers use
|
|
5064
|
+
* this to mirror the live pool — e.g. to disable an external spin
|
|
5065
|
+
* trigger when the pool drains below 2 instead of presenting a dead
|
|
5066
|
+
* button, and to offer a reset once the draft completes.
|
|
5067
|
+
*/
|
|
5068
|
+
onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
|
|
5069
|
+
/**
|
|
5070
|
+
* Caption shown in place of `placeholderCaption` when draft-mode pops
|
|
5071
|
+
* drain the pool below 2 while the roster itself still has 2+ members.
|
|
5072
|
+
* Default: "Draft complete — reset to spin again".
|
|
5073
|
+
*/
|
|
5074
|
+
draftCompleteCaption?: string;
|
|
5053
5075
|
/**
|
|
5054
5076
|
* Visual intensity preset. Default "normal".
|
|
5055
|
-
* - "subtle" / "normal":
|
|
5056
|
-
*
|
|
5057
|
-
*
|
|
5058
|
-
*
|
|
5059
|
-
*
|
|
5060
|
-
* particles/glow/shake on reveal.
|
|
5077
|
+
* - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
|
|
5078
|
+
* - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
|
|
5079
|
+
* exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
|
|
5080
|
+
* visibly winding down through the FULL duration and lands exactly
|
|
5081
|
+
* at the end, +12 extra revolutions, ticks fired on actual
|
|
5082
|
+
* wedge/row crossings, plus more particles/glow/shake on reveal.
|
|
5083
|
+
* Falls back to a 250ms CSS settle under prefers-reduced-motion.
|
|
5061
5084
|
*/
|
|
5062
5085
|
intensity?: RandomPlayerIntensity;
|
|
5063
5086
|
/**
|
|
@@ -5100,7 +5123,7 @@ interface RandomPlayerPickerProps {
|
|
|
5100
5123
|
/** Accessible label for the wheel root. */
|
|
5101
5124
|
ariaLabel?: string;
|
|
5102
5125
|
}
|
|
5103
|
-
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;
|
|
5126
|
+
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;
|
|
5104
5127
|
|
|
5105
5128
|
type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
|
|
5106
5129
|
type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
|
|
@@ -5666,10 +5689,20 @@ interface TeamGeneratorProps {
|
|
|
5666
5689
|
maxPlayers?: number;
|
|
5667
5690
|
/** Initial roster. Uncontrolled — the component manages names internally. */
|
|
5668
5691
|
defaultNames?: string[];
|
|
5692
|
+
/** Initial generated teams (e.g. restored from a saved session).
|
|
5693
|
+
* Uncontrolled — the component takes over after mount. An empty array
|
|
5694
|
+
* is treated as "no teams". */
|
|
5695
|
+
defaultTeams?: string[][];
|
|
5669
5696
|
/** Called whenever the name pool changes. */
|
|
5670
5697
|
onNamesChange?: (names: string[]) => void;
|
|
5671
5698
|
/** Called whenever teams are (re)generated. */
|
|
5672
5699
|
onGenerate?: (teams: string[][]) => void;
|
|
5700
|
+
/** Called whenever the generated teams change for ANY reason — a
|
|
5701
|
+
* (re)generation, or invalidation to `null` when a player is removed,
|
|
5702
|
+
* the pool is cleared, or the team count changes. Unlike `onGenerate`,
|
|
5703
|
+
* this mirrors the full lifecycle of the on-screen split so hosts can
|
|
5704
|
+
* persist it and restore via `defaultTeams`. */
|
|
5705
|
+
onTeamsChange?: (teams: string[][] | null) => void;
|
|
5673
5706
|
/** Called whenever the team count selection changes. */
|
|
5674
5707
|
onTeamCountChange?: (count: number) => void;
|
|
5675
5708
|
/** Localized / customized labels. */
|
|
@@ -5682,7 +5715,7 @@ interface TeamGeneratorProps {
|
|
|
5682
5715
|
* CSV / TSV roster from a spreadsheet. Default `true`. */
|
|
5683
5716
|
enableImport?: boolean;
|
|
5684
5717
|
}
|
|
5685
|
-
declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
|
|
5718
|
+
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;
|
|
5686
5719
|
declare namespace TeamGenerator {
|
|
5687
5720
|
var displayName: string;
|
|
5688
5721
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5050,14 +5050,37 @@ interface RandomPlayerPickerProps {
|
|
|
5050
5050
|
renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
|
|
5051
5051
|
/** Imperative handle. `spinRef.current?.()` triggers a spin. */
|
|
5052
5052
|
spinRef?: MutableRefObject<(() => void) | null>;
|
|
5053
|
+
/**
|
|
5054
|
+
* Imperative reset handle. `resetRef.current?.()` restores the pool to
|
|
5055
|
+
* the full `participants` list and clears history/winner — the escape
|
|
5056
|
+
* hatch for consumers that hide the built-in controls
|
|
5057
|
+
* (`showControls={false}`) but still need a "reset draft" affordance
|
|
5058
|
+
* once draft-mode pops drain the pool.
|
|
5059
|
+
*/
|
|
5060
|
+
resetRef?: MutableRefObject<(() => void) | null>;
|
|
5061
|
+
/**
|
|
5062
|
+
* Fired whenever the internal pool changes (mount, structural sync,
|
|
5063
|
+
* draft-mode pop, spin-again flush, reset). Draft-mode consumers use
|
|
5064
|
+
* this to mirror the live pool — e.g. to disable an external spin
|
|
5065
|
+
* trigger when the pool drains below 2 instead of presenting a dead
|
|
5066
|
+
* button, and to offer a reset once the draft completes.
|
|
5067
|
+
*/
|
|
5068
|
+
onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
|
|
5069
|
+
/**
|
|
5070
|
+
* Caption shown in place of `placeholderCaption` when draft-mode pops
|
|
5071
|
+
* drain the pool below 2 while the roster itself still has 2+ members.
|
|
5072
|
+
* Default: "Draft complete — reset to spin again".
|
|
5073
|
+
*/
|
|
5074
|
+
draftCompleteCaption?: string;
|
|
5053
5075
|
/**
|
|
5054
5076
|
* Visual intensity preset. Default "normal".
|
|
5055
|
-
* - "subtle" / "normal":
|
|
5056
|
-
*
|
|
5057
|
-
*
|
|
5058
|
-
*
|
|
5059
|
-
*
|
|
5060
|
-
* particles/glow/shake on reveal.
|
|
5077
|
+
* - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
|
|
5078
|
+
* - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
|
|
5079
|
+
* exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
|
|
5080
|
+
* visibly winding down through the FULL duration and lands exactly
|
|
5081
|
+
* at the end, +12 extra revolutions, ticks fired on actual
|
|
5082
|
+
* wedge/row crossings, plus more particles/glow/shake on reveal.
|
|
5083
|
+
* Falls back to a 250ms CSS settle under prefers-reduced-motion.
|
|
5061
5084
|
*/
|
|
5062
5085
|
intensity?: RandomPlayerIntensity;
|
|
5063
5086
|
/**
|
|
@@ -5100,7 +5123,7 @@ interface RandomPlayerPickerProps {
|
|
|
5100
5123
|
/** Accessible label for the wheel root. */
|
|
5101
5124
|
ariaLabel?: string;
|
|
5102
5125
|
}
|
|
5103
|
-
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;
|
|
5126
|
+
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;
|
|
5104
5127
|
|
|
5105
5128
|
type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
|
|
5106
5129
|
type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
|
|
@@ -5666,10 +5689,20 @@ interface TeamGeneratorProps {
|
|
|
5666
5689
|
maxPlayers?: number;
|
|
5667
5690
|
/** Initial roster. Uncontrolled — the component manages names internally. */
|
|
5668
5691
|
defaultNames?: string[];
|
|
5692
|
+
/** Initial generated teams (e.g. restored from a saved session).
|
|
5693
|
+
* Uncontrolled — the component takes over after mount. An empty array
|
|
5694
|
+
* is treated as "no teams". */
|
|
5695
|
+
defaultTeams?: string[][];
|
|
5669
5696
|
/** Called whenever the name pool changes. */
|
|
5670
5697
|
onNamesChange?: (names: string[]) => void;
|
|
5671
5698
|
/** Called whenever teams are (re)generated. */
|
|
5672
5699
|
onGenerate?: (teams: string[][]) => void;
|
|
5700
|
+
/** Called whenever the generated teams change for ANY reason — a
|
|
5701
|
+
* (re)generation, or invalidation to `null` when a player is removed,
|
|
5702
|
+
* the pool is cleared, or the team count changes. Unlike `onGenerate`,
|
|
5703
|
+
* this mirrors the full lifecycle of the on-screen split so hosts can
|
|
5704
|
+
* persist it and restore via `defaultTeams`. */
|
|
5705
|
+
onTeamsChange?: (teams: string[][] | null) => void;
|
|
5673
5706
|
/** Called whenever the team count selection changes. */
|
|
5674
5707
|
onTeamCountChange?: (count: number) => void;
|
|
5675
5708
|
/** Localized / customized labels. */
|
|
@@ -5682,7 +5715,7 @@ interface TeamGeneratorProps {
|
|
|
5682
5715
|
* CSV / TSV roster from a spreadsheet. Default `true`. */
|
|
5683
5716
|
enableImport?: boolean;
|
|
5684
5717
|
}
|
|
5685
|
-
declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
|
|
5718
|
+
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;
|
|
5686
5719
|
declare namespace TeamGenerator {
|
|
5687
5720
|
var displayName: string;
|
|
5688
5721
|
}
|
package/dist/index.js
CHANGED
|
@@ -33535,6 +33535,11 @@ var WEDGE_PALETTE = [
|
|
|
33535
33535
|
];
|
|
33536
33536
|
var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
|
|
33537
33537
|
var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
|
|
33538
|
+
var PHYSICS_DECAY_K = 4;
|
|
33539
|
+
function physicsRampDown(t) {
|
|
33540
|
+
return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
|
|
33541
|
+
}
|
|
33542
|
+
var REEL_LEAD_IN = 32;
|
|
33538
33543
|
var INTENSITY_CONFIG = {
|
|
33539
33544
|
subtle: {
|
|
33540
33545
|
particles: 14,
|
|
@@ -33606,6 +33611,9 @@ function ReelStage({
|
|
|
33606
33611
|
winner,
|
|
33607
33612
|
spinning,
|
|
33608
33613
|
reelY,
|
|
33614
|
+
displayY,
|
|
33615
|
+
cssTransition,
|
|
33616
|
+
tapeRef,
|
|
33609
33617
|
reelVisibleRows,
|
|
33610
33618
|
reelRowHeight,
|
|
33611
33619
|
size,
|
|
@@ -33629,17 +33637,16 @@ function ReelStage({
|
|
|
33629
33637
|
const stripHeight = reelVisibleRows * reelRowHeight;
|
|
33630
33638
|
const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
|
|
33631
33639
|
const P = pool.length;
|
|
33632
|
-
const leadIn = 32;
|
|
33633
33640
|
const tapeRows = Math.max(
|
|
33634
|
-
reelVisibleRows +
|
|
33635
|
-
Math.ceil((reelY + stripHeight) / reelRowHeight) +
|
|
33641
|
+
reelVisibleRows + REEL_LEAD_IN + 1,
|
|
33642
|
+
Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
|
|
33636
33643
|
);
|
|
33637
33644
|
const tape = Array.from({ length: tapeRows }, (_, i) => {
|
|
33638
|
-
const poolIdx = P === 0 ? 0 : ((i -
|
|
33645
|
+
const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
|
|
33639
33646
|
return pool[poolIdx];
|
|
33640
33647
|
});
|
|
33641
|
-
const effectiveY =
|
|
33642
|
-
const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
|
|
33648
|
+
const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
|
|
33649
|
+
const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
|
|
33643
33650
|
return /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
|
|
33644
33651
|
"div",
|
|
33645
33652
|
{
|
|
@@ -33684,6 +33691,7 @@ function ReelStage({
|
|
|
33684
33691
|
) : /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
33685
33692
|
"div",
|
|
33686
33693
|
{
|
|
33694
|
+
ref: tapeRef,
|
|
33687
33695
|
"aria-hidden": "true",
|
|
33688
33696
|
style: {
|
|
33689
33697
|
position: "absolute",
|
|
@@ -33899,6 +33907,9 @@ function RandomPlayerPicker({
|
|
|
33899
33907
|
showStats = true,
|
|
33900
33908
|
renderWinner,
|
|
33901
33909
|
spinRef,
|
|
33910
|
+
resetRef,
|
|
33911
|
+
onPoolChange,
|
|
33912
|
+
draftCompleteCaption = "Draft complete \u2014 reset to spin again",
|
|
33902
33913
|
intensity = "normal",
|
|
33903
33914
|
enableHaptics = false,
|
|
33904
33915
|
tickSoundUrl,
|
|
@@ -33913,6 +33924,7 @@ function RandomPlayerPicker({
|
|
|
33913
33924
|
const dark = useDarkMode(darkMode);
|
|
33914
33925
|
const reducedMotion = usePrefersReducedMotion();
|
|
33915
33926
|
const intensityCfg = INTENSITY_CONFIG[intensity];
|
|
33927
|
+
const physicsDriven = intensity === "dramatic" && !reducedMotion;
|
|
33916
33928
|
const [pool, setPool] = (0, import_react71.useState)(participants);
|
|
33917
33929
|
const [rotation, setRotation] = (0, import_react71.useState)(0);
|
|
33918
33930
|
const [reelY, setReelY] = (0, import_react71.useState)(0);
|
|
@@ -33931,6 +33943,11 @@ function RandomPlayerPicker({
|
|
|
33931
33943
|
const winAudioRef = (0, import_react71.useRef)(null);
|
|
33932
33944
|
const containerRef = (0, import_react71.useRef)(null);
|
|
33933
33945
|
const prevIdsRef = (0, import_react71.useRef)("");
|
|
33946
|
+
const wheelSvgRef = (0, import_react71.useRef)(null);
|
|
33947
|
+
const reelTapeRef = (0, import_react71.useRef)(null);
|
|
33948
|
+
const rafRef = (0, import_react71.useRef)(null);
|
|
33949
|
+
const visualRotationRef = (0, import_react71.useRef)(null);
|
|
33950
|
+
const visualReelYRef = (0, import_react71.useRef)(null);
|
|
33934
33951
|
(0, import_react71.useEffect)(() => {
|
|
33935
33952
|
if (typeof window === "undefined" || !tickSoundUrl) {
|
|
33936
33953
|
tickAudioRef.current = null;
|
|
@@ -33984,6 +34001,12 @@ function RandomPlayerPicker({
|
|
|
33984
34001
|
clearTimeout(popTimer.current);
|
|
33985
34002
|
popTimer.current = null;
|
|
33986
34003
|
}
|
|
34004
|
+
if (rafRef.current) {
|
|
34005
|
+
cancelAnimationFrame(rafRef.current);
|
|
34006
|
+
rafRef.current = null;
|
|
34007
|
+
}
|
|
34008
|
+
visualRotationRef.current = null;
|
|
34009
|
+
visualReelYRef.current = null;
|
|
33987
34010
|
pendingPopIdxRef.current = null;
|
|
33988
34011
|
setSpinning(false);
|
|
33989
34012
|
setPool(participants);
|
|
@@ -34095,7 +34118,56 @@ function RandomPlayerPicker({
|
|
|
34095
34118
|
} else {
|
|
34096
34119
|
setRotation(finalRotation);
|
|
34097
34120
|
}
|
|
34098
|
-
|
|
34121
|
+
const fireTick = () => {
|
|
34122
|
+
onTick?.();
|
|
34123
|
+
const ta = tickAudioRef.current;
|
|
34124
|
+
if (ta) {
|
|
34125
|
+
try {
|
|
34126
|
+
ta.currentTime = 0;
|
|
34127
|
+
void ta.play().catch(() => {
|
|
34128
|
+
});
|
|
34129
|
+
} catch {
|
|
34130
|
+
}
|
|
34131
|
+
}
|
|
34132
|
+
};
|
|
34133
|
+
if (physicsDriven) {
|
|
34134
|
+
const from = mode === "reel" ? activeReelY : activeRotation;
|
|
34135
|
+
const to = mode === "reel" ? finalReelY : finalRotation;
|
|
34136
|
+
const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
|
|
34137
|
+
if (mode === "reel") visualReelYRef.current = from;
|
|
34138
|
+
else visualRotationRef.current = from;
|
|
34139
|
+
let crossings = 0;
|
|
34140
|
+
const t0 = performance.now();
|
|
34141
|
+
const step = (now) => {
|
|
34142
|
+
const t = Math.min(1, (now - t0) / effectiveDuration);
|
|
34143
|
+
const value = from + (to - from) * physicsRampDown(t);
|
|
34144
|
+
if (mode === "reel") {
|
|
34145
|
+
visualReelYRef.current = value;
|
|
34146
|
+
const el = reelTapeRef.current;
|
|
34147
|
+
if (el) {
|
|
34148
|
+
el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
|
|
34149
|
+
}
|
|
34150
|
+
} else {
|
|
34151
|
+
visualRotationRef.current = value;
|
|
34152
|
+
const el = wheelSvgRef.current;
|
|
34153
|
+
if (el) el.style.transform = `rotate(${value}deg)`;
|
|
34154
|
+
}
|
|
34155
|
+
const crossed = Math.floor(Math.abs(value - from) / crossUnit);
|
|
34156
|
+
if (crossed > crossings) {
|
|
34157
|
+
crossings = crossed;
|
|
34158
|
+
fireTick();
|
|
34159
|
+
}
|
|
34160
|
+
if (t < 1) {
|
|
34161
|
+
rafRef.current = requestAnimationFrame(step);
|
|
34162
|
+
} else {
|
|
34163
|
+
rafRef.current = null;
|
|
34164
|
+
visualRotationRef.current = null;
|
|
34165
|
+
visualReelYRef.current = null;
|
|
34166
|
+
}
|
|
34167
|
+
};
|
|
34168
|
+
rafRef.current = requestAnimationFrame(step);
|
|
34169
|
+
}
|
|
34170
|
+
if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
|
|
34099
34171
|
const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
|
|
34100
34172
|
const totalDelta = deltaMod + revolutions * 360;
|
|
34101
34173
|
const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
|
|
@@ -34113,16 +34185,7 @@ function RandomPlayerPicker({
|
|
|
34113
34185
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
34114
34186
|
const interval = startInterval + (endInterval - startInterval) * eased;
|
|
34115
34187
|
tickInterval.current = setTimeout(() => {
|
|
34116
|
-
|
|
34117
|
-
const ta = tickAudioRef.current;
|
|
34118
|
-
if (ta) {
|
|
34119
|
-
try {
|
|
34120
|
-
ta.currentTime = 0;
|
|
34121
|
-
void ta.play().catch(() => {
|
|
34122
|
-
});
|
|
34123
|
-
} catch {
|
|
34124
|
-
}
|
|
34125
|
-
}
|
|
34188
|
+
fireTick();
|
|
34126
34189
|
elapsed += interval;
|
|
34127
34190
|
schedule();
|
|
34128
34191
|
}, interval);
|
|
@@ -34182,7 +34245,7 @@ function RandomPlayerPicker({
|
|
|
34182
34245
|
pendingPopIdxRef.current = null;
|
|
34183
34246
|
if (!removeWinnersRef.current) return;
|
|
34184
34247
|
setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
|
|
34185
|
-
setWinner(null);
|
|
34248
|
+
if (activePool.length - 1 >= 2) setWinner(null);
|
|
34186
34249
|
setRotation(0);
|
|
34187
34250
|
setReelY(0);
|
|
34188
34251
|
}, 2800);
|
|
@@ -34205,6 +34268,7 @@ function RandomPlayerPicker({
|
|
|
34205
34268
|
maxRevolutions,
|
|
34206
34269
|
history,
|
|
34207
34270
|
reducedMotion,
|
|
34271
|
+
physicsDriven,
|
|
34208
34272
|
enableHaptics,
|
|
34209
34273
|
intensityCfg.particles,
|
|
34210
34274
|
intensityCfg.durationMult,
|
|
@@ -34216,12 +34280,20 @@ function RandomPlayerPicker({
|
|
|
34216
34280
|
if (spinRef && spinRef.current === spin) spinRef.current = null;
|
|
34217
34281
|
};
|
|
34218
34282
|
}, [spin, spinRef]);
|
|
34283
|
+
const onPoolChangeRef = (0, import_react71.useRef)(onPoolChange);
|
|
34284
|
+
(0, import_react71.useEffect)(() => {
|
|
34285
|
+
onPoolChangeRef.current = onPoolChange;
|
|
34286
|
+
});
|
|
34287
|
+
(0, import_react71.useEffect)(() => {
|
|
34288
|
+
onPoolChangeRef.current?.(pool);
|
|
34289
|
+
}, [pool]);
|
|
34219
34290
|
(0, import_react71.useEffect)(
|
|
34220
34291
|
() => () => {
|
|
34221
34292
|
if (winnerTimer.current) clearTimeout(winnerTimer.current);
|
|
34222
34293
|
if (particleTimer.current) clearTimeout(particleTimer.current);
|
|
34223
34294
|
if (popTimer.current) clearTimeout(popTimer.current);
|
|
34224
34295
|
if (tickInterval.current) clearTimeout(tickInterval.current);
|
|
34296
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
34225
34297
|
},
|
|
34226
34298
|
[]
|
|
34227
34299
|
);
|
|
@@ -34251,6 +34323,12 @@ function RandomPlayerPicker({
|
|
|
34251
34323
|
clearTimeout(popTimer.current);
|
|
34252
34324
|
popTimer.current = null;
|
|
34253
34325
|
}
|
|
34326
|
+
if (rafRef.current) {
|
|
34327
|
+
cancelAnimationFrame(rafRef.current);
|
|
34328
|
+
rafRef.current = null;
|
|
34329
|
+
}
|
|
34330
|
+
visualRotationRef.current = null;
|
|
34331
|
+
visualReelYRef.current = null;
|
|
34254
34332
|
pendingPopIdxRef.current = null;
|
|
34255
34333
|
setSpinning(false);
|
|
34256
34334
|
setPool(participants);
|
|
@@ -34260,9 +34338,17 @@ function RandomPlayerPicker({
|
|
|
34260
34338
|
setReelY(0);
|
|
34261
34339
|
setParticles([]);
|
|
34262
34340
|
}, [participants]);
|
|
34341
|
+
(0, import_react71.useEffect)(() => {
|
|
34342
|
+
if (resetRef) resetRef.current = reset;
|
|
34343
|
+
return () => {
|
|
34344
|
+
if (resetRef && resetRef.current === reset) resetRef.current = null;
|
|
34345
|
+
};
|
|
34346
|
+
}, [reset, resetRef]);
|
|
34263
34347
|
const isIdle = n < 2;
|
|
34348
|
+
const drainedByDraft = isIdle && participants.length >= 2;
|
|
34349
|
+
const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
|
|
34264
34350
|
const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
|
|
34265
|
-
if (isIdle && placeholderMode === "message") {
|
|
34351
|
+
if (isIdle && !drainedByDraft && placeholderMode === "message") {
|
|
34266
34352
|
return /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
34267
34353
|
"div",
|
|
34268
34354
|
{
|
|
@@ -34284,6 +34370,8 @@ function RandomPlayerPicker({
|
|
|
34284
34370
|
const shakeKeyframe = `rppShake_${intensity}`;
|
|
34285
34371
|
const glowKeyframe = `rppGlow_${intensity}`;
|
|
34286
34372
|
const pulseKeyframe = `rppPulse_${intensity}`;
|
|
34373
|
+
const displayRotation = visualRotationRef.current ?? rotation;
|
|
34374
|
+
const displayReelY = visualReelYRef.current ?? reelY;
|
|
34287
34375
|
return /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
|
|
34288
34376
|
"div",
|
|
34289
34377
|
{
|
|
@@ -34330,13 +34418,33 @@ function RandomPlayerPicker({
|
|
|
34330
34418
|
[data-rpp-anim] { animation: none !important; }
|
|
34331
34419
|
}
|
|
34332
34420
|
` }),
|
|
34333
|
-
|
|
34421
|
+
isIdle && placeholderMode === "message" ? (
|
|
34422
|
+
/* Drained draft in "message" mode — compact box in the wheel's
|
|
34423
|
+
place; the winner card / stats / controls / history below stay
|
|
34424
|
+
mounted so the endgame isn't a dead end. */
|
|
34425
|
+
/* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
34426
|
+
"div",
|
|
34427
|
+
{
|
|
34428
|
+
style: {
|
|
34429
|
+
padding: "2rem 1rem",
|
|
34430
|
+
textAlign: "center",
|
|
34431
|
+
color: c.dim,
|
|
34432
|
+
borderRadius: 12,
|
|
34433
|
+
border: `1px dashed ${c.border}`
|
|
34434
|
+
},
|
|
34435
|
+
children: effectiveIdleCaption
|
|
34436
|
+
}
|
|
34437
|
+
)
|
|
34438
|
+
) : mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
34334
34439
|
ReelStage,
|
|
34335
34440
|
{
|
|
34336
34441
|
pool,
|
|
34337
34442
|
winner,
|
|
34338
34443
|
spinning,
|
|
34339
34444
|
reelY,
|
|
34445
|
+
displayY: displayReelY,
|
|
34446
|
+
cssTransition: !physicsDriven,
|
|
34447
|
+
tapeRef: reelTapeRef,
|
|
34340
34448
|
reelVisibleRows,
|
|
34341
34449
|
reelRowHeight,
|
|
34342
34450
|
size,
|
|
@@ -34345,7 +34453,7 @@ function RandomPlayerPicker({
|
|
|
34345
34453
|
dark,
|
|
34346
34454
|
isIdle,
|
|
34347
34455
|
placeholderMode,
|
|
34348
|
-
placeholderCaption,
|
|
34456
|
+
placeholderCaption: effectiveIdleCaption,
|
|
34349
34457
|
placeholderDiscColor,
|
|
34350
34458
|
spinDuration,
|
|
34351
34459
|
intensityCfg,
|
|
@@ -34402,6 +34510,7 @@ function RandomPlayerPicker({
|
|
|
34402
34510
|
/* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
|
|
34403
34511
|
"svg",
|
|
34404
34512
|
{
|
|
34513
|
+
ref: wheelSvgRef,
|
|
34405
34514
|
width: size,
|
|
34406
34515
|
height: size,
|
|
34407
34516
|
"aria-hidden": "true",
|
|
@@ -34410,8 +34519,8 @@ function RandomPlayerPicker({
|
|
|
34410
34519
|
top: 36,
|
|
34411
34520
|
left: 0,
|
|
34412
34521
|
transformOrigin: `${cx}px ${cy}px`,
|
|
34413
|
-
transform: `rotate(${
|
|
34414
|
-
transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
|
|
34522
|
+
transform: `rotate(${displayRotation}deg)`,
|
|
34523
|
+
transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
|
|
34415
34524
|
filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
|
|
34416
34525
|
},
|
|
34417
34526
|
children: [
|
|
@@ -34564,7 +34673,7 @@ function RandomPlayerPicker({
|
|
|
34564
34673
|
pointerEvents: "none",
|
|
34565
34674
|
zIndex: 3
|
|
34566
34675
|
},
|
|
34567
|
-
children:
|
|
34676
|
+
children: effectiveIdleCaption
|
|
34568
34677
|
}
|
|
34569
34678
|
),
|
|
34570
34679
|
particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
@@ -40249,8 +40358,10 @@ function TeamGenerator({
|
|
|
40249
40358
|
minPlayersPerTeam = 2,
|
|
40250
40359
|
maxPlayers = Number.POSITIVE_INFINITY,
|
|
40251
40360
|
defaultNames = [],
|
|
40361
|
+
defaultTeams,
|
|
40252
40362
|
onNamesChange,
|
|
40253
40363
|
onGenerate,
|
|
40364
|
+
onTeamsChange,
|
|
40254
40365
|
onTeamCountChange,
|
|
40255
40366
|
labels,
|
|
40256
40367
|
ariaLabel = "Team generator",
|
|
@@ -40265,12 +40376,17 @@ function TeamGenerator({
|
|
|
40265
40376
|
const [names, setNames] = (0, import_react76.useState)(defaultNames);
|
|
40266
40377
|
const [input, setInput] = (0, import_react76.useState)("");
|
|
40267
40378
|
const [teamCount, setTeamCount] = (0, import_react76.useState)(initialCount);
|
|
40268
|
-
const [teams, setTeams] = (0, import_react76.useState)(
|
|
40379
|
+
const [teams, setTeams] = (0, import_react76.useState)(
|
|
40380
|
+
defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
|
|
40381
|
+
);
|
|
40269
40382
|
const inputRef = (0, import_react76.useRef)(null);
|
|
40270
40383
|
const minNames = teamCount * minPlayersPerTeam;
|
|
40271
40384
|
(0, import_react76.useEffect)(() => {
|
|
40272
40385
|
onNamesChange?.(names);
|
|
40273
40386
|
}, [names, onNamesChange]);
|
|
40387
|
+
(0, import_react76.useEffect)(() => {
|
|
40388
|
+
onTeamsChange?.(teams);
|
|
40389
|
+
}, [teams, onTeamsChange]);
|
|
40274
40390
|
const addName = (0, import_react76.useCallback)(() => {
|
|
40275
40391
|
const trimmed = input.trim();
|
|
40276
40392
|
if (!trimmed) return;
|