@digilogiclabs/saas-factory-ui 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33405,6 +33405,11 @@ var WEDGE_PALETTE = [
33405
33405
  ];
33406
33406
  var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
33407
33407
  var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
33408
+ var PHYSICS_DECAY_K = 4;
33409
+ function physicsRampDown(t) {
33410
+ return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
33411
+ }
33412
+ var REEL_LEAD_IN = 32;
33408
33413
  var INTENSITY_CONFIG = {
33409
33414
  subtle: {
33410
33415
  particles: 14,
@@ -33476,6 +33481,9 @@ function ReelStage({
33476
33481
  winner,
33477
33482
  spinning,
33478
33483
  reelY,
33484
+ displayY,
33485
+ cssTransition,
33486
+ tapeRef,
33479
33487
  reelVisibleRows,
33480
33488
  reelRowHeight,
33481
33489
  size,
@@ -33499,17 +33507,16 @@ function ReelStage({
33499
33507
  const stripHeight = reelVisibleRows * reelRowHeight;
33500
33508
  const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
33501
33509
  const P = pool.length;
33502
- const leadIn = 32;
33503
33510
  const tapeRows = Math.max(
33504
- reelVisibleRows + leadIn + 1,
33505
- Math.ceil((reelY + stripHeight) / reelRowHeight) + leadIn + 1
33511
+ reelVisibleRows + REEL_LEAD_IN + 1,
33512
+ Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
33506
33513
  );
33507
33514
  const tape = Array.from({ length: tapeRows }, (_, i) => {
33508
- const poolIdx = P === 0 ? 0 : ((i - leadIn) % P + P) % P;
33515
+ const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
33509
33516
  return pool[poolIdx];
33510
33517
  });
33511
- const effectiveY = reelY + leadIn * reelRowHeight;
33512
- const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33518
+ const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
33519
+ const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33513
33520
  return /* @__PURE__ */ jsxs89(
33514
33521
  "div",
33515
33522
  {
@@ -33554,6 +33561,7 @@ function ReelStage({
33554
33561
  ) : /* @__PURE__ */ jsx116(
33555
33562
  "div",
33556
33563
  {
33564
+ ref: tapeRef,
33557
33565
  "aria-hidden": "true",
33558
33566
  style: {
33559
33567
  position: "absolute",
@@ -33769,6 +33777,9 @@ function RandomPlayerPicker({
33769
33777
  showStats = true,
33770
33778
  renderWinner,
33771
33779
  spinRef,
33780
+ resetRef,
33781
+ onPoolChange,
33782
+ draftCompleteCaption = "Draft complete \u2014 reset to spin again",
33772
33783
  intensity = "normal",
33773
33784
  enableHaptics = false,
33774
33785
  tickSoundUrl,
@@ -33783,6 +33794,7 @@ function RandomPlayerPicker({
33783
33794
  const dark = useDarkMode(darkMode);
33784
33795
  const reducedMotion = usePrefersReducedMotion();
33785
33796
  const intensityCfg = INTENSITY_CONFIG[intensity];
33797
+ const physicsDriven = intensity === "dramatic" && !reducedMotion;
33786
33798
  const [pool, setPool] = useState70(participants);
33787
33799
  const [rotation, setRotation] = useState70(0);
33788
33800
  const [reelY, setReelY] = useState70(0);
@@ -33801,6 +33813,11 @@ function RandomPlayerPicker({
33801
33813
  const winAudioRef = useRef43(null);
33802
33814
  const containerRef = useRef43(null);
33803
33815
  const prevIdsRef = useRef43("");
33816
+ const wheelSvgRef = useRef43(null);
33817
+ const reelTapeRef = useRef43(null);
33818
+ const rafRef = useRef43(null);
33819
+ const visualRotationRef = useRef43(null);
33820
+ const visualReelYRef = useRef43(null);
33804
33821
  useEffect55(() => {
33805
33822
  if (typeof window === "undefined" || !tickSoundUrl) {
33806
33823
  tickAudioRef.current = null;
@@ -33854,6 +33871,12 @@ function RandomPlayerPicker({
33854
33871
  clearTimeout(popTimer.current);
33855
33872
  popTimer.current = null;
33856
33873
  }
33874
+ if (rafRef.current) {
33875
+ cancelAnimationFrame(rafRef.current);
33876
+ rafRef.current = null;
33877
+ }
33878
+ visualRotationRef.current = null;
33879
+ visualReelYRef.current = null;
33857
33880
  pendingPopIdxRef.current = null;
33858
33881
  setSpinning(false);
33859
33882
  setPool(participants);
@@ -33926,7 +33949,20 @@ function RandomPlayerPicker({
33926
33949
  }
33927
33950
  }
33928
33951
  const activeN = activePool.length;
33929
- if (activeN < 2) return;
33952
+ if (activeN < 2) {
33953
+ if (removeWinnersRef.current && activePool.length === 1 && history.length > 0) {
33954
+ const survivor = activePool[0];
33955
+ if (survivor) {
33956
+ const promoted = [survivor, ...history].slice(0, 20);
33957
+ setPool([]);
33958
+ setWinner(survivor);
33959
+ setRevealKey((k) => k + 1);
33960
+ setHistory(promoted);
33961
+ onPick?.(survivor, promoted, { lastStanding: true });
33962
+ }
33963
+ }
33964
+ return;
33965
+ }
33930
33966
  const activeAnglePerWedge = 2 * Math.PI / activeN;
33931
33967
  const activeRotation = rotation;
33932
33968
  const activeReelY = reelY;
@@ -33965,7 +34001,56 @@ function RandomPlayerPicker({
33965
34001
  } else {
33966
34002
  setRotation(finalRotation);
33967
34003
  }
33968
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
34004
+ const fireTick = () => {
34005
+ onTick?.();
34006
+ const ta = tickAudioRef.current;
34007
+ if (ta) {
34008
+ try {
34009
+ ta.currentTime = 0;
34010
+ void ta.play().catch(() => {
34011
+ });
34012
+ } catch {
34013
+ }
34014
+ }
34015
+ };
34016
+ if (physicsDriven) {
34017
+ const from = mode === "reel" ? activeReelY : activeRotation;
34018
+ const to = mode === "reel" ? finalReelY : finalRotation;
34019
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
34020
+ if (mode === "reel") visualReelYRef.current = from;
34021
+ else visualRotationRef.current = from;
34022
+ let crossings = 0;
34023
+ const t0 = performance.now();
34024
+ const step = (now) => {
34025
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34026
+ const value = from + (to - from) * physicsRampDown(t);
34027
+ if (mode === "reel") {
34028
+ visualReelYRef.current = value;
34029
+ const el = reelTapeRef.current;
34030
+ if (el) {
34031
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34032
+ }
34033
+ } else {
34034
+ visualRotationRef.current = value;
34035
+ const el = wheelSvgRef.current;
34036
+ if (el) el.style.transform = `rotate(${value}deg)`;
34037
+ }
34038
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34039
+ if (crossed > crossings) {
34040
+ crossings = crossed;
34041
+ fireTick();
34042
+ }
34043
+ if (t < 1) {
34044
+ rafRef.current = requestAnimationFrame(step);
34045
+ } else {
34046
+ rafRef.current = null;
34047
+ visualRotationRef.current = null;
34048
+ visualReelYRef.current = null;
34049
+ }
34050
+ };
34051
+ rafRef.current = requestAnimationFrame(step);
34052
+ }
34053
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
33969
34054
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
33970
34055
  const totalDelta = deltaMod + revolutions * 360;
33971
34056
  const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
@@ -33983,16 +34068,7 @@ function RandomPlayerPicker({
33983
34068
  const eased = 1 - Math.pow(1 - progress, 3);
33984
34069
  const interval = startInterval + (endInterval - startInterval) * eased;
33985
34070
  tickInterval.current = setTimeout(() => {
33986
- onTick?.();
33987
- const ta = tickAudioRef.current;
33988
- if (ta) {
33989
- try {
33990
- ta.currentTime = 0;
33991
- void ta.play().catch(() => {
33992
- });
33993
- } catch {
33994
- }
33995
- }
34071
+ fireTick();
33996
34072
  elapsed += interval;
33997
34073
  schedule();
33998
34074
  }, interval);
@@ -34051,8 +34127,19 @@ function RandomPlayerPicker({
34051
34127
  popTimer.current = null;
34052
34128
  pendingPopIdxRef.current = null;
34053
34129
  if (!removeWinnersRef.current) return;
34054
- setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34055
- setWinner(null);
34130
+ const survivors = activePool.filter((_, i) => i !== finalWinnerIdx);
34131
+ const survivor = survivors.length === 1 ? survivors[0] : void 0;
34132
+ if (survivor) {
34133
+ const promoted = [survivor, ...nextHistory].slice(0, 20);
34134
+ setPool([]);
34135
+ setWinner(survivor);
34136
+ setRevealKey((k) => k + 1);
34137
+ setHistory(promoted);
34138
+ onPick?.(survivor, promoted, { lastStanding: true });
34139
+ } else {
34140
+ setPool(survivors);
34141
+ setWinner(null);
34142
+ }
34056
34143
  setRotation(0);
34057
34144
  setReelY(0);
34058
34145
  }, 2800);
@@ -34075,6 +34162,7 @@ function RandomPlayerPicker({
34075
34162
  maxRevolutions,
34076
34163
  history,
34077
34164
  reducedMotion,
34165
+ physicsDriven,
34078
34166
  enableHaptics,
34079
34167
  intensityCfg.particles,
34080
34168
  intensityCfg.durationMult,
@@ -34086,12 +34174,20 @@ function RandomPlayerPicker({
34086
34174
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34087
34175
  };
34088
34176
  }, [spin, spinRef]);
34177
+ const onPoolChangeRef = useRef43(onPoolChange);
34178
+ useEffect55(() => {
34179
+ onPoolChangeRef.current = onPoolChange;
34180
+ });
34181
+ useEffect55(() => {
34182
+ onPoolChangeRef.current?.(pool);
34183
+ }, [pool]);
34089
34184
  useEffect55(
34090
34185
  () => () => {
34091
34186
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34092
34187
  if (particleTimer.current) clearTimeout(particleTimer.current);
34093
34188
  if (popTimer.current) clearTimeout(popTimer.current);
34094
34189
  if (tickInterval.current) clearTimeout(tickInterval.current);
34190
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34095
34191
  },
34096
34192
  []
34097
34193
  );
@@ -34121,6 +34217,12 @@ function RandomPlayerPicker({
34121
34217
  clearTimeout(popTimer.current);
34122
34218
  popTimer.current = null;
34123
34219
  }
34220
+ if (rafRef.current) {
34221
+ cancelAnimationFrame(rafRef.current);
34222
+ rafRef.current = null;
34223
+ }
34224
+ visualRotationRef.current = null;
34225
+ visualReelYRef.current = null;
34124
34226
  pendingPopIdxRef.current = null;
34125
34227
  setSpinning(false);
34126
34228
  setPool(participants);
@@ -34130,9 +34232,17 @@ function RandomPlayerPicker({
34130
34232
  setReelY(0);
34131
34233
  setParticles([]);
34132
34234
  }, [participants]);
34235
+ useEffect55(() => {
34236
+ if (resetRef) resetRef.current = reset;
34237
+ return () => {
34238
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34239
+ };
34240
+ }, [reset, resetRef]);
34133
34241
  const isIdle = n < 2;
34242
+ const drainedByDraft = isIdle && participants.length >= 2;
34243
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34134
34244
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34135
- if (isIdle && placeholderMode === "message") {
34245
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34136
34246
  return /* @__PURE__ */ jsx116(
34137
34247
  "div",
34138
34248
  {
@@ -34154,6 +34264,8 @@ function RandomPlayerPicker({
34154
34264
  const shakeKeyframe = `rppShake_${intensity}`;
34155
34265
  const glowKeyframe = `rppGlow_${intensity}`;
34156
34266
  const pulseKeyframe = `rppPulse_${intensity}`;
34267
+ const displayRotation = visualRotationRef.current ?? rotation;
34268
+ const displayReelY = visualReelYRef.current ?? reelY;
34157
34269
  return /* @__PURE__ */ jsxs89(
34158
34270
  "div",
34159
34271
  {
@@ -34200,13 +34312,33 @@ function RandomPlayerPicker({
34200
34312
  [data-rpp-anim] { animation: none !important; }
34201
34313
  }
34202
34314
  ` }),
34203
- mode === "reel" ? /* @__PURE__ */ jsx116(
34315
+ isIdle && placeholderMode === "message" ? (
34316
+ /* Drained draft in "message" mode — compact box in the wheel's
34317
+ place; the winner card / stats / controls / history below stay
34318
+ mounted so the endgame isn't a dead end. */
34319
+ /* @__PURE__ */ jsx116(
34320
+ "div",
34321
+ {
34322
+ style: {
34323
+ padding: "2rem 1rem",
34324
+ textAlign: "center",
34325
+ color: c.dim,
34326
+ borderRadius: 12,
34327
+ border: `1px dashed ${c.border}`
34328
+ },
34329
+ children: effectiveIdleCaption
34330
+ }
34331
+ )
34332
+ ) : mode === "reel" ? /* @__PURE__ */ jsx116(
34204
34333
  ReelStage,
34205
34334
  {
34206
34335
  pool,
34207
34336
  winner,
34208
34337
  spinning,
34209
34338
  reelY,
34339
+ displayY: displayReelY,
34340
+ cssTransition: !physicsDriven,
34341
+ tapeRef: reelTapeRef,
34210
34342
  reelVisibleRows,
34211
34343
  reelRowHeight,
34212
34344
  size,
@@ -34215,7 +34347,7 @@ function RandomPlayerPicker({
34215
34347
  dark,
34216
34348
  isIdle,
34217
34349
  placeholderMode,
34218
- placeholderCaption,
34350
+ placeholderCaption: effectiveIdleCaption,
34219
34351
  placeholderDiscColor,
34220
34352
  spinDuration,
34221
34353
  intensityCfg,
@@ -34272,6 +34404,7 @@ function RandomPlayerPicker({
34272
34404
  /* @__PURE__ */ jsxs89(
34273
34405
  "svg",
34274
34406
  {
34407
+ ref: wheelSvgRef,
34275
34408
  width: size,
34276
34409
  height: size,
34277
34410
  "aria-hidden": "true",
@@ -34280,8 +34413,8 @@ function RandomPlayerPicker({
34280
34413
  top: 36,
34281
34414
  left: 0,
34282
34415
  transformOrigin: `${cx}px ${cy}px`,
34283
- transform: `rotate(${rotation}deg)`,
34284
- transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34416
+ transform: `rotate(${displayRotation}deg)`,
34417
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34285
34418
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34286
34419
  },
34287
34420
  children: [
@@ -34434,7 +34567,7 @@ function RandomPlayerPicker({
34434
34567
  pointerEvents: "none",
34435
34568
  zIndex: 3
34436
34569
  },
34437
- children: placeholderCaption
34570
+ children: effectiveIdleCaption
34438
34571
  }
34439
34572
  ),
34440
34573
  particles.map((p) => /* @__PURE__ */ jsx116(
@@ -38628,6 +38761,33 @@ function GameTimer({
38628
38761
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
38629
38762
  };
38630
38763
  }, [running, tick]);
38764
+ useEffect59(() => {
38765
+ if (!running || mode !== "countdown") return;
38766
+ let timeoutId = null;
38767
+ const disarm = () => {
38768
+ if (timeoutId !== null) {
38769
+ clearTimeout(timeoutId);
38770
+ timeoutId = null;
38771
+ }
38772
+ };
38773
+ const arm = () => {
38774
+ disarm();
38775
+ const remainingUntilDone = durationMs - elapsedRef.current;
38776
+ if (remainingUntilDone > 0) {
38777
+ timeoutId = setTimeout(tick, remainingUntilDone + 50);
38778
+ }
38779
+ };
38780
+ const onVisibility = () => {
38781
+ if (document.hidden) arm();
38782
+ else disarm();
38783
+ };
38784
+ document.addEventListener("visibilitychange", onVisibility);
38785
+ if (document.hidden) arm();
38786
+ return () => {
38787
+ disarm();
38788
+ document.removeEventListener("visibilitychange", onVisibility);
38789
+ };
38790
+ }, [running, mode, durationMs, tick]);
38631
38791
  const start = useCallback48(() => {
38632
38792
  if (completed && mode === "countdown") return;
38633
38793
  setRunning(true);
@@ -39946,8 +40106,10 @@ function TeamGenerator({
39946
40106
  minPlayersPerTeam = 2,
39947
40107
  maxPlayers = Number.POSITIVE_INFINITY,
39948
40108
  defaultNames = [],
40109
+ defaultTeams,
39949
40110
  onNamesChange,
39950
40111
  onGenerate,
40112
+ onTeamsChange,
39951
40113
  onTeamCountChange,
39952
40114
  labels,
39953
40115
  ariaLabel = "Team generator",
@@ -39962,12 +40124,17 @@ function TeamGenerator({
39962
40124
  const [names, setNames] = useState76(defaultNames);
39963
40125
  const [input, setInput] = useState76("");
39964
40126
  const [teamCount, setTeamCount] = useState76(initialCount);
39965
- const [teams, setTeams] = useState76(null);
40127
+ const [teams, setTeams] = useState76(
40128
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40129
+ );
39966
40130
  const inputRef = useRef49(null);
39967
40131
  const minNames = teamCount * minPlayersPerTeam;
39968
40132
  useEffect61(() => {
39969
40133
  onNamesChange?.(names);
39970
40134
  }, [names, onNamesChange]);
40135
+ useEffect61(() => {
40136
+ onTeamsChange?.(teams);
40137
+ }, [teams, onTeamsChange]);
39971
40138
  const addName = useCallback50(() => {
39972
40139
  const trimmed = input.trim();
39973
40140
  if (!trimmed) return;