@digilogiclabs/saas-factory-ui 2.2.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.js CHANGED
@@ -282,6 +282,7 @@ __export(index_exports, {
282
282
  CommentSystem: () => CommentSystem,
283
283
  Container: () => Container,
284
284
  ConversionKPI: () => ConversionKPI,
285
+ D6_FACE_PLACEMENTS: () => D6_FACE_PLACEMENTS,
285
286
  DISTANCES: () => DISTANCES,
286
287
  DURATIONS: () => DURATIONS,
287
288
  DashboardGrid: () => DashboardGrid,
@@ -571,6 +572,7 @@ __export(index_exports, {
571
572
  createOptimizedStore: () => createOptimizedStore,
572
573
  createSlideVariant: () => createSlideVariant,
573
574
  createTransition: () => createTransition,
575
+ d6LandingRotation: () => d6LandingRotation,
574
576
  dailyTrendingConfig: () => dailyTrendingConfig,
575
577
  detectAuthProvider: () => detectAuthProvider,
576
578
  detectDelimiter: () => detectDelimiter,
@@ -30515,14 +30517,35 @@ var PIP_POS = {
30515
30517
  [75, 75]
30516
30518
  ]
30517
30519
  };
30518
- var SHOW_ROT = {
30520
+ var D6_FACE_PLACEMENTS = {
30519
30521
  1: { x: 0, y: 0 },
30520
- 2: { x: 0, y: 180 },
30521
- 3: { x: 0, y: -90 },
30522
- 4: { x: 0, y: 90 },
30523
- 5: { x: -90, y: 0 },
30524
- 6: { x: 90, y: 0 }
30522
+ 2: { x: 0, y: 90 },
30523
+ 3: { x: 90, y: 0 },
30524
+ 4: { x: -90, y: 0 },
30525
+ 5: { x: 0, y: -90 },
30526
+ 6: { x: 0, y: 180 }
30525
30527
  };
30528
+ function d6LandingRotation(value) {
30529
+ const p = D6_FACE_PLACEMENTS[value];
30530
+ return {
30531
+ x: Math.abs(p.x) === 180 ? 180 : -p.x || 0,
30532
+ y: Math.abs(p.y) === 180 ? 180 : -p.y || 0
30533
+ };
30534
+ }
30535
+ var POLY_SHAPES = {
30536
+ 4: { points: "50,8 93,86 7,86", textY: 62, fontScale: 0.3 },
30537
+ 8: { points: "50,4 96,50 50,96 4,50", textY: 51, fontScale: 0.33 },
30538
+ 10: { points: "50,4 93,44 50,96 7,44", textY: 47, fontScale: 0.3 },
30539
+ 12: { points: "50,4 95,37 78,93 22,93 5,37", textY: 54, fontScale: 0.33 },
30540
+ 20: {
30541
+ points: "50,3 91,26 91,74 50,97 9,74 9,26",
30542
+ textY: 51,
30543
+ fontScale: 0.33
30544
+ }
30545
+ };
30546
+ function formatDieValue(v, sides) {
30547
+ return sides > 8 && (v === 6 || v === 9) ? `${v}.` : String(v);
30548
+ }
30526
30549
  var MAX_COUNT = 6;
30527
30550
  var TUMBLE_MS = 950;
30528
30551
  var SETTLE_MS = 1100;
@@ -30674,6 +30697,7 @@ function Face({ value, pipColor, pipShadow, pipDiameter }) {
30674
30697
  }
30675
30698
  function DiceRoller({
30676
30699
  count: count2 = 2,
30700
+ sides = 6,
30677
30701
  theme: theme5 = "red",
30678
30702
  darkMode,
30679
30703
  size = 80,
@@ -30695,13 +30719,13 @@ function DiceRoller({
30695
30719
  }) {
30696
30720
  const clampedCount = Math.max(1, Math.min(MAX_COUNT, count2));
30697
30721
  const dark = useDarkMode(darkMode);
30698
- const reducedMotion = usePrefersReducedMotion();
30699
30722
  const [rolling, setRolling] = (0, import_react68.useState)(false);
30700
30723
  const [results, setResults] = (0, import_react68.useState)(null);
30701
30724
  const [history, setHistory] = (0, import_react68.useState)([]);
30702
30725
  const dieWrapperRefs = (0, import_react68.useRef)([]);
30703
30726
  const cubeRefs = (0, import_react68.useRef)([]);
30704
30727
  const shadowRefs = (0, import_react68.useRef)([]);
30728
+ const numeralRefs = (0, import_react68.useRef)([]);
30705
30729
  const currentPositionsRef = (0, import_react68.useRef)(null);
30706
30730
  const rollingRef = (0, import_react68.useRef)(false);
30707
30731
  const dieSetters = (0, import_react68.useRef)([]);
@@ -30731,6 +30755,15 @@ function DiceRoller({
30731
30755
  }
30732
30756
  return shadowSetters.current[i];
30733
30757
  };
30758
+ const numeralSetters = (0, import_react68.useRef)([]);
30759
+ const getNumeralSetter = (i) => {
30760
+ if (!numeralSetters.current[i]) {
30761
+ numeralSetters.current[i] = (el) => {
30762
+ numeralRefs.current[i] = el;
30763
+ };
30764
+ }
30765
+ return numeralSetters.current[i];
30766
+ };
30734
30767
  const timersRef = (0, import_react68.useRef)([]);
30735
30768
  const rafHandleRef = (0, import_react68.useRef)(null);
30736
30769
  const arenaRef = (0, import_react68.useRef)(null);
@@ -30783,15 +30816,15 @@ function DiceRoller({
30783
30816
  const pipDiameter = Math.round(effectiveSize * 0.2);
30784
30817
  const faceRadius = Math.round(effectiveSize * 0.07);
30785
30818
  const arenaHeight = arenaHeightProp ?? Math.max(effectiveSize * 3, 200);
30786
- const faceTransforms = (0, import_react68.useMemo)(
30787
- () => [
30788
- `translateZ(${half}px)`,
30789
- `rotateY(180deg) translateZ(${half}px)`,
30790
- `rotateY(-90deg) translateZ(${half}px)`,
30791
- `rotateY(90deg) translateZ(${half}px)`,
30792
- `rotateX(90deg) translateZ(${half}px)`,
30793
- `rotateX(-90deg) translateZ(${half}px)`
30794
- ],
30819
+ const faceTransformFor = (0, import_react68.useCallback)(
30820
+ (value) => {
30821
+ const p = D6_FACE_PLACEMENTS[value];
30822
+ const parts = [];
30823
+ if (p.x !== 0) parts.push(`rotateX(${p.x}deg)`);
30824
+ if (p.y !== 0) parts.push(`rotateY(${p.y}deg)`);
30825
+ parts.push(`translateZ(${half}px)`);
30826
+ return parts.join(" ");
30827
+ },
30795
30828
  [half]
30796
30829
  );
30797
30830
  const defaultPositions = (0, import_react68.useCallback)(
@@ -30813,15 +30846,23 @@ function DiceRoller({
30813
30846
  [half]
30814
30847
  );
30815
30848
  const writeCubeRotation = (0, import_react68.useCallback)(
30816
- (i, rx, ry, rz, withTransition) => {
30849
+ (i, rx, ry, rz, withTransition, tiltZ = 0) => {
30817
30850
  const el = cubeRefs.current[i];
30818
30851
  if (!el) return;
30819
30852
  el.style.transition = withTransition ? "transform 0.95s cubic-bezier(0.12,0.8,0.22,1)" : "none";
30820
- el.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg)`;
30853
+ el.style.transform = `${tiltZ ? `rotateZ(${tiltZ}deg) ` : ""}rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg)`;
30821
30854
  },
30822
30855
  []
30823
30856
  );
30824
30857
  (0, import_react68.useEffect)(() => {
30858
+ if (timersRef.current.length > 0) {
30859
+ timersRef.current.forEach((t) => clearTimeout(t));
30860
+ timersRef.current = [];
30861
+ }
30862
+ if (rollingRef.current) {
30863
+ rollingRef.current = false;
30864
+ setRolling(false);
30865
+ }
30825
30866
  const rawW = arenaRef.current?.offsetWidth ?? 0;
30826
30867
  const rawH = arenaRef.current?.offsetHeight ?? 0;
30827
30868
  const w = rawW > 20 ? rawW : arena.w > 20 ? arena.w : 400;
@@ -30847,19 +30888,23 @@ function DiceRoller({
30847
30888
  setResults(null);
30848
30889
  }, [
30849
30890
  clampedCount,
30891
+ sides,
30850
30892
  effectiveSize,
30851
30893
  arena.w,
30852
30894
  arenaHeight,
30853
30895
  defaultPositions,
30854
30896
  half
30855
30897
  ]);
30898
+ (0, import_react68.useEffect)(() => {
30899
+ setHistory([]);
30900
+ }, [sides]);
30856
30901
  const roll = (0, import_react68.useCallback)(() => {
30857
30902
  if (rollingRef.current) return;
30858
30903
  rollingRef.current = true;
30859
30904
  setRolling(true);
30860
30905
  const vals = Array.from(
30861
30906
  { length: clampedCount },
30862
- () => Math.floor(Math.random() * 6) + 1
30907
+ () => Math.floor(Math.random() * sides) + 1
30863
30908
  );
30864
30909
  timersRef.current.forEach((t) => clearTimeout(t));
30865
30910
  timersRef.current = [];
@@ -30874,13 +30919,17 @@ function DiceRoller({
30874
30919
  const starts = currentPositionsRef.current ?? defaultPositions(w, h, clampedCount);
30875
30920
  const frames = simulateDice(w, h, effectiveSize, starts, clampedCount);
30876
30921
  for (let i = 0; i < clampedCount; i++) {
30877
- writeCubeRotation(
30878
- i,
30879
- (Math.random() - 0.5) * 720,
30880
- (Math.random() - 0.5) * 720,
30881
- (Math.random() - 0.5) * 720,
30882
- false
30883
- );
30922
+ if (sides === 6) {
30923
+ writeCubeRotation(
30924
+ i,
30925
+ (Math.random() - 0.5) * 720,
30926
+ (Math.random() - 0.5) * 720,
30927
+ (Math.random() - 0.5) * 720,
30928
+ false
30929
+ );
30930
+ } else {
30931
+ writeCubeRotation(i, 0, 0, (Math.random() - 0.5) * 720, false);
30932
+ }
30884
30933
  const el = dieWrapperRefs.current[i];
30885
30934
  if (el) el.style.transition = "none";
30886
30935
  const shadow = shadowRefs.current[i];
@@ -30913,18 +30962,47 @@ function DiceRoller({
30913
30962
  }, capturedStep * stepMs);
30914
30963
  timersRef.current.push(timer);
30915
30964
  }
30965
+ if (sides !== 6) {
30966
+ const flickerTicks = 8;
30967
+ const flickerStep = Math.max(60, Math.floor(TUMBLE_MS / flickerTicks));
30968
+ for (let f = 1; f <= flickerTicks; f++) {
30969
+ const t = setTimeout(() => {
30970
+ for (let i = 0; i < clampedCount; i++) {
30971
+ const el = numeralRefs.current[i];
30972
+ if (el)
30973
+ el.textContent = formatDieValue(
30974
+ Math.floor(Math.random() * sides) + 1,
30975
+ sides
30976
+ );
30977
+ }
30978
+ }, f * flickerStep);
30979
+ timersRef.current.push(t);
30980
+ }
30981
+ }
30916
30982
  requestAnimationFrame(() => {
30917
30983
  requestAnimationFrame(() => {
30918
30984
  for (let i = 0; i < clampedCount; i++) {
30919
- const tgt = SHOW_ROT[vals[i]];
30920
- const lx = tgt.x + (Math.random() > 0.5 ? 720 : -720);
30921
- const ly = tgt.y;
30922
- const lz = (Math.random() > 0.5 ? 1 : -1) * 360;
30923
- writeCubeRotation(i, lx, ly, lz, true);
30985
+ const tilt = (Math.random() - 0.5) * 18;
30986
+ if (sides === 6) {
30987
+ const tgt = d6LandingRotation(vals[i]);
30988
+ const lx = tgt.x + (Math.random() > 0.5 ? 720 : -720);
30989
+ const ly = tgt.y;
30990
+ const lz = (Math.random() > 0.5 ? 1 : -1) * 360;
30991
+ writeCubeRotation(i, lx, ly, lz, true, tilt);
30992
+ } else {
30993
+ const lz = (Math.random() > 0.5 ? 1 : -1) * 720 + tilt;
30994
+ writeCubeRotation(i, 0, 0, lz, true);
30995
+ }
30924
30996
  }
30925
30997
  });
30926
30998
  });
30927
30999
  const settleTimer = setTimeout(() => {
31000
+ if (sides !== 6) {
31001
+ for (let i = 0; i < clampedCount; i++) {
31002
+ const el = numeralRefs.current[i];
31003
+ if (el) el.textContent = formatDieValue(vals[i], sides);
31004
+ }
31005
+ }
30928
31006
  setResults(vals);
30929
31007
  setRolling(false);
30930
31008
  rollingRef.current = false;
@@ -30937,10 +31015,10 @@ function DiceRoller({
30937
31015
  timersRef.current.push(settleTimer);
30938
31016
  }, [
30939
31017
  clampedCount,
31018
+ sides,
30940
31019
  defaultPositions,
30941
31020
  effectiveSize,
30942
31021
  half,
30943
- reducedMotion,
30944
31022
  onRoll,
30945
31023
  writeCubeRotation,
30946
31024
  writeDiePosition,
@@ -30982,7 +31060,7 @@ function DiceRoller({
30982
31060
  {
30983
31061
  ref: arenaRef,
30984
31062
  role: "img",
30985
- "aria-label": ariaLabel ?? `${clampedCount} dice`,
31063
+ "aria-label": ariaLabel ?? `${clampedCount} ${sides === 6 ? "dice" : `${sides}-sided dice`}`,
30986
31064
  "aria-live": "polite",
30987
31065
  style: {
30988
31066
  position: "relative",
@@ -31045,7 +31123,7 @@ function DiceRoller({
31045
31123
  transformStyle: "preserve-3d",
31046
31124
  willChange: "transform"
31047
31125
  },
31048
- children: [1, 2, 3, 4, 5, 6].map((val, fi) => /* @__PURE__ */ (0, import_jsx_runtime114.jsx)(
31126
+ children: sides === 6 ? [1, 2, 3, 4, 5, 6].map((val) => /* @__PURE__ */ (0, import_jsx_runtime114.jsx)(
31049
31127
  "div",
31050
31128
  {
31051
31129
  style: {
@@ -31055,7 +31133,7 @@ function DiceRoller({
31055
31133
  background: colors.face,
31056
31134
  border: `1px solid ${colors.faceBorder}`,
31057
31135
  borderRadius: faceRadius,
31058
- transform: faceTransforms[fi],
31136
+ transform: faceTransformFor(val),
31059
31137
  transition: "background 0.3s, border-color 0.3s"
31060
31138
  },
31061
31139
  children: /* @__PURE__ */ (0, import_jsx_runtime114.jsx)(
@@ -31069,7 +31147,64 @@ function DiceRoller({
31069
31147
  )
31070
31148
  },
31071
31149
  val
31072
- ))
31150
+ )) : (() => {
31151
+ const shape = POLY_SHAPES[sides];
31152
+ const shown = results?.[d] ?? sides;
31153
+ return /* @__PURE__ */ (0, import_jsx_runtime114.jsxs)(
31154
+ "svg",
31155
+ {
31156
+ viewBox: "0 0 100 100",
31157
+ width: "100%",
31158
+ height: "100%",
31159
+ style: { display: "block", overflow: "visible" },
31160
+ "aria-hidden": "true",
31161
+ children: [
31162
+ /* @__PURE__ */ (0, import_jsx_runtime114.jsx)(
31163
+ "polygon",
31164
+ {
31165
+ points: shape.points,
31166
+ fill: colors.face,
31167
+ stroke: colors.faceBorder,
31168
+ strokeWidth: 2.5,
31169
+ strokeLinejoin: "round",
31170
+ style: { transition: "fill 0.3s, stroke 0.3s" }
31171
+ }
31172
+ ),
31173
+ /* @__PURE__ */ (0, import_jsx_runtime114.jsx)(
31174
+ "polygon",
31175
+ {
31176
+ points: shape.points,
31177
+ fill: "none",
31178
+ stroke: colors.faceBorder,
31179
+ strokeWidth: 1.5,
31180
+ strokeLinejoin: "round",
31181
+ opacity: 0.55,
31182
+ transform: "translate(50 50) scale(0.76) translate(-50 -50)"
31183
+ }
31184
+ ),
31185
+ /* @__PURE__ */ (0, import_jsx_runtime114.jsx)(
31186
+ "text",
31187
+ {
31188
+ ref: getNumeralSetter(d),
31189
+ x: 50,
31190
+ y: shape.textY,
31191
+ textAnchor: "middle",
31192
+ dominantBaseline: "central",
31193
+ fontSize: 100 * shape.fontScale,
31194
+ fontWeight: 800,
31195
+ fill: colors.pip,
31196
+ style: {
31197
+ fontVariantNumeric: "tabular-nums",
31198
+ transition: "fill 0.3s",
31199
+ userSelect: "none"
31200
+ },
31201
+ children: formatDieValue(shown, sides)
31202
+ }
31203
+ )
31204
+ ]
31205
+ }
31206
+ );
31207
+ })()
31073
31208
  }
31074
31209
  )
31075
31210
  ] }, d);
@@ -32896,6 +33031,9 @@ function CoinFlipper({
32896
33031
  }, [flipping, onFlip, historyMax]);
32897
33032
  (0, import_react70.useEffect)(() => {
32898
33033
  if (flipRef) flipRef.current = flip;
33034
+ return () => {
33035
+ if (flipRef && flipRef.current === flip) flipRef.current = null;
33036
+ };
32899
33037
  }, [flip, flipRef]);
32900
33038
  (0, import_react70.useEffect)(() => {
32901
33039
  if (autoFlip) {
@@ -33787,6 +33925,8 @@ function RandomPlayerPicker({
33787
33925
  const particleTimer = (0, import_react71.useRef)(null);
33788
33926
  const popTimer = (0, import_react71.useRef)(null);
33789
33927
  const tickInterval = (0, import_react71.useRef)(null);
33928
+ const pendingPopIdxRef = (0, import_react71.useRef)(null);
33929
+ const removeWinnersRef = (0, import_react71.useRef)(removeWinners);
33790
33930
  const tickAudioRef = (0, import_react71.useRef)(null);
33791
33931
  const winAudioRef = (0, import_react71.useRef)(null);
33792
33932
  const containerRef = (0, import_react71.useRef)(null);
@@ -33825,10 +33965,27 @@ function RandomPlayerPicker({
33825
33965
  winAudioRef.current = null;
33826
33966
  };
33827
33967
  }, [winSoundUrl, soundVolume]);
33968
+ (0, import_react71.useEffect)(() => {
33969
+ removeWinnersRef.current = removeWinners;
33970
+ }, [removeWinners]);
33828
33971
  (0, import_react71.useEffect)(() => {
33829
33972
  const ids = participants.map((p) => p.id).join("|");
33830
33973
  const structuralChange = ids !== prevIdsRef.current;
33831
33974
  if (structuralChange) {
33975
+ if (winnerTimer.current) {
33976
+ clearTimeout(winnerTimer.current);
33977
+ winnerTimer.current = null;
33978
+ }
33979
+ if (tickInterval.current) {
33980
+ clearTimeout(tickInterval.current);
33981
+ tickInterval.current = null;
33982
+ }
33983
+ if (popTimer.current) {
33984
+ clearTimeout(popTimer.current);
33985
+ popTimer.current = null;
33986
+ }
33987
+ pendingPopIdxRef.current = null;
33988
+ setSpinning(false);
33832
33989
  setPool(participants);
33833
33990
  setHistory([]);
33834
33991
  setWinner(null);
@@ -33885,33 +34042,50 @@ function RandomPlayerPicker({
33885
34042
  [dark]
33886
34043
  );
33887
34044
  const spin = (0, import_react71.useCallback)(() => {
33888
- if (spinning || n < 2) return;
34045
+ if (spinning) return;
34046
+ let activePool = pool;
34047
+ if (popTimer.current) {
34048
+ clearTimeout(popTimer.current);
34049
+ popTimer.current = null;
34050
+ const pendingIdx = pendingPopIdxRef.current;
34051
+ pendingPopIdxRef.current = null;
34052
+ if (pendingIdx != null && pendingIdx >= 0 && pendingIdx < pool.length && removeWinnersRef.current) {
34053
+ activePool = pool.filter((_, i) => i !== pendingIdx);
34054
+ setPool(activePool);
34055
+ setWinner(null);
34056
+ }
34057
+ }
34058
+ const activeN = activePool.length;
34059
+ if (activeN < 2) return;
34060
+ const activeAnglePerWedge = 2 * Math.PI / activeN;
34061
+ const activeRotation = rotation;
34062
+ const activeReelY = reelY;
33889
34063
  onSpinStart?.();
33890
- const winnerIdx = weightedIndex(pool);
34064
+ const winnerIdx = weightedIndex(activePool);
33891
34065
  const minRev = minRevolutions + intensityCfg.revsBoost;
33892
34066
  const maxRev = maxRevolutions + intensityCfg.revsBoost;
33893
34067
  const minFullRevs = Math.ceil(minRev);
33894
34068
  const maxFullRevs = Math.max(minFullRevs, Math.floor(maxRev));
33895
34069
  const revolutions = minFullRevs + Math.floor(Math.random() * (maxFullRevs - minFullRevs + 1));
33896
- const wedgeMidRad = winnerIdx * anglePerWedge + anglePerWedge / 2;
34070
+ const wedgeMidRad = winnerIdx * activeAnglePerWedge + activeAnglePerWedge / 2;
33897
34071
  const wedgeMidDeg = wedgeMidRad * 180 / Math.PI;
33898
- const wheelWobble = (Math.random() - 0.5) * (anglePerWedge * 180 / Math.PI) * 0.6;
33899
- const currentMod = (rotation % 360 + 360) % 360;
34072
+ const wheelWobble = (Math.random() - 0.5) * (activeAnglePerWedge * 180 / Math.PI) * 0.6;
34073
+ const currentMod = (activeRotation % 360 + 360) % 360;
33900
34074
  const targetMod = ((-wedgeMidDeg + wheelWobble) % 360 + 360) % 360;
33901
34075
  let deltaMod = targetMod - currentMod;
33902
34076
  if (deltaMod < 0) deltaMod += 360;
33903
- const finalRotation = rotation + deltaMod + revolutions * 360;
34077
+ const finalRotation = activeRotation + deltaMod + revolutions * 360;
33904
34078
  const centerOffsetRows = (reelVisibleRows - 1) / 2;
33905
34079
  const centerOffsetPx = centerOffsetRows * reelRowHeight;
33906
34080
  const currentCenterRow = Math.round(
33907
- (reelY + centerOffsetPx) / reelRowHeight
34081
+ (activeReelY + centerOffsetPx) / reelRowHeight
33908
34082
  );
33909
- const currentCenterMod = mod(currentCenterRow, n);
33910
- const targetRowOffset = (winnerIdx - currentCenterMod + n) % n;
33911
- const reelRevRows = Math.round(revolutions * n);
33912
- const finalReelY = reelY + (reelRevRows + targetRowOffset) * reelRowHeight;
33913
- const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, n) : wheelIndexAtPointer(finalRotation, n);
33914
- const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < pool.length ? settledWinnerIdx : winnerIdx;
34083
+ const currentCenterMod = mod(currentCenterRow, activeN);
34084
+ const targetRowOffset = (winnerIdx - currentCenterMod + activeN) % activeN;
34085
+ const reelRevRows = Math.round(revolutions * activeN);
34086
+ const finalReelY = activeReelY + (reelRevRows + targetRowOffset) * reelRowHeight;
34087
+ const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, activeN) : wheelIndexAtPointer(finalRotation, activeN);
34088
+ const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < activePool.length ? settledWinnerIdx : winnerIdx;
33915
34089
  const effectiveDuration = reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult);
33916
34090
  setSpinning(true);
33917
34091
  setWinner(null);
@@ -33924,7 +34098,7 @@ function RandomPlayerPicker({
33924
34098
  if ((onTick || tickAudioRef.current) && !reducedMotion) {
33925
34099
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
33926
34100
  const totalDelta = deltaMod + revolutions * 360;
33927
- const wedgeDeg = anglePerWedge * 180 / Math.PI;
34101
+ const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
33928
34102
  return Math.max(
33929
34103
  1,
33930
34104
  Math.round(totalDelta / Math.max(wedgeDeg, 1))
@@ -33956,7 +34130,7 @@ function RandomPlayerPicker({
33956
34130
  schedule();
33957
34131
  }
33958
34132
  winnerTimer.current = setTimeout(() => {
33959
- const picked = pool[finalWinnerIdx];
34133
+ const picked = activePool[finalWinnerIdx];
33960
34134
  if (!picked) {
33961
34135
  setSpinning(false);
33962
34136
  return;
@@ -34002,7 +34176,11 @@ function RandomPlayerPicker({
34002
34176
  particleTimer.current = setTimeout(() => setParticles([]), 2400);
34003
34177
  }
34004
34178
  if (removeWinners) {
34179
+ pendingPopIdxRef.current = finalWinnerIdx;
34005
34180
  popTimer.current = setTimeout(() => {
34181
+ popTimer.current = null;
34182
+ pendingPopIdxRef.current = null;
34183
+ if (!removeWinnersRef.current) return;
34006
34184
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34007
34185
  setWinner(null);
34008
34186
  setRotation(0);
@@ -34013,8 +34191,6 @@ function RandomPlayerPicker({
34013
34191
  }, [
34014
34192
  pool,
34015
34193
  spinning,
34016
- n,
34017
- anglePerWedge,
34018
34194
  rotation,
34019
34195
  reelY,
34020
34196
  mode,
@@ -34050,6 +34226,7 @@ function RandomPlayerPicker({
34050
34226
  []
34051
34227
  );
34052
34228
  (0, import_react71.useEffect)(() => {
34229
+ if (trigger === "external") return;
34053
34230
  const el = containerRef.current;
34054
34231
  if (!el) return;
34055
34232
  const h = (e) => {
@@ -34060,8 +34237,22 @@ function RandomPlayerPicker({
34060
34237
  };
34061
34238
  el.addEventListener("keydown", h);
34062
34239
  return () => el.removeEventListener("keydown", h);
34063
- }, [spin]);
34240
+ }, [spin, trigger]);
34064
34241
  const reset = (0, import_react71.useCallback)(() => {
34242
+ if (winnerTimer.current) {
34243
+ clearTimeout(winnerTimer.current);
34244
+ winnerTimer.current = null;
34245
+ }
34246
+ if (tickInterval.current) {
34247
+ clearTimeout(tickInterval.current);
34248
+ tickInterval.current = null;
34249
+ }
34250
+ if (popTimer.current) {
34251
+ clearTimeout(popTimer.current);
34252
+ popTimer.current = null;
34253
+ }
34254
+ pendingPopIdxRef.current = null;
34255
+ setSpinning(false);
34065
34256
  setPool(participants);
34066
34257
  setHistory([]);
34067
34258
  setWinner(null);
@@ -38514,27 +38705,28 @@ function GameTimer({
38514
38705
  const now = performance.now();
38515
38706
  const delta = now - lastTickRef.current;
38516
38707
  lastTickRef.current = now;
38517
- setElapsedMs((prev) => {
38518
- const next = prev + delta;
38519
- if (mode === "countdown" && next >= durationMs) {
38520
- setRunning(false);
38521
- setCompleted(true);
38522
- onComplete?.();
38523
- return durationMs;
38708
+ const next = elapsedRef.current + delta;
38709
+ if (mode === "countdown" && next >= durationMs) {
38710
+ elapsedRef.current = durationMs;
38711
+ setElapsedMs(durationMs);
38712
+ setRunning(false);
38713
+ setCompleted(true);
38714
+ onComplete?.();
38715
+ return;
38716
+ }
38717
+ if (mode === "countdown") {
38718
+ const secLeft = Math.ceil((durationMs - next) / 1e3);
38719
+ if (secLeft <= warningAt && !warningFiredRef.current) {
38720
+ warningFiredRef.current = true;
38721
+ onWarning?.();
38524
38722
  }
38525
- if (mode === "countdown") {
38526
- const secLeft = Math.ceil((durationMs - next) / 1e3);
38527
- if (secLeft <= warningAt && !warningFiredRef.current) {
38528
- warningFiredRef.current = true;
38529
- onWarning?.();
38530
- }
38531
- if (secLeft <= dangerAt && !dangerFiredRef.current) {
38532
- dangerFiredRef.current = true;
38533
- onDanger?.();
38534
- }
38723
+ if (secLeft <= dangerAt && !dangerFiredRef.current) {
38724
+ dangerFiredRef.current = true;
38725
+ onDanger?.();
38535
38726
  }
38536
- return next;
38537
- });
38727
+ }
38728
+ elapsedRef.current = next;
38729
+ setElapsedMs(next);
38538
38730
  rafRef.current = requestAnimationFrame(tick);
38539
38731
  }, [mode, durationMs, warningAt, dangerAt, onComplete, onWarning, onDanger]);
38540
38732
  (0, import_react74.useEffect)(() => {
@@ -38555,6 +38747,7 @@ function GameTimer({
38555
38747
  }, []);
38556
38748
  const reset = (0, import_react74.useCallback)(() => {
38557
38749
  setRunning(false);
38750
+ elapsedRef.current = 0;
38558
38751
  setElapsedMs(0);
38559
38752
  setCompleted(false);
38560
38753
  setLaps([]);
@@ -40699,6 +40892,8 @@ function RoundRobinScheduler({
40699
40892
  theme: theme5 = "slate",
40700
40893
  darkMode,
40701
40894
  minTeams = 3,
40895
+ maxTeams,
40896
+ nameMaxLength,
40702
40897
  defaultTeams = [],
40703
40898
  resumeRounds,
40704
40899
  onTeamsChange,
@@ -40721,39 +40916,41 @@ function RoundRobinScheduler({
40721
40916
  (0, import_react77.useEffect)(() => {
40722
40917
  onTeamsChange?.(teams);
40723
40918
  }, [teams, onTeamsChange]);
40724
- const addTeams = (0, import_react77.useCallback)((raw) => {
40725
- const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40726
- if (names.length === 0) return;
40727
- setTeams((prev) => {
40919
+ const mergeNames = (0, import_react77.useCallback)(
40920
+ (prev, incoming) => {
40728
40921
  const existing = new Set(prev.map((t) => t.toLowerCase()));
40729
40922
  const next = [...prev];
40730
- for (const name of names) {
40731
- if (!existing.has(name.toLowerCase())) {
40732
- existing.add(name.toLowerCase());
40733
- next.push(name);
40734
- }
40735
- }
40736
- return next;
40737
- });
40738
- setInput("");
40739
- setRounds([]);
40740
- }, []);
40741
- const importTeams = (0, import_react77.useCallback)((incoming) => {
40742
- if (incoming.length === 0) return;
40743
- setTeams((prev) => {
40744
- const existing = new Set(prev.map((t) => t.toLowerCase()));
40745
- const next = [...prev];
40746
- for (const name of incoming) {
40923
+ for (const rawName of incoming) {
40924
+ if (maxTeams != null && next.length >= maxTeams) break;
40925
+ const name = nameMaxLength != null ? rawName.slice(0, nameMaxLength) : rawName;
40747
40926
  const key = name.toLowerCase();
40748
- if (!existing.has(key)) {
40927
+ if (name.length > 0 && !existing.has(key)) {
40749
40928
  existing.add(key);
40750
40929
  next.push(name);
40751
40930
  }
40752
40931
  }
40753
40932
  return next;
40754
- });
40755
- setRounds([]);
40756
- }, []);
40933
+ },
40934
+ [maxTeams, nameMaxLength]
40935
+ );
40936
+ const addTeams = (0, import_react77.useCallback)(
40937
+ (raw) => {
40938
+ const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40939
+ if (names.length === 0) return;
40940
+ setTeams((prev) => mergeNames(prev, names));
40941
+ setInput("");
40942
+ setRounds([]);
40943
+ },
40944
+ [mergeNames]
40945
+ );
40946
+ const importTeams = (0, import_react77.useCallback)(
40947
+ (incoming) => {
40948
+ if (incoming.length === 0) return;
40949
+ setTeams((prev) => mergeNames(prev, incoming));
40950
+ setRounds([]);
40951
+ },
40952
+ [mergeNames]
40953
+ );
40757
40954
  const handleKeyDown = (0, import_react77.useCallback)(
40758
40955
  (e) => {
40759
40956
  if (e.key === "Enter") {
@@ -41242,6 +41439,8 @@ function Scoreboard({
41242
41439
  onTeamsChange,
41243
41440
  onRoundAdvance,
41244
41441
  onNewGame,
41442
+ onReset,
41443
+ nameMaxLength,
41245
41444
  labels,
41246
41445
  ariaLabel = "Scoreboard",
41247
41446
  className
@@ -41316,7 +41515,8 @@ function Scoreboard({
41316
41515
  setRound(1);
41317
41516
  setHistory([]);
41318
41517
  setHistoryOpen(false);
41319
- }, []);
41518
+ onReset?.();
41519
+ }, [onReset]);
41320
41520
  const newGame = (0, import_react78.useCallback)(() => {
41321
41521
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41322
41522
  setRound(1);
@@ -41538,6 +41738,7 @@ function Scoreboard({
41538
41738
  {
41539
41739
  type: "text",
41540
41740
  value: team.name,
41741
+ maxLength: nameMaxLength,
41541
41742
  onChange: (e) => updateName(index, e.target.value),
41542
41743
  style: nameInputStyle,
41543
41744
  "aria-label": `Team ${index + 1} name`