@digilogiclabs/saas-factory-ui 2.3.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.mjs CHANGED
@@ -30665,7 +30665,6 @@ function DiceRoller({
30665
30665
  }) {
30666
30666
  const clampedCount = Math.max(1, Math.min(MAX_COUNT, count2));
30667
30667
  const dark = useDarkMode(darkMode);
30668
- const reducedMotion = usePrefersReducedMotion();
30669
30668
  const [rolling, setRolling] = useState70(false);
30670
30669
  const [results, setResults] = useState70(null);
30671
30670
  const [history, setHistory] = useState70([]);
@@ -30802,6 +30801,14 @@ function DiceRoller({
30802
30801
  []
30803
30802
  );
30804
30803
  useEffect54(() => {
30804
+ if (timersRef.current.length > 0) {
30805
+ timersRef.current.forEach((t) => clearTimeout(t));
30806
+ timersRef.current = [];
30807
+ }
30808
+ if (rollingRef.current) {
30809
+ rollingRef.current = false;
30810
+ setRolling(false);
30811
+ }
30805
30812
  const rawW = arenaRef.current?.offsetWidth ?? 0;
30806
30813
  const rawH = arenaRef.current?.offsetHeight ?? 0;
30807
30814
  const w = rawW > 20 ? rawW : arena.w > 20 ? arena.w : 400;
@@ -30958,7 +30965,6 @@ function DiceRoller({
30958
30965
  defaultPositions,
30959
30966
  effectiveSize,
30960
30967
  half,
30961
- reducedMotion,
30962
30968
  onRoll,
30963
30969
  writeCubeRotation,
30964
30970
  writeDiePosition,
@@ -32984,6 +32990,9 @@ function CoinFlipper({
32984
32990
  }, [flipping, onFlip, historyMax]);
32985
32991
  useEffect56(() => {
32986
32992
  if (flipRef) flipRef.current = flip;
32993
+ return () => {
32994
+ if (flipRef && flipRef.current === flip) flipRef.current = null;
32995
+ };
32987
32996
  }, [flip, flipRef]);
32988
32997
  useEffect56(() => {
32989
32998
  if (autoFlip) {
@@ -33491,6 +33500,11 @@ var WEDGE_PALETTE = [
33491
33500
  ];
33492
33501
  var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
33493
33502
  var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
33503
+ var PHYSICS_DECAY_K = 4;
33504
+ function physicsRampDown(t) {
33505
+ return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
33506
+ }
33507
+ var REEL_LEAD_IN = 32;
33494
33508
  var INTENSITY_CONFIG = {
33495
33509
  subtle: {
33496
33510
  particles: 14,
@@ -33562,6 +33576,9 @@ function ReelStage({
33562
33576
  winner,
33563
33577
  spinning,
33564
33578
  reelY,
33579
+ displayY,
33580
+ cssTransition,
33581
+ tapeRef,
33565
33582
  reelVisibleRows,
33566
33583
  reelRowHeight,
33567
33584
  size,
@@ -33585,17 +33602,16 @@ function ReelStage({
33585
33602
  const stripHeight = reelVisibleRows * reelRowHeight;
33586
33603
  const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
33587
33604
  const P = pool.length;
33588
- const leadIn = 32;
33589
33605
  const tapeRows = Math.max(
33590
- reelVisibleRows + leadIn + 1,
33591
- Math.ceil((reelY + stripHeight) / reelRowHeight) + leadIn + 1
33606
+ reelVisibleRows + REEL_LEAD_IN + 1,
33607
+ Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
33592
33608
  );
33593
33609
  const tape = Array.from({ length: tapeRows }, (_, i) => {
33594
- const poolIdx = P === 0 ? 0 : ((i - leadIn) % P + P) % P;
33610
+ const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
33595
33611
  return pool[poolIdx];
33596
33612
  });
33597
- const effectiveY = reelY + leadIn * reelRowHeight;
33598
- const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33613
+ const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
33614
+ const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33599
33615
  return /* @__PURE__ */ jsxs89(
33600
33616
  "div",
33601
33617
  {
@@ -33640,6 +33656,7 @@ function ReelStage({
33640
33656
  ) : /* @__PURE__ */ jsx117(
33641
33657
  "div",
33642
33658
  {
33659
+ ref: tapeRef,
33643
33660
  "aria-hidden": "true",
33644
33661
  style: {
33645
33662
  position: "absolute",
@@ -33855,6 +33872,9 @@ function RandomPlayerPicker({
33855
33872
  showStats = true,
33856
33873
  renderWinner,
33857
33874
  spinRef,
33875
+ resetRef,
33876
+ onPoolChange,
33877
+ draftCompleteCaption = "Draft complete \u2014 reset to spin again",
33858
33878
  intensity = "normal",
33859
33879
  enableHaptics = false,
33860
33880
  tickSoundUrl,
@@ -33869,6 +33889,7 @@ function RandomPlayerPicker({
33869
33889
  const dark = useDarkMode(darkMode);
33870
33890
  const reducedMotion = usePrefersReducedMotion();
33871
33891
  const intensityCfg = INTENSITY_CONFIG[intensity];
33892
+ const physicsDriven = intensity === "dramatic" && !reducedMotion;
33872
33893
  const [pool, setPool] = useState73(participants);
33873
33894
  const [rotation, setRotation] = useState73(0);
33874
33895
  const [reelY, setReelY] = useState73(0);
@@ -33881,10 +33902,17 @@ function RandomPlayerPicker({
33881
33902
  const particleTimer = useRef42(null);
33882
33903
  const popTimer = useRef42(null);
33883
33904
  const tickInterval = useRef42(null);
33905
+ const pendingPopIdxRef = useRef42(null);
33906
+ const removeWinnersRef = useRef42(removeWinners);
33884
33907
  const tickAudioRef = useRef42(null);
33885
33908
  const winAudioRef = useRef42(null);
33886
33909
  const containerRef = useRef42(null);
33887
33910
  const prevIdsRef = useRef42("");
33911
+ const wheelSvgRef = useRef42(null);
33912
+ const reelTapeRef = useRef42(null);
33913
+ const rafRef = useRef42(null);
33914
+ const visualRotationRef = useRef42(null);
33915
+ const visualReelYRef = useRef42(null);
33888
33916
  useEffect57(() => {
33889
33917
  if (typeof window === "undefined" || !tickSoundUrl) {
33890
33918
  tickAudioRef.current = null;
@@ -33919,10 +33947,33 @@ function RandomPlayerPicker({
33919
33947
  winAudioRef.current = null;
33920
33948
  };
33921
33949
  }, [winSoundUrl, soundVolume]);
33950
+ useEffect57(() => {
33951
+ removeWinnersRef.current = removeWinners;
33952
+ }, [removeWinners]);
33922
33953
  useEffect57(() => {
33923
33954
  const ids = participants.map((p) => p.id).join("|");
33924
33955
  const structuralChange = ids !== prevIdsRef.current;
33925
33956
  if (structuralChange) {
33957
+ if (winnerTimer.current) {
33958
+ clearTimeout(winnerTimer.current);
33959
+ winnerTimer.current = null;
33960
+ }
33961
+ if (tickInterval.current) {
33962
+ clearTimeout(tickInterval.current);
33963
+ tickInterval.current = null;
33964
+ }
33965
+ if (popTimer.current) {
33966
+ clearTimeout(popTimer.current);
33967
+ popTimer.current = null;
33968
+ }
33969
+ if (rafRef.current) {
33970
+ cancelAnimationFrame(rafRef.current);
33971
+ rafRef.current = null;
33972
+ }
33973
+ visualRotationRef.current = null;
33974
+ visualReelYRef.current = null;
33975
+ pendingPopIdxRef.current = null;
33976
+ setSpinning(false);
33926
33977
  setPool(participants);
33927
33978
  setHistory([]);
33928
33979
  setWinner(null);
@@ -33979,33 +34030,50 @@ function RandomPlayerPicker({
33979
34030
  [dark]
33980
34031
  );
33981
34032
  const spin = useCallback47(() => {
33982
- if (spinning || n < 2) return;
34033
+ if (spinning) return;
34034
+ let activePool = pool;
34035
+ if (popTimer.current) {
34036
+ clearTimeout(popTimer.current);
34037
+ popTimer.current = null;
34038
+ const pendingIdx = pendingPopIdxRef.current;
34039
+ pendingPopIdxRef.current = null;
34040
+ if (pendingIdx != null && pendingIdx >= 0 && pendingIdx < pool.length && removeWinnersRef.current) {
34041
+ activePool = pool.filter((_, i) => i !== pendingIdx);
34042
+ setPool(activePool);
34043
+ setWinner(null);
34044
+ }
34045
+ }
34046
+ const activeN = activePool.length;
34047
+ if (activeN < 2) return;
34048
+ const activeAnglePerWedge = 2 * Math.PI / activeN;
34049
+ const activeRotation = rotation;
34050
+ const activeReelY = reelY;
33983
34051
  onSpinStart?.();
33984
- const winnerIdx = weightedIndex(pool);
34052
+ const winnerIdx = weightedIndex(activePool);
33985
34053
  const minRev = minRevolutions + intensityCfg.revsBoost;
33986
34054
  const maxRev = maxRevolutions + intensityCfg.revsBoost;
33987
34055
  const minFullRevs = Math.ceil(minRev);
33988
34056
  const maxFullRevs = Math.max(minFullRevs, Math.floor(maxRev));
33989
34057
  const revolutions = minFullRevs + Math.floor(Math.random() * (maxFullRevs - minFullRevs + 1));
33990
- const wedgeMidRad = winnerIdx * anglePerWedge + anglePerWedge / 2;
34058
+ const wedgeMidRad = winnerIdx * activeAnglePerWedge + activeAnglePerWedge / 2;
33991
34059
  const wedgeMidDeg = wedgeMidRad * 180 / Math.PI;
33992
- const wheelWobble = (Math.random() - 0.5) * (anglePerWedge * 180 / Math.PI) * 0.6;
33993
- const currentMod = (rotation % 360 + 360) % 360;
34060
+ const wheelWobble = (Math.random() - 0.5) * (activeAnglePerWedge * 180 / Math.PI) * 0.6;
34061
+ const currentMod = (activeRotation % 360 + 360) % 360;
33994
34062
  const targetMod = ((-wedgeMidDeg + wheelWobble) % 360 + 360) % 360;
33995
34063
  let deltaMod = targetMod - currentMod;
33996
34064
  if (deltaMod < 0) deltaMod += 360;
33997
- const finalRotation = rotation + deltaMod + revolutions * 360;
34065
+ const finalRotation = activeRotation + deltaMod + revolutions * 360;
33998
34066
  const centerOffsetRows = (reelVisibleRows - 1) / 2;
33999
34067
  const centerOffsetPx = centerOffsetRows * reelRowHeight;
34000
34068
  const currentCenterRow = Math.round(
34001
- (reelY + centerOffsetPx) / reelRowHeight
34069
+ (activeReelY + centerOffsetPx) / reelRowHeight
34002
34070
  );
34003
- const currentCenterMod = mod(currentCenterRow, n);
34004
- const targetRowOffset = (winnerIdx - currentCenterMod + n) % n;
34005
- const reelRevRows = Math.round(revolutions * n);
34006
- const finalReelY = reelY + (reelRevRows + targetRowOffset) * reelRowHeight;
34007
- const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, n) : wheelIndexAtPointer(finalRotation, n);
34008
- const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < pool.length ? settledWinnerIdx : winnerIdx;
34071
+ const currentCenterMod = mod(currentCenterRow, activeN);
34072
+ const targetRowOffset = (winnerIdx - currentCenterMod + activeN) % activeN;
34073
+ const reelRevRows = Math.round(revolutions * activeN);
34074
+ const finalReelY = activeReelY + (reelRevRows + targetRowOffset) * reelRowHeight;
34075
+ const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, activeN) : wheelIndexAtPointer(finalRotation, activeN);
34076
+ const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < activePool.length ? settledWinnerIdx : winnerIdx;
34009
34077
  const effectiveDuration = reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult);
34010
34078
  setSpinning(true);
34011
34079
  setWinner(null);
@@ -34015,10 +34083,59 @@ function RandomPlayerPicker({
34015
34083
  } else {
34016
34084
  setRotation(finalRotation);
34017
34085
  }
34018
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
34086
+ const fireTick = () => {
34087
+ onTick?.();
34088
+ const ta = tickAudioRef.current;
34089
+ if (ta) {
34090
+ try {
34091
+ ta.currentTime = 0;
34092
+ void ta.play().catch(() => {
34093
+ });
34094
+ } catch {
34095
+ }
34096
+ }
34097
+ };
34098
+ if (physicsDriven) {
34099
+ const from = mode === "reel" ? activeReelY : activeRotation;
34100
+ const to = mode === "reel" ? finalReelY : finalRotation;
34101
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
34102
+ if (mode === "reel") visualReelYRef.current = from;
34103
+ else visualRotationRef.current = from;
34104
+ let crossings = 0;
34105
+ const t0 = performance.now();
34106
+ const step = (now) => {
34107
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34108
+ const value = from + (to - from) * physicsRampDown(t);
34109
+ if (mode === "reel") {
34110
+ visualReelYRef.current = value;
34111
+ const el = reelTapeRef.current;
34112
+ if (el) {
34113
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34114
+ }
34115
+ } else {
34116
+ visualRotationRef.current = value;
34117
+ const el = wheelSvgRef.current;
34118
+ if (el) el.style.transform = `rotate(${value}deg)`;
34119
+ }
34120
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34121
+ if (crossed > crossings) {
34122
+ crossings = crossed;
34123
+ fireTick();
34124
+ }
34125
+ if (t < 1) {
34126
+ rafRef.current = requestAnimationFrame(step);
34127
+ } else {
34128
+ rafRef.current = null;
34129
+ visualRotationRef.current = null;
34130
+ visualReelYRef.current = null;
34131
+ }
34132
+ };
34133
+ rafRef.current = requestAnimationFrame(step);
34134
+ }
34135
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
34019
34136
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
34020
34137
  const totalDelta = deltaMod + revolutions * 360;
34021
- const wedgeDeg = anglePerWedge * 180 / Math.PI;
34138
+ const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
34022
34139
  return Math.max(
34023
34140
  1,
34024
34141
  Math.round(totalDelta / Math.max(wedgeDeg, 1))
@@ -34033,16 +34150,7 @@ function RandomPlayerPicker({
34033
34150
  const eased = 1 - Math.pow(1 - progress, 3);
34034
34151
  const interval = startInterval + (endInterval - startInterval) * eased;
34035
34152
  tickInterval.current = setTimeout(() => {
34036
- onTick?.();
34037
- const ta = tickAudioRef.current;
34038
- if (ta) {
34039
- try {
34040
- ta.currentTime = 0;
34041
- void ta.play().catch(() => {
34042
- });
34043
- } catch {
34044
- }
34045
- }
34153
+ fireTick();
34046
34154
  elapsed += interval;
34047
34155
  schedule();
34048
34156
  }, interval);
@@ -34050,7 +34158,7 @@ function RandomPlayerPicker({
34050
34158
  schedule();
34051
34159
  }
34052
34160
  winnerTimer.current = setTimeout(() => {
34053
- const picked = pool[finalWinnerIdx];
34161
+ const picked = activePool[finalWinnerIdx];
34054
34162
  if (!picked) {
34055
34163
  setSpinning(false);
34056
34164
  return;
@@ -34096,9 +34204,13 @@ function RandomPlayerPicker({
34096
34204
  particleTimer.current = setTimeout(() => setParticles([]), 2400);
34097
34205
  }
34098
34206
  if (removeWinners) {
34207
+ pendingPopIdxRef.current = finalWinnerIdx;
34099
34208
  popTimer.current = setTimeout(() => {
34209
+ popTimer.current = null;
34210
+ pendingPopIdxRef.current = null;
34211
+ if (!removeWinnersRef.current) return;
34100
34212
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34101
- setWinner(null);
34213
+ if (activePool.length - 1 >= 2) setWinner(null);
34102
34214
  setRotation(0);
34103
34215
  setReelY(0);
34104
34216
  }, 2800);
@@ -34107,8 +34219,6 @@ function RandomPlayerPicker({
34107
34219
  }, [
34108
34220
  pool,
34109
34221
  spinning,
34110
- n,
34111
- anglePerWedge,
34112
34222
  rotation,
34113
34223
  reelY,
34114
34224
  mode,
@@ -34123,6 +34233,7 @@ function RandomPlayerPicker({
34123
34233
  maxRevolutions,
34124
34234
  history,
34125
34235
  reducedMotion,
34236
+ physicsDriven,
34126
34237
  enableHaptics,
34127
34238
  intensityCfg.particles,
34128
34239
  intensityCfg.durationMult,
@@ -34134,16 +34245,25 @@ function RandomPlayerPicker({
34134
34245
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34135
34246
  };
34136
34247
  }, [spin, spinRef]);
34248
+ const onPoolChangeRef = useRef42(onPoolChange);
34249
+ useEffect57(() => {
34250
+ onPoolChangeRef.current = onPoolChange;
34251
+ });
34252
+ useEffect57(() => {
34253
+ onPoolChangeRef.current?.(pool);
34254
+ }, [pool]);
34137
34255
  useEffect57(
34138
34256
  () => () => {
34139
34257
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34140
34258
  if (particleTimer.current) clearTimeout(particleTimer.current);
34141
34259
  if (popTimer.current) clearTimeout(popTimer.current);
34142
34260
  if (tickInterval.current) clearTimeout(tickInterval.current);
34261
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34143
34262
  },
34144
34263
  []
34145
34264
  );
34146
34265
  useEffect57(() => {
34266
+ if (trigger === "external") return;
34147
34267
  const el = containerRef.current;
34148
34268
  if (!el) return;
34149
34269
  const h = (e) => {
@@ -34154,8 +34274,28 @@ function RandomPlayerPicker({
34154
34274
  };
34155
34275
  el.addEventListener("keydown", h);
34156
34276
  return () => el.removeEventListener("keydown", h);
34157
- }, [spin]);
34277
+ }, [spin, trigger]);
34158
34278
  const reset = useCallback47(() => {
34279
+ if (winnerTimer.current) {
34280
+ clearTimeout(winnerTimer.current);
34281
+ winnerTimer.current = null;
34282
+ }
34283
+ if (tickInterval.current) {
34284
+ clearTimeout(tickInterval.current);
34285
+ tickInterval.current = null;
34286
+ }
34287
+ if (popTimer.current) {
34288
+ clearTimeout(popTimer.current);
34289
+ popTimer.current = null;
34290
+ }
34291
+ if (rafRef.current) {
34292
+ cancelAnimationFrame(rafRef.current);
34293
+ rafRef.current = null;
34294
+ }
34295
+ visualRotationRef.current = null;
34296
+ visualReelYRef.current = null;
34297
+ pendingPopIdxRef.current = null;
34298
+ setSpinning(false);
34159
34299
  setPool(participants);
34160
34300
  setHistory([]);
34161
34301
  setWinner(null);
@@ -34163,9 +34303,17 @@ function RandomPlayerPicker({
34163
34303
  setReelY(0);
34164
34304
  setParticles([]);
34165
34305
  }, [participants]);
34306
+ useEffect57(() => {
34307
+ if (resetRef) resetRef.current = reset;
34308
+ return () => {
34309
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34310
+ };
34311
+ }, [reset, resetRef]);
34166
34312
  const isIdle = n < 2;
34313
+ const drainedByDraft = isIdle && participants.length >= 2;
34314
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34167
34315
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34168
- if (isIdle && placeholderMode === "message") {
34316
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34169
34317
  return /* @__PURE__ */ jsx117(
34170
34318
  "div",
34171
34319
  {
@@ -34187,6 +34335,8 @@ function RandomPlayerPicker({
34187
34335
  const shakeKeyframe = `rppShake_${intensity}`;
34188
34336
  const glowKeyframe = `rppGlow_${intensity}`;
34189
34337
  const pulseKeyframe = `rppPulse_${intensity}`;
34338
+ const displayRotation = visualRotationRef.current ?? rotation;
34339
+ const displayReelY = visualReelYRef.current ?? reelY;
34190
34340
  return /* @__PURE__ */ jsxs89(
34191
34341
  "div",
34192
34342
  {
@@ -34233,13 +34383,33 @@ function RandomPlayerPicker({
34233
34383
  [data-rpp-anim] { animation: none !important; }
34234
34384
  }
34235
34385
  ` }),
34236
- mode === "reel" ? /* @__PURE__ */ jsx117(
34386
+ isIdle && placeholderMode === "message" ? (
34387
+ /* Drained draft in "message" mode — compact box in the wheel's
34388
+ place; the winner card / stats / controls / history below stay
34389
+ mounted so the endgame isn't a dead end. */
34390
+ /* @__PURE__ */ jsx117(
34391
+ "div",
34392
+ {
34393
+ style: {
34394
+ padding: "2rem 1rem",
34395
+ textAlign: "center",
34396
+ color: c.dim,
34397
+ borderRadius: 12,
34398
+ border: `1px dashed ${c.border}`
34399
+ },
34400
+ children: effectiveIdleCaption
34401
+ }
34402
+ )
34403
+ ) : mode === "reel" ? /* @__PURE__ */ jsx117(
34237
34404
  ReelStage,
34238
34405
  {
34239
34406
  pool,
34240
34407
  winner,
34241
34408
  spinning,
34242
34409
  reelY,
34410
+ displayY: displayReelY,
34411
+ cssTransition: !physicsDriven,
34412
+ tapeRef: reelTapeRef,
34243
34413
  reelVisibleRows,
34244
34414
  reelRowHeight,
34245
34415
  size,
@@ -34248,7 +34418,7 @@ function RandomPlayerPicker({
34248
34418
  dark,
34249
34419
  isIdle,
34250
34420
  placeholderMode,
34251
- placeholderCaption,
34421
+ placeholderCaption: effectiveIdleCaption,
34252
34422
  placeholderDiscColor,
34253
34423
  spinDuration,
34254
34424
  intensityCfg,
@@ -34305,6 +34475,7 @@ function RandomPlayerPicker({
34305
34475
  /* @__PURE__ */ jsxs89(
34306
34476
  "svg",
34307
34477
  {
34478
+ ref: wheelSvgRef,
34308
34479
  width: size,
34309
34480
  height: size,
34310
34481
  "aria-hidden": "true",
@@ -34313,8 +34484,8 @@ function RandomPlayerPicker({
34313
34484
  top: 36,
34314
34485
  left: 0,
34315
34486
  transformOrigin: `${cx}px ${cy}px`,
34316
- transform: `rotate(${rotation}deg)`,
34317
- transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34487
+ transform: `rotate(${displayRotation}deg)`,
34488
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34318
34489
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34319
34490
  },
34320
34491
  children: [
@@ -34467,7 +34638,7 @@ function RandomPlayerPicker({
34467
34638
  pointerEvents: "none",
34468
34639
  zIndex: 3
34469
34640
  },
34470
- children: placeholderCaption
34641
+ children: effectiveIdleCaption
34471
34642
  }
34472
34643
  ),
34473
34644
  particles.map((p) => /* @__PURE__ */ jsx117(
@@ -38628,27 +38799,28 @@ function GameTimer({
38628
38799
  const now = performance.now();
38629
38800
  const delta = now - lastTickRef.current;
38630
38801
  lastTickRef.current = now;
38631
- setElapsedMs((prev) => {
38632
- const next = prev + delta;
38633
- if (mode === "countdown" && next >= durationMs) {
38634
- setRunning(false);
38635
- setCompleted(true);
38636
- onComplete?.();
38637
- return durationMs;
38802
+ const next = elapsedRef.current + delta;
38803
+ if (mode === "countdown" && next >= durationMs) {
38804
+ elapsedRef.current = durationMs;
38805
+ setElapsedMs(durationMs);
38806
+ setRunning(false);
38807
+ setCompleted(true);
38808
+ onComplete?.();
38809
+ return;
38810
+ }
38811
+ if (mode === "countdown") {
38812
+ const secLeft = Math.ceil((durationMs - next) / 1e3);
38813
+ if (secLeft <= warningAt && !warningFiredRef.current) {
38814
+ warningFiredRef.current = true;
38815
+ onWarning?.();
38638
38816
  }
38639
- if (mode === "countdown") {
38640
- const secLeft = Math.ceil((durationMs - next) / 1e3);
38641
- if (secLeft <= warningAt && !warningFiredRef.current) {
38642
- warningFiredRef.current = true;
38643
- onWarning?.();
38644
- }
38645
- if (secLeft <= dangerAt && !dangerFiredRef.current) {
38646
- dangerFiredRef.current = true;
38647
- onDanger?.();
38648
- }
38817
+ if (secLeft <= dangerAt && !dangerFiredRef.current) {
38818
+ dangerFiredRef.current = true;
38819
+ onDanger?.();
38649
38820
  }
38650
- return next;
38651
- });
38821
+ }
38822
+ elapsedRef.current = next;
38823
+ setElapsedMs(next);
38652
38824
  rafRef.current = requestAnimationFrame(tick);
38653
38825
  }, [mode, durationMs, warningAt, dangerAt, onComplete, onWarning, onDanger]);
38654
38826
  useEffect61(() => {
@@ -38669,6 +38841,7 @@ function GameTimer({
38669
38841
  }, []);
38670
38842
  const reset = useCallback51(() => {
38671
38843
  setRunning(false);
38844
+ elapsedRef.current = 0;
38672
38845
  setElapsedMs(0);
38673
38846
  setCompleted(false);
38674
38847
  setLaps([]);
@@ -40182,8 +40355,10 @@ function TeamGenerator({
40182
40355
  minPlayersPerTeam = 2,
40183
40356
  maxPlayers = Number.POSITIVE_INFINITY,
40184
40357
  defaultNames = [],
40358
+ defaultTeams,
40185
40359
  onNamesChange,
40186
40360
  onGenerate,
40361
+ onTeamsChange,
40187
40362
  onTeamCountChange,
40188
40363
  labels,
40189
40364
  ariaLabel = "Team generator",
@@ -40198,12 +40373,17 @@ function TeamGenerator({
40198
40373
  const [names, setNames] = useState79(defaultNames);
40199
40374
  const [input, setInput] = useState79("");
40200
40375
  const [teamCount, setTeamCount] = useState79(initialCount);
40201
- const [teams, setTeams] = useState79(null);
40376
+ const [teams, setTeams] = useState79(
40377
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40378
+ );
40202
40379
  const inputRef = useRef48(null);
40203
40380
  const minNames = teamCount * minPlayersPerTeam;
40204
40381
  useEffect63(() => {
40205
40382
  onNamesChange?.(names);
40206
40383
  }, [names, onNamesChange]);
40384
+ useEffect63(() => {
40385
+ onTeamsChange?.(teams);
40386
+ }, [teams, onTeamsChange]);
40207
40387
  const addName = useCallback53(() => {
40208
40388
  const trimmed = input.trim();
40209
40389
  if (!trimmed) return;
@@ -40830,6 +41010,8 @@ function RoundRobinScheduler({
40830
41010
  theme: theme5 = "slate",
40831
41011
  darkMode,
40832
41012
  minTeams = 3,
41013
+ maxTeams,
41014
+ nameMaxLength,
40833
41015
  defaultTeams = [],
40834
41016
  resumeRounds,
40835
41017
  onTeamsChange,
@@ -40852,39 +41034,41 @@ function RoundRobinScheduler({
40852
41034
  useEffect64(() => {
40853
41035
  onTeamsChange?.(teams);
40854
41036
  }, [teams, onTeamsChange]);
40855
- const addTeams = useCallback54((raw) => {
40856
- const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40857
- if (names.length === 0) return;
40858
- setTeams((prev) => {
40859
- const existing = new Set(prev.map((t) => t.toLowerCase()));
40860
- const next = [...prev];
40861
- for (const name of names) {
40862
- if (!existing.has(name.toLowerCase())) {
40863
- existing.add(name.toLowerCase());
40864
- next.push(name);
40865
- }
40866
- }
40867
- return next;
40868
- });
40869
- setInput("");
40870
- setRounds([]);
40871
- }, []);
40872
- const importTeams = useCallback54((incoming) => {
40873
- if (incoming.length === 0) return;
40874
- setTeams((prev) => {
41037
+ const mergeNames = useCallback54(
41038
+ (prev, incoming) => {
40875
41039
  const existing = new Set(prev.map((t) => t.toLowerCase()));
40876
41040
  const next = [...prev];
40877
- for (const name of incoming) {
41041
+ for (const rawName of incoming) {
41042
+ if (maxTeams != null && next.length >= maxTeams) break;
41043
+ const name = nameMaxLength != null ? rawName.slice(0, nameMaxLength) : rawName;
40878
41044
  const key = name.toLowerCase();
40879
- if (!existing.has(key)) {
41045
+ if (name.length > 0 && !existing.has(key)) {
40880
41046
  existing.add(key);
40881
41047
  next.push(name);
40882
41048
  }
40883
41049
  }
40884
41050
  return next;
40885
- });
40886
- setRounds([]);
40887
- }, []);
41051
+ },
41052
+ [maxTeams, nameMaxLength]
41053
+ );
41054
+ const addTeams = useCallback54(
41055
+ (raw) => {
41056
+ const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
41057
+ if (names.length === 0) return;
41058
+ setTeams((prev) => mergeNames(prev, names));
41059
+ setInput("");
41060
+ setRounds([]);
41061
+ },
41062
+ [mergeNames]
41063
+ );
41064
+ const importTeams = useCallback54(
41065
+ (incoming) => {
41066
+ if (incoming.length === 0) return;
41067
+ setTeams((prev) => mergeNames(prev, incoming));
41068
+ setRounds([]);
41069
+ },
41070
+ [mergeNames]
41071
+ );
40888
41072
  const handleKeyDown = useCallback54(
40889
41073
  (e) => {
40890
41074
  if (e.key === "Enter") {
@@ -41378,6 +41562,8 @@ function Scoreboard({
41378
41562
  onTeamsChange,
41379
41563
  onRoundAdvance,
41380
41564
  onNewGame,
41565
+ onReset,
41566
+ nameMaxLength,
41381
41567
  labels,
41382
41568
  ariaLabel = "Scoreboard",
41383
41569
  className
@@ -41452,7 +41638,8 @@ function Scoreboard({
41452
41638
  setRound(1);
41453
41639
  setHistory([]);
41454
41640
  setHistoryOpen(false);
41455
- }, []);
41641
+ onReset?.();
41642
+ }, [onReset]);
41456
41643
  const newGame = useCallback55(() => {
41457
41644
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41458
41645
  setRound(1);
@@ -41674,6 +41861,7 @@ function Scoreboard({
41674
41861
  {
41675
41862
  type: "text",
41676
41863
  value: team.name,
41864
+ maxLength: nameMaxLength,
41677
41865
  onChange: (e) => updateName(index, e.target.value),
41678
41866
  style: nameInputStyle,
41679
41867
  "aria-label": `Team ${index + 1} name`