@digilogiclabs/saas-factory-ui 2.3.0 → 2.4.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.
@@ -30226,10 +30226,10 @@ function TiltCard({
30226
30226
  // src/components/ui/dice-roller/index.tsx
30227
30227
  import {
30228
30228
  useCallback as useCallback41,
30229
- useEffect as useEffect52,
30229
+ useEffect as useEffect51,
30230
30230
  useMemo as useMemo26,
30231
30231
  useRef as useRef40,
30232
- useState as useState67
30232
+ useState as useState66
30233
30233
  } from "react";
30234
30234
 
30235
30235
  // src/hooks/web/use-dark-mode.ts
@@ -30279,21 +30279,6 @@ function useDarkMode(forced) {
30279
30279
  return dark;
30280
30280
  }
30281
30281
 
30282
- // src/hooks/web/use-prefers-reduced-motion.ts
30283
- import { useEffect as useEffect51, useState as useState66 } from "react";
30284
- function usePrefersReducedMotion() {
30285
- const [reduced, setReduced] = useState66(false);
30286
- useEffect51(() => {
30287
- if (typeof window === "undefined" || !window.matchMedia) return;
30288
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
30289
- setReduced(mq.matches);
30290
- const read = () => setReduced(mq.matches);
30291
- mq.addEventListener("change", read);
30292
- return () => mq.removeEventListener("change", read);
30293
- }, []);
30294
- return reduced;
30295
- }
30296
-
30297
30282
  // src/components/ui/dice-roller/index.tsx
30298
30283
  import { Fragment as Fragment30, jsx as jsx113, jsxs as jsxs86 } from "react/jsx-runtime";
30299
30284
  var PRESETS = {
@@ -30512,8 +30497,8 @@ function simulateDice(arenaW, arenaH, diceSize, starts, numDice) {
30512
30497
  return frames;
30513
30498
  }
30514
30499
  function useArenaBounds(ref) {
30515
- const [bounds, setBounds] = useState67({ w: 400, h: 260 });
30516
- useEffect52(() => {
30500
+ const [bounds, setBounds] = useState66({ w: 400, h: 260 });
30501
+ useEffect51(() => {
30517
30502
  if (!ref.current) return;
30518
30503
  const el = ref.current;
30519
30504
  const update = () => setBounds({ w: el.offsetWidth, h: el.offsetHeight });
@@ -30568,10 +30553,9 @@ function DiceRoller({
30568
30553
  }) {
30569
30554
  const clampedCount = Math.max(1, Math.min(MAX_COUNT, count2));
30570
30555
  const dark = useDarkMode(darkMode);
30571
- const reducedMotion = usePrefersReducedMotion();
30572
- const [rolling, setRolling] = useState67(false);
30573
- const [results, setResults] = useState67(null);
30574
- const [history, setHistory] = useState67([]);
30556
+ const [rolling, setRolling] = useState66(false);
30557
+ const [results, setResults] = useState66(null);
30558
+ const [history, setHistory] = useState66([]);
30575
30559
  const dieWrapperRefs = useRef40([]);
30576
30560
  const cubeRefs = useRef40([]);
30577
30561
  const shadowRefs = useRef40([]);
@@ -30704,7 +30688,15 @@ function DiceRoller({
30704
30688
  },
30705
30689
  []
30706
30690
  );
30707
- useEffect52(() => {
30691
+ useEffect51(() => {
30692
+ if (timersRef.current.length > 0) {
30693
+ timersRef.current.forEach((t) => clearTimeout(t));
30694
+ timersRef.current = [];
30695
+ }
30696
+ if (rollingRef.current) {
30697
+ rollingRef.current = false;
30698
+ setRolling(false);
30699
+ }
30708
30700
  const rawW = arenaRef.current?.offsetWidth ?? 0;
30709
30701
  const rawH = arenaRef.current?.offsetHeight ?? 0;
30710
30702
  const w = rawW > 20 ? rawW : arena.w > 20 ? arena.w : 400;
@@ -30737,7 +30729,7 @@ function DiceRoller({
30737
30729
  defaultPositions,
30738
30730
  half
30739
30731
  ]);
30740
- useEffect52(() => {
30732
+ useEffect51(() => {
30741
30733
  setHistory([]);
30742
30734
  }, [sides]);
30743
30735
  const roll = useCallback41(() => {
@@ -30861,25 +30853,24 @@ function DiceRoller({
30861
30853
  defaultPositions,
30862
30854
  effectiveSize,
30863
30855
  half,
30864
- reducedMotion,
30865
30856
  onRoll,
30866
30857
  writeCubeRotation,
30867
30858
  writeDiePosition,
30868
30859
  showHistory,
30869
30860
  historyMax
30870
30861
  ]);
30871
- useEffect52(() => {
30862
+ useEffect51(() => {
30872
30863
  if (rollRef) rollRef.current = roll;
30873
30864
  return () => {
30874
30865
  if (rollRef && rollRef.current === roll) rollRef.current = null;
30875
30866
  };
30876
30867
  }, [roll, rollRef]);
30877
- useEffect52(() => {
30868
+ useEffect51(() => {
30878
30869
  if (!autoRoll) return;
30879
30870
  const t = setTimeout(roll, 300);
30880
30871
  return () => clearTimeout(t);
30881
30872
  }, []);
30882
- useEffect52(
30873
+ useEffect51(
30883
30874
  () => () => {
30884
30875
  timersRef.current.forEach((t) => clearTimeout(t));
30885
30876
  timersRef.current = [];
@@ -31180,10 +31171,10 @@ DiceRoller.displayName = "DiceRoller";
31180
31171
 
31181
31172
  // src/components/ui/bracket-generator/index.tsx
31182
31173
  import {
31183
- useState as useState68,
31174
+ useState as useState67,
31184
31175
  useCallback as useCallback42,
31185
31176
  useRef as useRef41,
31186
- useEffect as useEffect53,
31177
+ useEffect as useEffect52,
31187
31178
  useMemo as useMemo27,
31188
31179
  useLayoutEffect as useLayoutEffect2
31189
31180
  } from "react";
@@ -31406,7 +31397,7 @@ function computeLayout(rounds, firstRoundCount) {
31406
31397
  };
31407
31398
  }
31408
31399
  function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31409
- const [transform, setTransform] = useState68({
31400
+ const [transform, setTransform] = useState67({
31410
31401
  x: 0,
31411
31402
  y: 0,
31412
31403
  scale: 1
@@ -31426,7 +31417,7 @@ function usePanZoom(viewportRef, contentSize, minZoom, maxZoom) {
31426
31417
  },
31427
31418
  [contentSize]
31428
31419
  );
31429
- useEffect53(() => {
31420
+ useEffect52(() => {
31430
31421
  const el = viewportRef.current;
31431
31422
  if (!el) return;
31432
31423
  const onWheel = (e) => {
@@ -31657,12 +31648,12 @@ function BracketGenerator({
31657
31648
  ariaLabel = "Tournament bracket"
31658
31649
  }) {
31659
31650
  const dark = useDarkMode(darkMode);
31660
- const [bracketState, setBracketState] = useState68({
31651
+ const [bracketState, setBracketState] = useState67({
31661
31652
  matches: [],
31662
31653
  rounds: 0,
31663
31654
  champion: null
31664
31655
  });
31665
- const [simulating, setSimulating] = useState68(false);
31656
+ const [simulating, setSimulating] = useState67(false);
31666
31657
  const autoRef = useRef41(null);
31667
31658
  const viewportRef = useRef41(null);
31668
31659
  const effectiveParticipants = useMemo27(() => {
@@ -31717,7 +31708,7 @@ function BracketGenerator({
31717
31708
  minZoom,
31718
31709
  maxZoom
31719
31710
  );
31720
- const [miniMapHidden, setMiniMapHidden] = useState68(false);
31711
+ const [miniMapHidden, setMiniMapHidden] = useState67(false);
31721
31712
  const miniMapDragRef = useRef41(false);
31722
31713
  const resumeRef = useRef41(resumeMatches);
31723
31714
  const hydrateFromResume = useCallback42(() => {
@@ -31851,7 +31842,7 @@ function BracketGenerator({
31851
31842
  []
31852
31843
  );
31853
31844
  const prevBracketStateRef = useRef41(bracketState);
31854
- useEffect53(() => {
31845
+ useEffect52(() => {
31855
31846
  const prev = prevBracketStateRef.current;
31856
31847
  const next = bracketState;
31857
31848
  if (prev === next) return;
@@ -31875,7 +31866,7 @@ function BracketGenerator({
31875
31866
  [effectiveParticipants, seedingStrategy]
31876
31867
  );
31877
31868
  const seededKeyRef = useRef41(null);
31878
- useEffect53(() => {
31869
+ useEffect52(() => {
31879
31870
  if (seededKeyRef.current === structuralKey) {
31880
31871
  setBracketState((prev) => {
31881
31872
  if (!prev.matches.length) return prev;
@@ -31906,7 +31897,7 @@ function BracketGenerator({
31906
31897
  seedBracket();
31907
31898
  }
31908
31899
  }, [structuralKey, effectiveParticipants, hydrateFromResume, seedBracket]);
31909
- useEffect53(() => {
31900
+ useEffect52(() => {
31910
31901
  return () => {
31911
31902
  if (autoRef.current) clearTimeout(autoRef.current);
31912
31903
  };
@@ -31917,7 +31908,7 @@ function BracketGenerator({
31917
31908
  return () => clearTimeout(timer);
31918
31909
  }
31919
31910
  }, [contentSize.w]);
31920
- useEffect53(() => {
31911
+ useEffect52(() => {
31921
31912
  if (autoSimulate && bracketState.matches.length && !bracketState.champion) {
31922
31913
  const timer = setTimeout(autoPlay, 500);
31923
31914
  return () => clearTimeout(timer);
@@ -32638,10 +32629,10 @@ function BracketGenerator({
32638
32629
  // src/components/ui/coin-flipper/index.tsx
32639
32630
  import {
32640
32631
  useCallback as useCallback43,
32641
- useEffect as useEffect54,
32632
+ useEffect as useEffect53,
32642
32633
  useMemo as useMemo28,
32643
32634
  useRef as useRef42,
32644
- useState as useState69
32635
+ useState as useState68
32645
32636
  } from "react";
32646
32637
  import { jsx as jsx115, jsxs as jsxs88 } from "react/jsx-runtime";
32647
32638
  var PRESETS3 = {
@@ -32826,10 +32817,10 @@ function CoinFlipper({
32826
32817
  }) {
32827
32818
  const dark = useDarkMode(darkMode);
32828
32819
  const tc = typeof theme5 === "string" ? PRESETS3[theme5] || PRESETS3.gold : theme5;
32829
- const [result, setResult] = useState69(null);
32830
- const [flipping, setFlipping] = useState69(false);
32831
- const [history, setHistory] = useState69([]);
32832
- const [stats, setStats] = useState69({ heads: 0, tails: 0 });
32820
+ const [result, setResult] = useState68(null);
32821
+ const [flipping, setFlipping] = useState68(false);
32822
+ const [history, setHistory] = useState68([]);
32823
+ const [stats, setStats] = useState68({ heads: 0, tails: 0 });
32833
32824
  const flipIdRef = useRef42(0);
32834
32825
  const coinRef = useRef42(null);
32835
32826
  const shadowRef = useRef42(null);
@@ -32885,16 +32876,19 @@ function CoinFlipper({
32885
32876
  }, frames.length * stepMs);
32886
32877
  timersRef.current.push(settleTimer);
32887
32878
  }, [flipping, onFlip, historyMax]);
32888
- useEffect54(() => {
32879
+ useEffect53(() => {
32889
32880
  if (flipRef) flipRef.current = flip;
32881
+ return () => {
32882
+ if (flipRef && flipRef.current === flip) flipRef.current = null;
32883
+ };
32890
32884
  }, [flip, flipRef]);
32891
- useEffect54(() => {
32885
+ useEffect53(() => {
32892
32886
  if (autoFlip) {
32893
32887
  const timer = setTimeout(flip, 300);
32894
32888
  return () => clearTimeout(timer);
32895
32889
  }
32896
32890
  }, []);
32897
- useEffect54(() => {
32891
+ useEffect53(() => {
32898
32892
  return () => {
32899
32893
  timersRef.current.forEach(clearTimeout);
32900
32894
  };
@@ -33337,6 +33331,23 @@ import {
33337
33331
  useRef as useRef43,
33338
33332
  useState as useState70
33339
33333
  } from "react";
33334
+
33335
+ // src/hooks/web/use-prefers-reduced-motion.ts
33336
+ import { useEffect as useEffect54, useState as useState69 } from "react";
33337
+ function usePrefersReducedMotion() {
33338
+ const [reduced, setReduced] = useState69(false);
33339
+ useEffect54(() => {
33340
+ if (typeof window === "undefined" || !window.matchMedia) return;
33341
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
33342
+ setReduced(mq.matches);
33343
+ const read = () => setReduced(mq.matches);
33344
+ mq.addEventListener("change", read);
33345
+ return () => mq.removeEventListener("change", read);
33346
+ }, []);
33347
+ return reduced;
33348
+ }
33349
+
33350
+ // src/components/ui/random-player-picker/index.tsx
33340
33351
  import { Fragment as Fragment31, jsx as jsx116, jsxs as jsxs89 } from "react/jsx-runtime";
33341
33352
  var PRESETS4 = {
33342
33353
  blue: {
@@ -33784,6 +33795,8 @@ function RandomPlayerPicker({
33784
33795
  const particleTimer = useRef43(null);
33785
33796
  const popTimer = useRef43(null);
33786
33797
  const tickInterval = useRef43(null);
33798
+ const pendingPopIdxRef = useRef43(null);
33799
+ const removeWinnersRef = useRef43(removeWinners);
33787
33800
  const tickAudioRef = useRef43(null);
33788
33801
  const winAudioRef = useRef43(null);
33789
33802
  const containerRef = useRef43(null);
@@ -33822,10 +33835,27 @@ function RandomPlayerPicker({
33822
33835
  winAudioRef.current = null;
33823
33836
  };
33824
33837
  }, [winSoundUrl, soundVolume]);
33838
+ useEffect55(() => {
33839
+ removeWinnersRef.current = removeWinners;
33840
+ }, [removeWinners]);
33825
33841
  useEffect55(() => {
33826
33842
  const ids = participants.map((p) => p.id).join("|");
33827
33843
  const structuralChange = ids !== prevIdsRef.current;
33828
33844
  if (structuralChange) {
33845
+ if (winnerTimer.current) {
33846
+ clearTimeout(winnerTimer.current);
33847
+ winnerTimer.current = null;
33848
+ }
33849
+ if (tickInterval.current) {
33850
+ clearTimeout(tickInterval.current);
33851
+ tickInterval.current = null;
33852
+ }
33853
+ if (popTimer.current) {
33854
+ clearTimeout(popTimer.current);
33855
+ popTimer.current = null;
33856
+ }
33857
+ pendingPopIdxRef.current = null;
33858
+ setSpinning(false);
33829
33859
  setPool(participants);
33830
33860
  setHistory([]);
33831
33861
  setWinner(null);
@@ -33882,33 +33912,50 @@ function RandomPlayerPicker({
33882
33912
  [dark]
33883
33913
  );
33884
33914
  const spin = useCallback44(() => {
33885
- if (spinning || n < 2) return;
33915
+ if (spinning) return;
33916
+ let activePool = pool;
33917
+ if (popTimer.current) {
33918
+ clearTimeout(popTimer.current);
33919
+ popTimer.current = null;
33920
+ const pendingIdx = pendingPopIdxRef.current;
33921
+ pendingPopIdxRef.current = null;
33922
+ if (pendingIdx != null && pendingIdx >= 0 && pendingIdx < pool.length && removeWinnersRef.current) {
33923
+ activePool = pool.filter((_, i) => i !== pendingIdx);
33924
+ setPool(activePool);
33925
+ setWinner(null);
33926
+ }
33927
+ }
33928
+ const activeN = activePool.length;
33929
+ if (activeN < 2) return;
33930
+ const activeAnglePerWedge = 2 * Math.PI / activeN;
33931
+ const activeRotation = rotation;
33932
+ const activeReelY = reelY;
33886
33933
  onSpinStart?.();
33887
- const winnerIdx = weightedIndex(pool);
33934
+ const winnerIdx = weightedIndex(activePool);
33888
33935
  const minRev = minRevolutions + intensityCfg.revsBoost;
33889
33936
  const maxRev = maxRevolutions + intensityCfg.revsBoost;
33890
33937
  const minFullRevs = Math.ceil(minRev);
33891
33938
  const maxFullRevs = Math.max(minFullRevs, Math.floor(maxRev));
33892
33939
  const revolutions = minFullRevs + Math.floor(Math.random() * (maxFullRevs - minFullRevs + 1));
33893
- const wedgeMidRad = winnerIdx * anglePerWedge + anglePerWedge / 2;
33940
+ const wedgeMidRad = winnerIdx * activeAnglePerWedge + activeAnglePerWedge / 2;
33894
33941
  const wedgeMidDeg = wedgeMidRad * 180 / Math.PI;
33895
- const wheelWobble = (Math.random() - 0.5) * (anglePerWedge * 180 / Math.PI) * 0.6;
33896
- const currentMod = (rotation % 360 + 360) % 360;
33942
+ const wheelWobble = (Math.random() - 0.5) * (activeAnglePerWedge * 180 / Math.PI) * 0.6;
33943
+ const currentMod = (activeRotation % 360 + 360) % 360;
33897
33944
  const targetMod = ((-wedgeMidDeg + wheelWobble) % 360 + 360) % 360;
33898
33945
  let deltaMod = targetMod - currentMod;
33899
33946
  if (deltaMod < 0) deltaMod += 360;
33900
- const finalRotation = rotation + deltaMod + revolutions * 360;
33947
+ const finalRotation = activeRotation + deltaMod + revolutions * 360;
33901
33948
  const centerOffsetRows = (reelVisibleRows - 1) / 2;
33902
33949
  const centerOffsetPx = centerOffsetRows * reelRowHeight;
33903
33950
  const currentCenterRow = Math.round(
33904
- (reelY + centerOffsetPx) / reelRowHeight
33951
+ (activeReelY + centerOffsetPx) / reelRowHeight
33905
33952
  );
33906
- const currentCenterMod = mod(currentCenterRow, n);
33907
- const targetRowOffset = (winnerIdx - currentCenterMod + n) % n;
33908
- const reelRevRows = Math.round(revolutions * n);
33909
- const finalReelY = reelY + (reelRevRows + targetRowOffset) * reelRowHeight;
33910
- const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, n) : wheelIndexAtPointer(finalRotation, n);
33911
- const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < pool.length ? settledWinnerIdx : winnerIdx;
33953
+ const currentCenterMod = mod(currentCenterRow, activeN);
33954
+ const targetRowOffset = (winnerIdx - currentCenterMod + activeN) % activeN;
33955
+ const reelRevRows = Math.round(revolutions * activeN);
33956
+ const finalReelY = activeReelY + (reelRevRows + targetRowOffset) * reelRowHeight;
33957
+ const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, activeN) : wheelIndexAtPointer(finalRotation, activeN);
33958
+ const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < activePool.length ? settledWinnerIdx : winnerIdx;
33912
33959
  const effectiveDuration = reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult);
33913
33960
  setSpinning(true);
33914
33961
  setWinner(null);
@@ -33921,7 +33968,7 @@ function RandomPlayerPicker({
33921
33968
  if ((onTick || tickAudioRef.current) && !reducedMotion) {
33922
33969
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
33923
33970
  const totalDelta = deltaMod + revolutions * 360;
33924
- const wedgeDeg = anglePerWedge * 180 / Math.PI;
33971
+ const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
33925
33972
  return Math.max(
33926
33973
  1,
33927
33974
  Math.round(totalDelta / Math.max(wedgeDeg, 1))
@@ -33953,7 +34000,7 @@ function RandomPlayerPicker({
33953
34000
  schedule();
33954
34001
  }
33955
34002
  winnerTimer.current = setTimeout(() => {
33956
- const picked = pool[finalWinnerIdx];
34003
+ const picked = activePool[finalWinnerIdx];
33957
34004
  if (!picked) {
33958
34005
  setSpinning(false);
33959
34006
  return;
@@ -33999,7 +34046,11 @@ function RandomPlayerPicker({
33999
34046
  particleTimer.current = setTimeout(() => setParticles([]), 2400);
34000
34047
  }
34001
34048
  if (removeWinners) {
34049
+ pendingPopIdxRef.current = finalWinnerIdx;
34002
34050
  popTimer.current = setTimeout(() => {
34051
+ popTimer.current = null;
34052
+ pendingPopIdxRef.current = null;
34053
+ if (!removeWinnersRef.current) return;
34003
34054
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34004
34055
  setWinner(null);
34005
34056
  setRotation(0);
@@ -34010,8 +34061,6 @@ function RandomPlayerPicker({
34010
34061
  }, [
34011
34062
  pool,
34012
34063
  spinning,
34013
- n,
34014
- anglePerWedge,
34015
34064
  rotation,
34016
34065
  reelY,
34017
34066
  mode,
@@ -34047,6 +34096,7 @@ function RandomPlayerPicker({
34047
34096
  []
34048
34097
  );
34049
34098
  useEffect55(() => {
34099
+ if (trigger === "external") return;
34050
34100
  const el = containerRef.current;
34051
34101
  if (!el) return;
34052
34102
  const h = (e) => {
@@ -34057,8 +34107,22 @@ function RandomPlayerPicker({
34057
34107
  };
34058
34108
  el.addEventListener("keydown", h);
34059
34109
  return () => el.removeEventListener("keydown", h);
34060
- }, [spin]);
34110
+ }, [spin, trigger]);
34061
34111
  const reset = useCallback44(() => {
34112
+ if (winnerTimer.current) {
34113
+ clearTimeout(winnerTimer.current);
34114
+ winnerTimer.current = null;
34115
+ }
34116
+ if (tickInterval.current) {
34117
+ clearTimeout(tickInterval.current);
34118
+ tickInterval.current = null;
34119
+ }
34120
+ if (popTimer.current) {
34121
+ clearTimeout(popTimer.current);
34122
+ popTimer.current = null;
34123
+ }
34124
+ pendingPopIdxRef.current = null;
34125
+ setSpinning(false);
34062
34126
  setPool(participants);
34063
34127
  setHistory([]);
34064
34128
  setWinner(null);
@@ -38531,27 +38595,28 @@ function GameTimer({
38531
38595
  const now = performance.now();
38532
38596
  const delta = now - lastTickRef.current;
38533
38597
  lastTickRef.current = now;
38534
- setElapsedMs((prev) => {
38535
- const next = prev + delta;
38536
- if (mode === "countdown" && next >= durationMs) {
38537
- setRunning(false);
38538
- setCompleted(true);
38539
- onComplete?.();
38540
- return durationMs;
38598
+ const next = elapsedRef.current + delta;
38599
+ if (mode === "countdown" && next >= durationMs) {
38600
+ elapsedRef.current = durationMs;
38601
+ setElapsedMs(durationMs);
38602
+ setRunning(false);
38603
+ setCompleted(true);
38604
+ onComplete?.();
38605
+ return;
38606
+ }
38607
+ if (mode === "countdown") {
38608
+ const secLeft = Math.ceil((durationMs - next) / 1e3);
38609
+ if (secLeft <= warningAt && !warningFiredRef.current) {
38610
+ warningFiredRef.current = true;
38611
+ onWarning?.();
38541
38612
  }
38542
- if (mode === "countdown") {
38543
- const secLeft = Math.ceil((durationMs - next) / 1e3);
38544
- if (secLeft <= warningAt && !warningFiredRef.current) {
38545
- warningFiredRef.current = true;
38546
- onWarning?.();
38547
- }
38548
- if (secLeft <= dangerAt && !dangerFiredRef.current) {
38549
- dangerFiredRef.current = true;
38550
- onDanger?.();
38551
- }
38613
+ if (secLeft <= dangerAt && !dangerFiredRef.current) {
38614
+ dangerFiredRef.current = true;
38615
+ onDanger?.();
38552
38616
  }
38553
- return next;
38554
- });
38617
+ }
38618
+ elapsedRef.current = next;
38619
+ setElapsedMs(next);
38555
38620
  rafRef.current = requestAnimationFrame(tick);
38556
38621
  }, [mode, durationMs, warningAt, dangerAt, onComplete, onWarning, onDanger]);
38557
38622
  useEffect59(() => {
@@ -38572,6 +38637,7 @@ function GameTimer({
38572
38637
  }, []);
38573
38638
  const reset = useCallback48(() => {
38574
38639
  setRunning(false);
38640
+ elapsedRef.current = 0;
38575
38641
  setElapsedMs(0);
38576
38642
  setCompleted(false);
38577
38643
  setLaps([]);
@@ -40528,6 +40594,8 @@ function RoundRobinScheduler({
40528
40594
  theme: theme5 = "slate",
40529
40595
  darkMode,
40530
40596
  minTeams = 3,
40597
+ maxTeams,
40598
+ nameMaxLength,
40531
40599
  defaultTeams = [],
40532
40600
  resumeRounds,
40533
40601
  onTeamsChange,
@@ -40550,39 +40618,41 @@ function RoundRobinScheduler({
40550
40618
  useEffect62(() => {
40551
40619
  onTeamsChange?.(teams);
40552
40620
  }, [teams, onTeamsChange]);
40553
- const addTeams = useCallback51((raw) => {
40554
- const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40555
- if (names.length === 0) return;
40556
- setTeams((prev) => {
40557
- const existing = new Set(prev.map((t) => t.toLowerCase()));
40558
- const next = [...prev];
40559
- for (const name of names) {
40560
- if (!existing.has(name.toLowerCase())) {
40561
- existing.add(name.toLowerCase());
40562
- next.push(name);
40563
- }
40564
- }
40565
- return next;
40566
- });
40567
- setInput("");
40568
- setRounds([]);
40569
- }, []);
40570
- const importTeams = useCallback51((incoming) => {
40571
- if (incoming.length === 0) return;
40572
- setTeams((prev) => {
40621
+ const mergeNames = useCallback51(
40622
+ (prev, incoming) => {
40573
40623
  const existing = new Set(prev.map((t) => t.toLowerCase()));
40574
40624
  const next = [...prev];
40575
- for (const name of incoming) {
40625
+ for (const rawName of incoming) {
40626
+ if (maxTeams != null && next.length >= maxTeams) break;
40627
+ const name = nameMaxLength != null ? rawName.slice(0, nameMaxLength) : rawName;
40576
40628
  const key = name.toLowerCase();
40577
- if (!existing.has(key)) {
40629
+ if (name.length > 0 && !existing.has(key)) {
40578
40630
  existing.add(key);
40579
40631
  next.push(name);
40580
40632
  }
40581
40633
  }
40582
40634
  return next;
40583
- });
40584
- setRounds([]);
40585
- }, []);
40635
+ },
40636
+ [maxTeams, nameMaxLength]
40637
+ );
40638
+ const addTeams = useCallback51(
40639
+ (raw) => {
40640
+ const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40641
+ if (names.length === 0) return;
40642
+ setTeams((prev) => mergeNames(prev, names));
40643
+ setInput("");
40644
+ setRounds([]);
40645
+ },
40646
+ [mergeNames]
40647
+ );
40648
+ const importTeams = useCallback51(
40649
+ (incoming) => {
40650
+ if (incoming.length === 0) return;
40651
+ setTeams((prev) => mergeNames(prev, incoming));
40652
+ setRounds([]);
40653
+ },
40654
+ [mergeNames]
40655
+ );
40586
40656
  const handleKeyDown = useCallback51(
40587
40657
  (e) => {
40588
40658
  if (e.key === "Enter") {
@@ -41076,6 +41146,8 @@ function Scoreboard({
41076
41146
  onTeamsChange,
41077
41147
  onRoundAdvance,
41078
41148
  onNewGame,
41149
+ onReset,
41150
+ nameMaxLength,
41079
41151
  labels,
41080
41152
  ariaLabel = "Scoreboard",
41081
41153
  className
@@ -41150,7 +41222,8 @@ function Scoreboard({
41150
41222
  setRound(1);
41151
41223
  setHistory([]);
41152
41224
  setHistoryOpen(false);
41153
- }, []);
41225
+ onReset?.();
41226
+ }, [onReset]);
41154
41227
  const newGame = useCallback52(() => {
41155
41228
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41156
41229
  setRound(1);
@@ -41372,6 +41445,7 @@ function Scoreboard({
41372
41445
  {
41373
41446
  type: "text",
41374
41447
  value: team.name,
41448
+ maxLength: nameMaxLength,
41375
41449
  onChange: (e) => updateName(index, e.target.value),
41376
41450
  style: nameInputStyle,
41377
41451
  "aria-label": `Team ${index + 1} name`