@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.
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) {
@@ -33881,6 +33890,8 @@ function RandomPlayerPicker({
33881
33890
  const particleTimer = useRef42(null);
33882
33891
  const popTimer = useRef42(null);
33883
33892
  const tickInterval = useRef42(null);
33893
+ const pendingPopIdxRef = useRef42(null);
33894
+ const removeWinnersRef = useRef42(removeWinners);
33884
33895
  const tickAudioRef = useRef42(null);
33885
33896
  const winAudioRef = useRef42(null);
33886
33897
  const containerRef = useRef42(null);
@@ -33919,10 +33930,27 @@ function RandomPlayerPicker({
33919
33930
  winAudioRef.current = null;
33920
33931
  };
33921
33932
  }, [winSoundUrl, soundVolume]);
33933
+ useEffect57(() => {
33934
+ removeWinnersRef.current = removeWinners;
33935
+ }, [removeWinners]);
33922
33936
  useEffect57(() => {
33923
33937
  const ids = participants.map((p) => p.id).join("|");
33924
33938
  const structuralChange = ids !== prevIdsRef.current;
33925
33939
  if (structuralChange) {
33940
+ if (winnerTimer.current) {
33941
+ clearTimeout(winnerTimer.current);
33942
+ winnerTimer.current = null;
33943
+ }
33944
+ if (tickInterval.current) {
33945
+ clearTimeout(tickInterval.current);
33946
+ tickInterval.current = null;
33947
+ }
33948
+ if (popTimer.current) {
33949
+ clearTimeout(popTimer.current);
33950
+ popTimer.current = null;
33951
+ }
33952
+ pendingPopIdxRef.current = null;
33953
+ setSpinning(false);
33926
33954
  setPool(participants);
33927
33955
  setHistory([]);
33928
33956
  setWinner(null);
@@ -33979,33 +34007,50 @@ function RandomPlayerPicker({
33979
34007
  [dark]
33980
34008
  );
33981
34009
  const spin = useCallback47(() => {
33982
- if (spinning || n < 2) return;
34010
+ if (spinning) return;
34011
+ let activePool = pool;
34012
+ if (popTimer.current) {
34013
+ clearTimeout(popTimer.current);
34014
+ popTimer.current = null;
34015
+ const pendingIdx = pendingPopIdxRef.current;
34016
+ pendingPopIdxRef.current = null;
34017
+ if (pendingIdx != null && pendingIdx >= 0 && pendingIdx < pool.length && removeWinnersRef.current) {
34018
+ activePool = pool.filter((_, i) => i !== pendingIdx);
34019
+ setPool(activePool);
34020
+ setWinner(null);
34021
+ }
34022
+ }
34023
+ const activeN = activePool.length;
34024
+ if (activeN < 2) return;
34025
+ const activeAnglePerWedge = 2 * Math.PI / activeN;
34026
+ const activeRotation = rotation;
34027
+ const activeReelY = reelY;
33983
34028
  onSpinStart?.();
33984
- const winnerIdx = weightedIndex(pool);
34029
+ const winnerIdx = weightedIndex(activePool);
33985
34030
  const minRev = minRevolutions + intensityCfg.revsBoost;
33986
34031
  const maxRev = maxRevolutions + intensityCfg.revsBoost;
33987
34032
  const minFullRevs = Math.ceil(minRev);
33988
34033
  const maxFullRevs = Math.max(minFullRevs, Math.floor(maxRev));
33989
34034
  const revolutions = minFullRevs + Math.floor(Math.random() * (maxFullRevs - minFullRevs + 1));
33990
- const wedgeMidRad = winnerIdx * anglePerWedge + anglePerWedge / 2;
34035
+ const wedgeMidRad = winnerIdx * activeAnglePerWedge + activeAnglePerWedge / 2;
33991
34036
  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;
34037
+ const wheelWobble = (Math.random() - 0.5) * (activeAnglePerWedge * 180 / Math.PI) * 0.6;
34038
+ const currentMod = (activeRotation % 360 + 360) % 360;
33994
34039
  const targetMod = ((-wedgeMidDeg + wheelWobble) % 360 + 360) % 360;
33995
34040
  let deltaMod = targetMod - currentMod;
33996
34041
  if (deltaMod < 0) deltaMod += 360;
33997
- const finalRotation = rotation + deltaMod + revolutions * 360;
34042
+ const finalRotation = activeRotation + deltaMod + revolutions * 360;
33998
34043
  const centerOffsetRows = (reelVisibleRows - 1) / 2;
33999
34044
  const centerOffsetPx = centerOffsetRows * reelRowHeight;
34000
34045
  const currentCenterRow = Math.round(
34001
- (reelY + centerOffsetPx) / reelRowHeight
34046
+ (activeReelY + centerOffsetPx) / reelRowHeight
34002
34047
  );
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;
34048
+ const currentCenterMod = mod(currentCenterRow, activeN);
34049
+ const targetRowOffset = (winnerIdx - currentCenterMod + activeN) % activeN;
34050
+ const reelRevRows = Math.round(revolutions * activeN);
34051
+ const finalReelY = activeReelY + (reelRevRows + targetRowOffset) * reelRowHeight;
34052
+ const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, activeN) : wheelIndexAtPointer(finalRotation, activeN);
34053
+ const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < activePool.length ? settledWinnerIdx : winnerIdx;
34009
34054
  const effectiveDuration = reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult);
34010
34055
  setSpinning(true);
34011
34056
  setWinner(null);
@@ -34018,7 +34063,7 @@ function RandomPlayerPicker({
34018
34063
  if ((onTick || tickAudioRef.current) && !reducedMotion) {
34019
34064
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
34020
34065
  const totalDelta = deltaMod + revolutions * 360;
34021
- const wedgeDeg = anglePerWedge * 180 / Math.PI;
34066
+ const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
34022
34067
  return Math.max(
34023
34068
  1,
34024
34069
  Math.round(totalDelta / Math.max(wedgeDeg, 1))
@@ -34050,7 +34095,7 @@ function RandomPlayerPicker({
34050
34095
  schedule();
34051
34096
  }
34052
34097
  winnerTimer.current = setTimeout(() => {
34053
- const picked = pool[finalWinnerIdx];
34098
+ const picked = activePool[finalWinnerIdx];
34054
34099
  if (!picked) {
34055
34100
  setSpinning(false);
34056
34101
  return;
@@ -34096,7 +34141,11 @@ function RandomPlayerPicker({
34096
34141
  particleTimer.current = setTimeout(() => setParticles([]), 2400);
34097
34142
  }
34098
34143
  if (removeWinners) {
34144
+ pendingPopIdxRef.current = finalWinnerIdx;
34099
34145
  popTimer.current = setTimeout(() => {
34146
+ popTimer.current = null;
34147
+ pendingPopIdxRef.current = null;
34148
+ if (!removeWinnersRef.current) return;
34100
34149
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34101
34150
  setWinner(null);
34102
34151
  setRotation(0);
@@ -34107,8 +34156,6 @@ function RandomPlayerPicker({
34107
34156
  }, [
34108
34157
  pool,
34109
34158
  spinning,
34110
- n,
34111
- anglePerWedge,
34112
34159
  rotation,
34113
34160
  reelY,
34114
34161
  mode,
@@ -34144,6 +34191,7 @@ function RandomPlayerPicker({
34144
34191
  []
34145
34192
  );
34146
34193
  useEffect57(() => {
34194
+ if (trigger === "external") return;
34147
34195
  const el = containerRef.current;
34148
34196
  if (!el) return;
34149
34197
  const h = (e) => {
@@ -34154,8 +34202,22 @@ function RandomPlayerPicker({
34154
34202
  };
34155
34203
  el.addEventListener("keydown", h);
34156
34204
  return () => el.removeEventListener("keydown", h);
34157
- }, [spin]);
34205
+ }, [spin, trigger]);
34158
34206
  const reset = useCallback47(() => {
34207
+ if (winnerTimer.current) {
34208
+ clearTimeout(winnerTimer.current);
34209
+ winnerTimer.current = null;
34210
+ }
34211
+ if (tickInterval.current) {
34212
+ clearTimeout(tickInterval.current);
34213
+ tickInterval.current = null;
34214
+ }
34215
+ if (popTimer.current) {
34216
+ clearTimeout(popTimer.current);
34217
+ popTimer.current = null;
34218
+ }
34219
+ pendingPopIdxRef.current = null;
34220
+ setSpinning(false);
34159
34221
  setPool(participants);
34160
34222
  setHistory([]);
34161
34223
  setWinner(null);
@@ -38628,27 +38690,28 @@ function GameTimer({
38628
38690
  const now = performance.now();
38629
38691
  const delta = now - lastTickRef.current;
38630
38692
  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;
38693
+ const next = elapsedRef.current + delta;
38694
+ if (mode === "countdown" && next >= durationMs) {
38695
+ elapsedRef.current = durationMs;
38696
+ setElapsedMs(durationMs);
38697
+ setRunning(false);
38698
+ setCompleted(true);
38699
+ onComplete?.();
38700
+ return;
38701
+ }
38702
+ if (mode === "countdown") {
38703
+ const secLeft = Math.ceil((durationMs - next) / 1e3);
38704
+ if (secLeft <= warningAt && !warningFiredRef.current) {
38705
+ warningFiredRef.current = true;
38706
+ onWarning?.();
38638
38707
  }
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
- }
38708
+ if (secLeft <= dangerAt && !dangerFiredRef.current) {
38709
+ dangerFiredRef.current = true;
38710
+ onDanger?.();
38649
38711
  }
38650
- return next;
38651
- });
38712
+ }
38713
+ elapsedRef.current = next;
38714
+ setElapsedMs(next);
38652
38715
  rafRef.current = requestAnimationFrame(tick);
38653
38716
  }, [mode, durationMs, warningAt, dangerAt, onComplete, onWarning, onDanger]);
38654
38717
  useEffect61(() => {
@@ -38669,6 +38732,7 @@ function GameTimer({
38669
38732
  }, []);
38670
38733
  const reset = useCallback51(() => {
38671
38734
  setRunning(false);
38735
+ elapsedRef.current = 0;
38672
38736
  setElapsedMs(0);
38673
38737
  setCompleted(false);
38674
38738
  setLaps([]);
@@ -40830,6 +40894,8 @@ function RoundRobinScheduler({
40830
40894
  theme: theme5 = "slate",
40831
40895
  darkMode,
40832
40896
  minTeams = 3,
40897
+ maxTeams,
40898
+ nameMaxLength,
40833
40899
  defaultTeams = [],
40834
40900
  resumeRounds,
40835
40901
  onTeamsChange,
@@ -40852,39 +40918,41 @@ function RoundRobinScheduler({
40852
40918
  useEffect64(() => {
40853
40919
  onTeamsChange?.(teams);
40854
40920
  }, [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) => {
40921
+ const mergeNames = useCallback54(
40922
+ (prev, incoming) => {
40875
40923
  const existing = new Set(prev.map((t) => t.toLowerCase()));
40876
40924
  const next = [...prev];
40877
- for (const name of incoming) {
40925
+ for (const rawName of incoming) {
40926
+ if (maxTeams != null && next.length >= maxTeams) break;
40927
+ const name = nameMaxLength != null ? rawName.slice(0, nameMaxLength) : rawName;
40878
40928
  const key = name.toLowerCase();
40879
- if (!existing.has(key)) {
40929
+ if (name.length > 0 && !existing.has(key)) {
40880
40930
  existing.add(key);
40881
40931
  next.push(name);
40882
40932
  }
40883
40933
  }
40884
40934
  return next;
40885
- });
40886
- setRounds([]);
40887
- }, []);
40935
+ },
40936
+ [maxTeams, nameMaxLength]
40937
+ );
40938
+ const addTeams = useCallback54(
40939
+ (raw) => {
40940
+ const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40941
+ if (names.length === 0) return;
40942
+ setTeams((prev) => mergeNames(prev, names));
40943
+ setInput("");
40944
+ setRounds([]);
40945
+ },
40946
+ [mergeNames]
40947
+ );
40948
+ const importTeams = useCallback54(
40949
+ (incoming) => {
40950
+ if (incoming.length === 0) return;
40951
+ setTeams((prev) => mergeNames(prev, incoming));
40952
+ setRounds([]);
40953
+ },
40954
+ [mergeNames]
40955
+ );
40888
40956
  const handleKeyDown = useCallback54(
40889
40957
  (e) => {
40890
40958
  if (e.key === "Enter") {
@@ -41378,6 +41446,8 @@ function Scoreboard({
41378
41446
  onTeamsChange,
41379
41447
  onRoundAdvance,
41380
41448
  onNewGame,
41449
+ onReset,
41450
+ nameMaxLength,
41381
41451
  labels,
41382
41452
  ariaLabel = "Scoreboard",
41383
41453
  className
@@ -41452,7 +41522,8 @@ function Scoreboard({
41452
41522
  setRound(1);
41453
41523
  setHistory([]);
41454
41524
  setHistoryOpen(false);
41455
- }, []);
41525
+ onReset?.();
41526
+ }, [onReset]);
41456
41527
  const newGame = useCallback55(() => {
41457
41528
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41458
41529
  setRound(1);
@@ -41674,6 +41745,7 @@ function Scoreboard({
41674
41745
  {
41675
41746
  type: "text",
41676
41747
  value: team.name,
41748
+ maxLength: nameMaxLength,
41677
41749
  onChange: (e) => updateName(index, e.target.value),
41678
41750
  style: nameInputStyle,
41679
41751
  "aria-label": `Team ${index + 1} name`