@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/index.d.mts
CHANGED
|
@@ -5015,8 +5015,14 @@ interface RandomPlayerPickerProps {
|
|
|
5015
5015
|
darkMode?: boolean;
|
|
5016
5016
|
/** Wheel diameter in px. Default 380. */
|
|
5017
5017
|
size?: number;
|
|
5018
|
-
/**
|
|
5019
|
-
|
|
5018
|
+
/**
|
|
5019
|
+
* Called when a winner is locked in. `info.lastStanding` is true for
|
|
5020
|
+
* the automatic final pick of a draft — the survivor promoted when
|
|
5021
|
+
* pops drain the pool to one.
|
|
5022
|
+
*/
|
|
5023
|
+
onPick?: (winner: RandomPlayerParticipant, history: RandomPlayerParticipant[], info?: {
|
|
5024
|
+
lastStanding: boolean;
|
|
5025
|
+
}) => void;
|
|
5020
5026
|
/** Fired the moment a spin begins (before the animation). */
|
|
5021
5027
|
onSpinStart?: () => void;
|
|
5022
5028
|
/**
|
|
@@ -5050,14 +5056,37 @@ interface RandomPlayerPickerProps {
|
|
|
5050
5056
|
renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
|
|
5051
5057
|
/** Imperative handle. `spinRef.current?.()` triggers a spin. */
|
|
5052
5058
|
spinRef?: MutableRefObject<(() => void) | null>;
|
|
5059
|
+
/**
|
|
5060
|
+
* Imperative reset handle. `resetRef.current?.()` restores the pool to
|
|
5061
|
+
* the full `participants` list and clears history/winner — the escape
|
|
5062
|
+
* hatch for consumers that hide the built-in controls
|
|
5063
|
+
* (`showControls={false}`) but still need a "reset draft" affordance
|
|
5064
|
+
* once draft-mode pops drain the pool.
|
|
5065
|
+
*/
|
|
5066
|
+
resetRef?: MutableRefObject<(() => void) | null>;
|
|
5067
|
+
/**
|
|
5068
|
+
* Fired whenever the internal pool changes (mount, structural sync,
|
|
5069
|
+
* draft-mode pop, spin-again flush, reset). Draft-mode consumers use
|
|
5070
|
+
* this to mirror the live pool — e.g. to disable an external spin
|
|
5071
|
+
* trigger when the pool drains below 2 instead of presenting a dead
|
|
5072
|
+
* button, and to offer a reset once the draft completes.
|
|
5073
|
+
*/
|
|
5074
|
+
onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
|
|
5075
|
+
/**
|
|
5076
|
+
* Caption shown in place of `placeholderCaption` when draft-mode pops
|
|
5077
|
+
* drain the pool below 2 while the roster itself still has 2+ members.
|
|
5078
|
+
* Default: "Draft complete — reset to spin again".
|
|
5079
|
+
*/
|
|
5080
|
+
draftCompleteCaption?: string;
|
|
5053
5081
|
/**
|
|
5054
5082
|
* Visual intensity preset. Default "normal".
|
|
5055
|
-
* - "subtle" / "normal":
|
|
5056
|
-
*
|
|
5057
|
-
*
|
|
5058
|
-
*
|
|
5059
|
-
*
|
|
5060
|
-
* particles/glow/shake on reveal.
|
|
5083
|
+
* - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
|
|
5084
|
+
* - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
|
|
5085
|
+
* exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
|
|
5086
|
+
* visibly winding down through the FULL duration and lands exactly
|
|
5087
|
+
* at the end, +12 extra revolutions, ticks fired on actual
|
|
5088
|
+
* wedge/row crossings, plus more particles/glow/shake on reveal.
|
|
5089
|
+
* Falls back to a 250ms CSS settle under prefers-reduced-motion.
|
|
5061
5090
|
*/
|
|
5062
5091
|
intensity?: RandomPlayerIntensity;
|
|
5063
5092
|
/**
|
|
@@ -5100,7 +5129,7 @@ interface RandomPlayerPickerProps {
|
|
|
5100
5129
|
/** Accessible label for the wheel root. */
|
|
5101
5130
|
ariaLabel?: string;
|
|
5102
5131
|
}
|
|
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;
|
|
5132
|
+
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
5133
|
|
|
5105
5134
|
type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
|
|
5106
5135
|
type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
|
|
@@ -5666,10 +5695,20 @@ interface TeamGeneratorProps {
|
|
|
5666
5695
|
maxPlayers?: number;
|
|
5667
5696
|
/** Initial roster. Uncontrolled — the component manages names internally. */
|
|
5668
5697
|
defaultNames?: string[];
|
|
5698
|
+
/** Initial generated teams (e.g. restored from a saved session).
|
|
5699
|
+
* Uncontrolled — the component takes over after mount. An empty array
|
|
5700
|
+
* is treated as "no teams". */
|
|
5701
|
+
defaultTeams?: string[][];
|
|
5669
5702
|
/** Called whenever the name pool changes. */
|
|
5670
5703
|
onNamesChange?: (names: string[]) => void;
|
|
5671
5704
|
/** Called whenever teams are (re)generated. */
|
|
5672
5705
|
onGenerate?: (teams: string[][]) => void;
|
|
5706
|
+
/** Called whenever the generated teams change for ANY reason — a
|
|
5707
|
+
* (re)generation, or invalidation to `null` when a player is removed,
|
|
5708
|
+
* the pool is cleared, or the team count changes. Unlike `onGenerate`,
|
|
5709
|
+
* this mirrors the full lifecycle of the on-screen split so hosts can
|
|
5710
|
+
* persist it and restore via `defaultTeams`. */
|
|
5711
|
+
onTeamsChange?: (teams: string[][] | null) => void;
|
|
5673
5712
|
/** Called whenever the team count selection changes. */
|
|
5674
5713
|
onTeamCountChange?: (count: number) => void;
|
|
5675
5714
|
/** Localized / customized labels. */
|
|
@@ -5682,7 +5721,7 @@ interface TeamGeneratorProps {
|
|
|
5682
5721
|
* CSV / TSV roster from a spreadsheet. Default `true`. */
|
|
5683
5722
|
enableImport?: boolean;
|
|
5684
5723
|
}
|
|
5685
|
-
declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
|
|
5724
|
+
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
5725
|
declare namespace TeamGenerator {
|
|
5687
5726
|
var displayName: string;
|
|
5688
5727
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5015,8 +5015,14 @@ interface RandomPlayerPickerProps {
|
|
|
5015
5015
|
darkMode?: boolean;
|
|
5016
5016
|
/** Wheel diameter in px. Default 380. */
|
|
5017
5017
|
size?: number;
|
|
5018
|
-
/**
|
|
5019
|
-
|
|
5018
|
+
/**
|
|
5019
|
+
* Called when a winner is locked in. `info.lastStanding` is true for
|
|
5020
|
+
* the automatic final pick of a draft — the survivor promoted when
|
|
5021
|
+
* pops drain the pool to one.
|
|
5022
|
+
*/
|
|
5023
|
+
onPick?: (winner: RandomPlayerParticipant, history: RandomPlayerParticipant[], info?: {
|
|
5024
|
+
lastStanding: boolean;
|
|
5025
|
+
}) => void;
|
|
5020
5026
|
/** Fired the moment a spin begins (before the animation). */
|
|
5021
5027
|
onSpinStart?: () => void;
|
|
5022
5028
|
/**
|
|
@@ -5050,14 +5056,37 @@ interface RandomPlayerPickerProps {
|
|
|
5050
5056
|
renderWinner?: (winner: RandomPlayerParticipant) => ReactNode;
|
|
5051
5057
|
/** Imperative handle. `spinRef.current?.()` triggers a spin. */
|
|
5052
5058
|
spinRef?: MutableRefObject<(() => void) | null>;
|
|
5059
|
+
/**
|
|
5060
|
+
* Imperative reset handle. `resetRef.current?.()` restores the pool to
|
|
5061
|
+
* the full `participants` list and clears history/winner — the escape
|
|
5062
|
+
* hatch for consumers that hide the built-in controls
|
|
5063
|
+
* (`showControls={false}`) but still need a "reset draft" affordance
|
|
5064
|
+
* once draft-mode pops drain the pool.
|
|
5065
|
+
*/
|
|
5066
|
+
resetRef?: MutableRefObject<(() => void) | null>;
|
|
5067
|
+
/**
|
|
5068
|
+
* Fired whenever the internal pool changes (mount, structural sync,
|
|
5069
|
+
* draft-mode pop, spin-again flush, reset). Draft-mode consumers use
|
|
5070
|
+
* this to mirror the live pool — e.g. to disable an external spin
|
|
5071
|
+
* trigger when the pool drains below 2 instead of presenting a dead
|
|
5072
|
+
* button, and to offer a reset once the draft completes.
|
|
5073
|
+
*/
|
|
5074
|
+
onPoolChange?: (pool: RandomPlayerParticipant[]) => void;
|
|
5075
|
+
/**
|
|
5076
|
+
* Caption shown in place of `placeholderCaption` when draft-mode pops
|
|
5077
|
+
* drain the pool below 2 while the roster itself still has 2+ members.
|
|
5078
|
+
* Default: "Draft complete — reset to spin again".
|
|
5079
|
+
*/
|
|
5080
|
+
draftCompleteCaption?: string;
|
|
5053
5081
|
/**
|
|
5054
5082
|
* Visual intensity preset. Default "normal".
|
|
5055
|
-
* - "subtle" / "normal":
|
|
5056
|
-
*
|
|
5057
|
-
*
|
|
5058
|
-
*
|
|
5059
|
-
*
|
|
5060
|
-
* particles/glow/shake on reveal.
|
|
5083
|
+
* - "subtle" / "normal": shorter spins, standard CSS ease-out curve.
|
|
5084
|
+
* - "dramatic": physics-driven spin — 2.5× duration, rAF-driven
|
|
5085
|
+
* exponential velocity decay (v ∝ e^(-4t)) that keeps the wheel
|
|
5086
|
+
* visibly winding down through the FULL duration and lands exactly
|
|
5087
|
+
* at the end, +12 extra revolutions, ticks fired on actual
|
|
5088
|
+
* wedge/row crossings, plus more particles/glow/shake on reveal.
|
|
5089
|
+
* Falls back to a 250ms CSS settle under prefers-reduced-motion.
|
|
5061
5090
|
*/
|
|
5062
5091
|
intensity?: RandomPlayerIntensity;
|
|
5063
5092
|
/**
|
|
@@ -5100,7 +5129,7 @@ interface RandomPlayerPickerProps {
|
|
|
5100
5129
|
/** Accessible label for the wheel root. */
|
|
5101
5130
|
ariaLabel?: string;
|
|
5102
5131
|
}
|
|
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;
|
|
5132
|
+
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
5133
|
|
|
5105
5134
|
type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
|
|
5106
5135
|
type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
|
|
@@ -5666,10 +5695,20 @@ interface TeamGeneratorProps {
|
|
|
5666
5695
|
maxPlayers?: number;
|
|
5667
5696
|
/** Initial roster. Uncontrolled — the component manages names internally. */
|
|
5668
5697
|
defaultNames?: string[];
|
|
5698
|
+
/** Initial generated teams (e.g. restored from a saved session).
|
|
5699
|
+
* Uncontrolled — the component takes over after mount. An empty array
|
|
5700
|
+
* is treated as "no teams". */
|
|
5701
|
+
defaultTeams?: string[][];
|
|
5669
5702
|
/** Called whenever the name pool changes. */
|
|
5670
5703
|
onNamesChange?: (names: string[]) => void;
|
|
5671
5704
|
/** Called whenever teams are (re)generated. */
|
|
5672
5705
|
onGenerate?: (teams: string[][]) => void;
|
|
5706
|
+
/** Called whenever the generated teams change for ANY reason — a
|
|
5707
|
+
* (re)generation, or invalidation to `null` when a player is removed,
|
|
5708
|
+
* the pool is cleared, or the team count changes. Unlike `onGenerate`,
|
|
5709
|
+
* this mirrors the full lifecycle of the on-screen split so hosts can
|
|
5710
|
+
* persist it and restore via `defaultTeams`. */
|
|
5711
|
+
onTeamsChange?: (teams: string[][] | null) => void;
|
|
5673
5712
|
/** Called whenever the team count selection changes. */
|
|
5674
5713
|
onTeamCountChange?: (count: number) => void;
|
|
5675
5714
|
/** Localized / customized labels. */
|
|
@@ -5682,7 +5721,7 @@ interface TeamGeneratorProps {
|
|
|
5682
5721
|
* CSV / TSV roster from a spreadsheet. Default `true`. */
|
|
5683
5722
|
enableImport?: boolean;
|
|
5684
5723
|
}
|
|
5685
|
-
declare function TeamGenerator({ theme, darkMode, teamCountOptions, defaultTeamCount, minPlayersPerTeam, maxPlayers, defaultNames, onNamesChange, onGenerate, onTeamCountChange, labels, ariaLabel, className, enableImport, }: TeamGeneratorProps): react_jsx_runtime.JSX.Element;
|
|
5724
|
+
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
5725
|
declare namespace TeamGenerator {
|
|
5687
5726
|
var displayName: string;
|
|
5688
5727
|
}
|
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);
|
|
@@ -34056,7 +34079,20 @@ function RandomPlayerPicker({
|
|
|
34056
34079
|
}
|
|
34057
34080
|
}
|
|
34058
34081
|
const activeN = activePool.length;
|
|
34059
|
-
if (activeN < 2)
|
|
34082
|
+
if (activeN < 2) {
|
|
34083
|
+
if (removeWinnersRef.current && activePool.length === 1 && history.length > 0) {
|
|
34084
|
+
const survivor = activePool[0];
|
|
34085
|
+
if (survivor) {
|
|
34086
|
+
const promoted = [survivor, ...history].slice(0, 20);
|
|
34087
|
+
setPool([]);
|
|
34088
|
+
setWinner(survivor);
|
|
34089
|
+
setRevealKey((k) => k + 1);
|
|
34090
|
+
setHistory(promoted);
|
|
34091
|
+
onPick?.(survivor, promoted, { lastStanding: true });
|
|
34092
|
+
}
|
|
34093
|
+
}
|
|
34094
|
+
return;
|
|
34095
|
+
}
|
|
34060
34096
|
const activeAnglePerWedge = 2 * Math.PI / activeN;
|
|
34061
34097
|
const activeRotation = rotation;
|
|
34062
34098
|
const activeReelY = reelY;
|
|
@@ -34095,7 +34131,56 @@ function RandomPlayerPicker({
|
|
|
34095
34131
|
} else {
|
|
34096
34132
|
setRotation(finalRotation);
|
|
34097
34133
|
}
|
|
34098
|
-
|
|
34134
|
+
const fireTick = () => {
|
|
34135
|
+
onTick?.();
|
|
34136
|
+
const ta = tickAudioRef.current;
|
|
34137
|
+
if (ta) {
|
|
34138
|
+
try {
|
|
34139
|
+
ta.currentTime = 0;
|
|
34140
|
+
void ta.play().catch(() => {
|
|
34141
|
+
});
|
|
34142
|
+
} catch {
|
|
34143
|
+
}
|
|
34144
|
+
}
|
|
34145
|
+
};
|
|
34146
|
+
if (physicsDriven) {
|
|
34147
|
+
const from = mode === "reel" ? activeReelY : activeRotation;
|
|
34148
|
+
const to = mode === "reel" ? finalReelY : finalRotation;
|
|
34149
|
+
const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
|
|
34150
|
+
if (mode === "reel") visualReelYRef.current = from;
|
|
34151
|
+
else visualRotationRef.current = from;
|
|
34152
|
+
let crossings = 0;
|
|
34153
|
+
const t0 = performance.now();
|
|
34154
|
+
const step = (now) => {
|
|
34155
|
+
const t = Math.min(1, (now - t0) / effectiveDuration);
|
|
34156
|
+
const value = from + (to - from) * physicsRampDown(t);
|
|
34157
|
+
if (mode === "reel") {
|
|
34158
|
+
visualReelYRef.current = value;
|
|
34159
|
+
const el = reelTapeRef.current;
|
|
34160
|
+
if (el) {
|
|
34161
|
+
el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
|
|
34162
|
+
}
|
|
34163
|
+
} else {
|
|
34164
|
+
visualRotationRef.current = value;
|
|
34165
|
+
const el = wheelSvgRef.current;
|
|
34166
|
+
if (el) el.style.transform = `rotate(${value}deg)`;
|
|
34167
|
+
}
|
|
34168
|
+
const crossed = Math.floor(Math.abs(value - from) / crossUnit);
|
|
34169
|
+
if (crossed > crossings) {
|
|
34170
|
+
crossings = crossed;
|
|
34171
|
+
fireTick();
|
|
34172
|
+
}
|
|
34173
|
+
if (t < 1) {
|
|
34174
|
+
rafRef.current = requestAnimationFrame(step);
|
|
34175
|
+
} else {
|
|
34176
|
+
rafRef.current = null;
|
|
34177
|
+
visualRotationRef.current = null;
|
|
34178
|
+
visualReelYRef.current = null;
|
|
34179
|
+
}
|
|
34180
|
+
};
|
|
34181
|
+
rafRef.current = requestAnimationFrame(step);
|
|
34182
|
+
}
|
|
34183
|
+
if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
|
|
34099
34184
|
const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
|
|
34100
34185
|
const totalDelta = deltaMod + revolutions * 360;
|
|
34101
34186
|
const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
|
|
@@ -34113,16 +34198,7 @@ function RandomPlayerPicker({
|
|
|
34113
34198
|
const eased = 1 - Math.pow(1 - progress, 3);
|
|
34114
34199
|
const interval = startInterval + (endInterval - startInterval) * eased;
|
|
34115
34200
|
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
|
-
}
|
|
34201
|
+
fireTick();
|
|
34126
34202
|
elapsed += interval;
|
|
34127
34203
|
schedule();
|
|
34128
34204
|
}, interval);
|
|
@@ -34181,8 +34257,19 @@ function RandomPlayerPicker({
|
|
|
34181
34257
|
popTimer.current = null;
|
|
34182
34258
|
pendingPopIdxRef.current = null;
|
|
34183
34259
|
if (!removeWinnersRef.current) return;
|
|
34184
|
-
|
|
34185
|
-
|
|
34260
|
+
const survivors = activePool.filter((_, i) => i !== finalWinnerIdx);
|
|
34261
|
+
const survivor = survivors.length === 1 ? survivors[0] : void 0;
|
|
34262
|
+
if (survivor) {
|
|
34263
|
+
const promoted = [survivor, ...nextHistory].slice(0, 20);
|
|
34264
|
+
setPool([]);
|
|
34265
|
+
setWinner(survivor);
|
|
34266
|
+
setRevealKey((k) => k + 1);
|
|
34267
|
+
setHistory(promoted);
|
|
34268
|
+
onPick?.(survivor, promoted, { lastStanding: true });
|
|
34269
|
+
} else {
|
|
34270
|
+
setPool(survivors);
|
|
34271
|
+
setWinner(null);
|
|
34272
|
+
}
|
|
34186
34273
|
setRotation(0);
|
|
34187
34274
|
setReelY(0);
|
|
34188
34275
|
}, 2800);
|
|
@@ -34205,6 +34292,7 @@ function RandomPlayerPicker({
|
|
|
34205
34292
|
maxRevolutions,
|
|
34206
34293
|
history,
|
|
34207
34294
|
reducedMotion,
|
|
34295
|
+
physicsDriven,
|
|
34208
34296
|
enableHaptics,
|
|
34209
34297
|
intensityCfg.particles,
|
|
34210
34298
|
intensityCfg.durationMult,
|
|
@@ -34216,12 +34304,20 @@ function RandomPlayerPicker({
|
|
|
34216
34304
|
if (spinRef && spinRef.current === spin) spinRef.current = null;
|
|
34217
34305
|
};
|
|
34218
34306
|
}, [spin, spinRef]);
|
|
34307
|
+
const onPoolChangeRef = (0, import_react71.useRef)(onPoolChange);
|
|
34308
|
+
(0, import_react71.useEffect)(() => {
|
|
34309
|
+
onPoolChangeRef.current = onPoolChange;
|
|
34310
|
+
});
|
|
34311
|
+
(0, import_react71.useEffect)(() => {
|
|
34312
|
+
onPoolChangeRef.current?.(pool);
|
|
34313
|
+
}, [pool]);
|
|
34219
34314
|
(0, import_react71.useEffect)(
|
|
34220
34315
|
() => () => {
|
|
34221
34316
|
if (winnerTimer.current) clearTimeout(winnerTimer.current);
|
|
34222
34317
|
if (particleTimer.current) clearTimeout(particleTimer.current);
|
|
34223
34318
|
if (popTimer.current) clearTimeout(popTimer.current);
|
|
34224
34319
|
if (tickInterval.current) clearTimeout(tickInterval.current);
|
|
34320
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
34225
34321
|
},
|
|
34226
34322
|
[]
|
|
34227
34323
|
);
|
|
@@ -34251,6 +34347,12 @@ function RandomPlayerPicker({
|
|
|
34251
34347
|
clearTimeout(popTimer.current);
|
|
34252
34348
|
popTimer.current = null;
|
|
34253
34349
|
}
|
|
34350
|
+
if (rafRef.current) {
|
|
34351
|
+
cancelAnimationFrame(rafRef.current);
|
|
34352
|
+
rafRef.current = null;
|
|
34353
|
+
}
|
|
34354
|
+
visualRotationRef.current = null;
|
|
34355
|
+
visualReelYRef.current = null;
|
|
34254
34356
|
pendingPopIdxRef.current = null;
|
|
34255
34357
|
setSpinning(false);
|
|
34256
34358
|
setPool(participants);
|
|
@@ -34260,9 +34362,17 @@ function RandomPlayerPicker({
|
|
|
34260
34362
|
setReelY(0);
|
|
34261
34363
|
setParticles([]);
|
|
34262
34364
|
}, [participants]);
|
|
34365
|
+
(0, import_react71.useEffect)(() => {
|
|
34366
|
+
if (resetRef) resetRef.current = reset;
|
|
34367
|
+
return () => {
|
|
34368
|
+
if (resetRef && resetRef.current === reset) resetRef.current = null;
|
|
34369
|
+
};
|
|
34370
|
+
}, [reset, resetRef]);
|
|
34263
34371
|
const isIdle = n < 2;
|
|
34372
|
+
const drainedByDraft = isIdle && participants.length >= 2;
|
|
34373
|
+
const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
|
|
34264
34374
|
const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
|
|
34265
|
-
if (isIdle && placeholderMode === "message") {
|
|
34375
|
+
if (isIdle && !drainedByDraft && placeholderMode === "message") {
|
|
34266
34376
|
return /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
34267
34377
|
"div",
|
|
34268
34378
|
{
|
|
@@ -34284,6 +34394,8 @@ function RandomPlayerPicker({
|
|
|
34284
34394
|
const shakeKeyframe = `rppShake_${intensity}`;
|
|
34285
34395
|
const glowKeyframe = `rppGlow_${intensity}`;
|
|
34286
34396
|
const pulseKeyframe = `rppPulse_${intensity}`;
|
|
34397
|
+
const displayRotation = visualRotationRef.current ?? rotation;
|
|
34398
|
+
const displayReelY = visualReelYRef.current ?? reelY;
|
|
34287
34399
|
return /* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
|
|
34288
34400
|
"div",
|
|
34289
34401
|
{
|
|
@@ -34330,13 +34442,33 @@ function RandomPlayerPicker({
|
|
|
34330
34442
|
[data-rpp-anim] { animation: none !important; }
|
|
34331
34443
|
}
|
|
34332
34444
|
` }),
|
|
34333
|
-
|
|
34445
|
+
isIdle && placeholderMode === "message" ? (
|
|
34446
|
+
/* Drained draft in "message" mode — compact box in the wheel's
|
|
34447
|
+
place; the winner card / stats / controls / history below stay
|
|
34448
|
+
mounted so the endgame isn't a dead end. */
|
|
34449
|
+
/* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
34450
|
+
"div",
|
|
34451
|
+
{
|
|
34452
|
+
style: {
|
|
34453
|
+
padding: "2rem 1rem",
|
|
34454
|
+
textAlign: "center",
|
|
34455
|
+
color: c.dim,
|
|
34456
|
+
borderRadius: 12,
|
|
34457
|
+
border: `1px dashed ${c.border}`
|
|
34458
|
+
},
|
|
34459
|
+
children: effectiveIdleCaption
|
|
34460
|
+
}
|
|
34461
|
+
)
|
|
34462
|
+
) : mode === "reel" ? /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
34334
34463
|
ReelStage,
|
|
34335
34464
|
{
|
|
34336
34465
|
pool,
|
|
34337
34466
|
winner,
|
|
34338
34467
|
spinning,
|
|
34339
34468
|
reelY,
|
|
34469
|
+
displayY: displayReelY,
|
|
34470
|
+
cssTransition: !physicsDriven,
|
|
34471
|
+
tapeRef: reelTapeRef,
|
|
34340
34472
|
reelVisibleRows,
|
|
34341
34473
|
reelRowHeight,
|
|
34342
34474
|
size,
|
|
@@ -34345,7 +34477,7 @@ function RandomPlayerPicker({
|
|
|
34345
34477
|
dark,
|
|
34346
34478
|
isIdle,
|
|
34347
34479
|
placeholderMode,
|
|
34348
|
-
placeholderCaption,
|
|
34480
|
+
placeholderCaption: effectiveIdleCaption,
|
|
34349
34481
|
placeholderDiscColor,
|
|
34350
34482
|
spinDuration,
|
|
34351
34483
|
intensityCfg,
|
|
@@ -34402,6 +34534,7 @@ function RandomPlayerPicker({
|
|
|
34402
34534
|
/* @__PURE__ */ (0, import_jsx_runtime117.jsxs)(
|
|
34403
34535
|
"svg",
|
|
34404
34536
|
{
|
|
34537
|
+
ref: wheelSvgRef,
|
|
34405
34538
|
width: size,
|
|
34406
34539
|
height: size,
|
|
34407
34540
|
"aria-hidden": "true",
|
|
@@ -34410,8 +34543,8 @@ function RandomPlayerPicker({
|
|
|
34410
34543
|
top: 36,
|
|
34411
34544
|
left: 0,
|
|
34412
34545
|
transformOrigin: `${cx}px ${cy}px`,
|
|
34413
|
-
transform: `rotate(${
|
|
34414
|
-
transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
|
|
34546
|
+
transform: `rotate(${displayRotation}deg)`,
|
|
34547
|
+
transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
|
|
34415
34548
|
filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
|
|
34416
34549
|
},
|
|
34417
34550
|
children: [
|
|
@@ -34564,7 +34697,7 @@ function RandomPlayerPicker({
|
|
|
34564
34697
|
pointerEvents: "none",
|
|
34565
34698
|
zIndex: 3
|
|
34566
34699
|
},
|
|
34567
|
-
children:
|
|
34700
|
+
children: effectiveIdleCaption
|
|
34568
34701
|
}
|
|
34569
34702
|
),
|
|
34570
34703
|
particles.map((p) => /* @__PURE__ */ (0, import_jsx_runtime117.jsx)(
|
|
@@ -38738,6 +38871,33 @@ function GameTimer({
|
|
|
38738
38871
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
38739
38872
|
};
|
|
38740
38873
|
}, [running, tick]);
|
|
38874
|
+
(0, import_react74.useEffect)(() => {
|
|
38875
|
+
if (!running || mode !== "countdown") return;
|
|
38876
|
+
let timeoutId = null;
|
|
38877
|
+
const disarm = () => {
|
|
38878
|
+
if (timeoutId !== null) {
|
|
38879
|
+
clearTimeout(timeoutId);
|
|
38880
|
+
timeoutId = null;
|
|
38881
|
+
}
|
|
38882
|
+
};
|
|
38883
|
+
const arm = () => {
|
|
38884
|
+
disarm();
|
|
38885
|
+
const remainingUntilDone = durationMs - elapsedRef.current;
|
|
38886
|
+
if (remainingUntilDone > 0) {
|
|
38887
|
+
timeoutId = setTimeout(tick, remainingUntilDone + 50);
|
|
38888
|
+
}
|
|
38889
|
+
};
|
|
38890
|
+
const onVisibility = () => {
|
|
38891
|
+
if (document.hidden) arm();
|
|
38892
|
+
else disarm();
|
|
38893
|
+
};
|
|
38894
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
38895
|
+
if (document.hidden) arm();
|
|
38896
|
+
return () => {
|
|
38897
|
+
disarm();
|
|
38898
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
38899
|
+
};
|
|
38900
|
+
}, [running, mode, durationMs, tick]);
|
|
38741
38901
|
const start = (0, import_react74.useCallback)(() => {
|
|
38742
38902
|
if (completed && mode === "countdown") return;
|
|
38743
38903
|
setRunning(true);
|
|
@@ -40249,8 +40409,10 @@ function TeamGenerator({
|
|
|
40249
40409
|
minPlayersPerTeam = 2,
|
|
40250
40410
|
maxPlayers = Number.POSITIVE_INFINITY,
|
|
40251
40411
|
defaultNames = [],
|
|
40412
|
+
defaultTeams,
|
|
40252
40413
|
onNamesChange,
|
|
40253
40414
|
onGenerate,
|
|
40415
|
+
onTeamsChange,
|
|
40254
40416
|
onTeamCountChange,
|
|
40255
40417
|
labels,
|
|
40256
40418
|
ariaLabel = "Team generator",
|
|
@@ -40265,12 +40427,17 @@ function TeamGenerator({
|
|
|
40265
40427
|
const [names, setNames] = (0, import_react76.useState)(defaultNames);
|
|
40266
40428
|
const [input, setInput] = (0, import_react76.useState)("");
|
|
40267
40429
|
const [teamCount, setTeamCount] = (0, import_react76.useState)(initialCount);
|
|
40268
|
-
const [teams, setTeams] = (0, import_react76.useState)(
|
|
40430
|
+
const [teams, setTeams] = (0, import_react76.useState)(
|
|
40431
|
+
defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
|
|
40432
|
+
);
|
|
40269
40433
|
const inputRef = (0, import_react76.useRef)(null);
|
|
40270
40434
|
const minNames = teamCount * minPlayersPerTeam;
|
|
40271
40435
|
(0, import_react76.useEffect)(() => {
|
|
40272
40436
|
onNamesChange?.(names);
|
|
40273
40437
|
}, [names, onNamesChange]);
|
|
40438
|
+
(0, import_react76.useEffect)(() => {
|
|
40439
|
+
onTeamsChange?.(teams);
|
|
40440
|
+
}, [teams, onTeamsChange]);
|
|
40274
40441
|
const addName = (0, import_react76.useCallback)(() => {
|
|
40275
40442
|
const trimmed = input.trim();
|
|
40276
40443
|
if (!trimmed) return;
|