@digilogiclabs/saas-factory-ui 2.4.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.
@@ -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);
@@ -33965,7 +33988,56 @@ function RandomPlayerPicker({
33965
33988
  } else {
33966
33989
  setRotation(finalRotation);
33967
33990
  }
33968
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
33991
+ const fireTick = () => {
33992
+ onTick?.();
33993
+ const ta = tickAudioRef.current;
33994
+ if (ta) {
33995
+ try {
33996
+ ta.currentTime = 0;
33997
+ void ta.play().catch(() => {
33998
+ });
33999
+ } catch {
34000
+ }
34001
+ }
34002
+ };
34003
+ if (physicsDriven) {
34004
+ const from = mode === "reel" ? activeReelY : activeRotation;
34005
+ const to = mode === "reel" ? finalReelY : finalRotation;
34006
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
34007
+ if (mode === "reel") visualReelYRef.current = from;
34008
+ else visualRotationRef.current = from;
34009
+ let crossings = 0;
34010
+ const t0 = performance.now();
34011
+ const step = (now) => {
34012
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34013
+ const value = from + (to - from) * physicsRampDown(t);
34014
+ if (mode === "reel") {
34015
+ visualReelYRef.current = value;
34016
+ const el = reelTapeRef.current;
34017
+ if (el) {
34018
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34019
+ }
34020
+ } else {
34021
+ visualRotationRef.current = value;
34022
+ const el = wheelSvgRef.current;
34023
+ if (el) el.style.transform = `rotate(${value}deg)`;
34024
+ }
34025
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34026
+ if (crossed > crossings) {
34027
+ crossings = crossed;
34028
+ fireTick();
34029
+ }
34030
+ if (t < 1) {
34031
+ rafRef.current = requestAnimationFrame(step);
34032
+ } else {
34033
+ rafRef.current = null;
34034
+ visualRotationRef.current = null;
34035
+ visualReelYRef.current = null;
34036
+ }
34037
+ };
34038
+ rafRef.current = requestAnimationFrame(step);
34039
+ }
34040
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
33969
34041
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
33970
34042
  const totalDelta = deltaMod + revolutions * 360;
33971
34043
  const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
@@ -33983,16 +34055,7 @@ function RandomPlayerPicker({
33983
34055
  const eased = 1 - Math.pow(1 - progress, 3);
33984
34056
  const interval = startInterval + (endInterval - startInterval) * eased;
33985
34057
  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
- }
34058
+ fireTick();
33996
34059
  elapsed += interval;
33997
34060
  schedule();
33998
34061
  }, interval);
@@ -34052,7 +34115,7 @@ function RandomPlayerPicker({
34052
34115
  pendingPopIdxRef.current = null;
34053
34116
  if (!removeWinnersRef.current) return;
34054
34117
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34055
- setWinner(null);
34118
+ if (activePool.length - 1 >= 2) setWinner(null);
34056
34119
  setRotation(0);
34057
34120
  setReelY(0);
34058
34121
  }, 2800);
@@ -34075,6 +34138,7 @@ function RandomPlayerPicker({
34075
34138
  maxRevolutions,
34076
34139
  history,
34077
34140
  reducedMotion,
34141
+ physicsDriven,
34078
34142
  enableHaptics,
34079
34143
  intensityCfg.particles,
34080
34144
  intensityCfg.durationMult,
@@ -34086,12 +34150,20 @@ function RandomPlayerPicker({
34086
34150
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34087
34151
  };
34088
34152
  }, [spin, spinRef]);
34153
+ const onPoolChangeRef = useRef43(onPoolChange);
34154
+ useEffect55(() => {
34155
+ onPoolChangeRef.current = onPoolChange;
34156
+ });
34157
+ useEffect55(() => {
34158
+ onPoolChangeRef.current?.(pool);
34159
+ }, [pool]);
34089
34160
  useEffect55(
34090
34161
  () => () => {
34091
34162
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34092
34163
  if (particleTimer.current) clearTimeout(particleTimer.current);
34093
34164
  if (popTimer.current) clearTimeout(popTimer.current);
34094
34165
  if (tickInterval.current) clearTimeout(tickInterval.current);
34166
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34095
34167
  },
34096
34168
  []
34097
34169
  );
@@ -34121,6 +34193,12 @@ function RandomPlayerPicker({
34121
34193
  clearTimeout(popTimer.current);
34122
34194
  popTimer.current = null;
34123
34195
  }
34196
+ if (rafRef.current) {
34197
+ cancelAnimationFrame(rafRef.current);
34198
+ rafRef.current = null;
34199
+ }
34200
+ visualRotationRef.current = null;
34201
+ visualReelYRef.current = null;
34124
34202
  pendingPopIdxRef.current = null;
34125
34203
  setSpinning(false);
34126
34204
  setPool(participants);
@@ -34130,9 +34208,17 @@ function RandomPlayerPicker({
34130
34208
  setReelY(0);
34131
34209
  setParticles([]);
34132
34210
  }, [participants]);
34211
+ useEffect55(() => {
34212
+ if (resetRef) resetRef.current = reset;
34213
+ return () => {
34214
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34215
+ };
34216
+ }, [reset, resetRef]);
34133
34217
  const isIdle = n < 2;
34218
+ const drainedByDraft = isIdle && participants.length >= 2;
34219
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34134
34220
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34135
- if (isIdle && placeholderMode === "message") {
34221
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34136
34222
  return /* @__PURE__ */ jsx116(
34137
34223
  "div",
34138
34224
  {
@@ -34154,6 +34240,8 @@ function RandomPlayerPicker({
34154
34240
  const shakeKeyframe = `rppShake_${intensity}`;
34155
34241
  const glowKeyframe = `rppGlow_${intensity}`;
34156
34242
  const pulseKeyframe = `rppPulse_${intensity}`;
34243
+ const displayRotation = visualRotationRef.current ?? rotation;
34244
+ const displayReelY = visualReelYRef.current ?? reelY;
34157
34245
  return /* @__PURE__ */ jsxs89(
34158
34246
  "div",
34159
34247
  {
@@ -34200,13 +34288,33 @@ function RandomPlayerPicker({
34200
34288
  [data-rpp-anim] { animation: none !important; }
34201
34289
  }
34202
34290
  ` }),
34203
- mode === "reel" ? /* @__PURE__ */ jsx116(
34291
+ isIdle && placeholderMode === "message" ? (
34292
+ /* Drained draft in "message" mode — compact box in the wheel's
34293
+ place; the winner card / stats / controls / history below stay
34294
+ mounted so the endgame isn't a dead end. */
34295
+ /* @__PURE__ */ jsx116(
34296
+ "div",
34297
+ {
34298
+ style: {
34299
+ padding: "2rem 1rem",
34300
+ textAlign: "center",
34301
+ color: c.dim,
34302
+ borderRadius: 12,
34303
+ border: `1px dashed ${c.border}`
34304
+ },
34305
+ children: effectiveIdleCaption
34306
+ }
34307
+ )
34308
+ ) : mode === "reel" ? /* @__PURE__ */ jsx116(
34204
34309
  ReelStage,
34205
34310
  {
34206
34311
  pool,
34207
34312
  winner,
34208
34313
  spinning,
34209
34314
  reelY,
34315
+ displayY: displayReelY,
34316
+ cssTransition: !physicsDriven,
34317
+ tapeRef: reelTapeRef,
34210
34318
  reelVisibleRows,
34211
34319
  reelRowHeight,
34212
34320
  size,
@@ -34215,7 +34323,7 @@ function RandomPlayerPicker({
34215
34323
  dark,
34216
34324
  isIdle,
34217
34325
  placeholderMode,
34218
- placeholderCaption,
34326
+ placeholderCaption: effectiveIdleCaption,
34219
34327
  placeholderDiscColor,
34220
34328
  spinDuration,
34221
34329
  intensityCfg,
@@ -34272,6 +34380,7 @@ function RandomPlayerPicker({
34272
34380
  /* @__PURE__ */ jsxs89(
34273
34381
  "svg",
34274
34382
  {
34383
+ ref: wheelSvgRef,
34275
34384
  width: size,
34276
34385
  height: size,
34277
34386
  "aria-hidden": "true",
@@ -34280,8 +34389,8 @@ function RandomPlayerPicker({
34280
34389
  top: 36,
34281
34390
  left: 0,
34282
34391
  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",
34392
+ transform: `rotate(${displayRotation}deg)`,
34393
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34285
34394
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34286
34395
  },
34287
34396
  children: [
@@ -34434,7 +34543,7 @@ function RandomPlayerPicker({
34434
34543
  pointerEvents: "none",
34435
34544
  zIndex: 3
34436
34545
  },
34437
- children: placeholderCaption
34546
+ children: effectiveIdleCaption
34438
34547
  }
34439
34548
  ),
34440
34549
  particles.map((p) => /* @__PURE__ */ jsx116(
@@ -39946,8 +40055,10 @@ function TeamGenerator({
39946
40055
  minPlayersPerTeam = 2,
39947
40056
  maxPlayers = Number.POSITIVE_INFINITY,
39948
40057
  defaultNames = [],
40058
+ defaultTeams,
39949
40059
  onNamesChange,
39950
40060
  onGenerate,
40061
+ onTeamsChange,
39951
40062
  onTeamCountChange,
39952
40063
  labels,
39953
40064
  ariaLabel = "Team generator",
@@ -39962,12 +40073,17 @@ function TeamGenerator({
39962
40073
  const [names, setNames] = useState76(defaultNames);
39963
40074
  const [input, setInput] = useState76("");
39964
40075
  const [teamCount, setTeamCount] = useState76(initialCount);
39965
- const [teams, setTeams] = useState76(null);
40076
+ const [teams, setTeams] = useState76(
40077
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40078
+ );
39966
40079
  const inputRef = useRef49(null);
39967
40080
  const minNames = teamCount * minPlayersPerTeam;
39968
40081
  useEffect61(() => {
39969
40082
  onNamesChange?.(names);
39970
40083
  }, [names, onNamesChange]);
40084
+ useEffect61(() => {
40085
+ onTeamsChange?.(teams);
40086
+ }, [teams, onTeamsChange]);
39971
40087
  const addName = useCallback50(() => {
39972
40088
  const trimmed = input.trim();
39973
40089
  if (!trimmed) return;