@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.
package/dist/index.mjs CHANGED
@@ -33500,6 +33500,11 @@ var WEDGE_PALETTE = [
33500
33500
  ];
33501
33501
  var DEFAULT_EASING = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
33502
33502
  var DRAMATIC_EASING = "cubic-bezier(0.39, 0.575, 0.565, 1)";
33503
+ var PHYSICS_DECAY_K = 4;
33504
+ function physicsRampDown(t) {
33505
+ return (1 - Math.exp(-PHYSICS_DECAY_K * t)) / (1 - Math.exp(-PHYSICS_DECAY_K));
33506
+ }
33507
+ var REEL_LEAD_IN = 32;
33503
33508
  var INTENSITY_CONFIG = {
33504
33509
  subtle: {
33505
33510
  particles: 14,
@@ -33571,6 +33576,9 @@ function ReelStage({
33571
33576
  winner,
33572
33577
  spinning,
33573
33578
  reelY,
33579
+ displayY,
33580
+ cssTransition,
33581
+ tapeRef,
33574
33582
  reelVisibleRows,
33575
33583
  reelRowHeight,
33576
33584
  size,
@@ -33594,17 +33602,16 @@ function ReelStage({
33594
33602
  const stripHeight = reelVisibleRows * reelRowHeight;
33595
33603
  const centerOffsetPx = (reelVisibleRows - 1) / 2 * reelRowHeight;
33596
33604
  const P = pool.length;
33597
- const leadIn = 32;
33598
33605
  const tapeRows = Math.max(
33599
- reelVisibleRows + leadIn + 1,
33600
- Math.ceil((reelY + stripHeight) / reelRowHeight) + leadIn + 1
33606
+ reelVisibleRows + REEL_LEAD_IN + 1,
33607
+ Math.ceil((reelY + stripHeight) / reelRowHeight) + REEL_LEAD_IN + 1
33601
33608
  );
33602
33609
  const tape = Array.from({ length: tapeRows }, (_, i) => {
33603
- const poolIdx = P === 0 ? 0 : ((i - leadIn) % P + P) % P;
33610
+ const poolIdx = P === 0 ? 0 : ((i - REEL_LEAD_IN) % P + P) % P;
33604
33611
  return pool[poolIdx];
33605
33612
  });
33606
- const effectiveY = reelY + leadIn * reelRowHeight;
33607
- const transition = spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33613
+ const effectiveY = displayY + REEL_LEAD_IN * reelRowHeight;
33614
+ const transition = spinning && cssTransition ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none";
33608
33615
  return /* @__PURE__ */ jsxs89(
33609
33616
  "div",
33610
33617
  {
@@ -33649,6 +33656,7 @@ function ReelStage({
33649
33656
  ) : /* @__PURE__ */ jsx117(
33650
33657
  "div",
33651
33658
  {
33659
+ ref: tapeRef,
33652
33660
  "aria-hidden": "true",
33653
33661
  style: {
33654
33662
  position: "absolute",
@@ -33864,6 +33872,9 @@ function RandomPlayerPicker({
33864
33872
  showStats = true,
33865
33873
  renderWinner,
33866
33874
  spinRef,
33875
+ resetRef,
33876
+ onPoolChange,
33877
+ draftCompleteCaption = "Draft complete \u2014 reset to spin again",
33867
33878
  intensity = "normal",
33868
33879
  enableHaptics = false,
33869
33880
  tickSoundUrl,
@@ -33878,6 +33889,7 @@ function RandomPlayerPicker({
33878
33889
  const dark = useDarkMode(darkMode);
33879
33890
  const reducedMotion = usePrefersReducedMotion();
33880
33891
  const intensityCfg = INTENSITY_CONFIG[intensity];
33892
+ const physicsDriven = intensity === "dramatic" && !reducedMotion;
33881
33893
  const [pool, setPool] = useState73(participants);
33882
33894
  const [rotation, setRotation] = useState73(0);
33883
33895
  const [reelY, setReelY] = useState73(0);
@@ -33896,6 +33908,11 @@ function RandomPlayerPicker({
33896
33908
  const winAudioRef = useRef42(null);
33897
33909
  const containerRef = useRef42(null);
33898
33910
  const prevIdsRef = useRef42("");
33911
+ const wheelSvgRef = useRef42(null);
33912
+ const reelTapeRef = useRef42(null);
33913
+ const rafRef = useRef42(null);
33914
+ const visualRotationRef = useRef42(null);
33915
+ const visualReelYRef = useRef42(null);
33899
33916
  useEffect57(() => {
33900
33917
  if (typeof window === "undefined" || !tickSoundUrl) {
33901
33918
  tickAudioRef.current = null;
@@ -33949,6 +33966,12 @@ function RandomPlayerPicker({
33949
33966
  clearTimeout(popTimer.current);
33950
33967
  popTimer.current = null;
33951
33968
  }
33969
+ if (rafRef.current) {
33970
+ cancelAnimationFrame(rafRef.current);
33971
+ rafRef.current = null;
33972
+ }
33973
+ visualRotationRef.current = null;
33974
+ visualReelYRef.current = null;
33952
33975
  pendingPopIdxRef.current = null;
33953
33976
  setSpinning(false);
33954
33977
  setPool(participants);
@@ -34021,7 +34044,20 @@ function RandomPlayerPicker({
34021
34044
  }
34022
34045
  }
34023
34046
  const activeN = activePool.length;
34024
- if (activeN < 2) return;
34047
+ if (activeN < 2) {
34048
+ if (removeWinnersRef.current && activePool.length === 1 && history.length > 0) {
34049
+ const survivor = activePool[0];
34050
+ if (survivor) {
34051
+ const promoted = [survivor, ...history].slice(0, 20);
34052
+ setPool([]);
34053
+ setWinner(survivor);
34054
+ setRevealKey((k) => k + 1);
34055
+ setHistory(promoted);
34056
+ onPick?.(survivor, promoted, { lastStanding: true });
34057
+ }
34058
+ }
34059
+ return;
34060
+ }
34025
34061
  const activeAnglePerWedge = 2 * Math.PI / activeN;
34026
34062
  const activeRotation = rotation;
34027
34063
  const activeReelY = reelY;
@@ -34060,7 +34096,56 @@ function RandomPlayerPicker({
34060
34096
  } else {
34061
34097
  setRotation(finalRotation);
34062
34098
  }
34063
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
34099
+ const fireTick = () => {
34100
+ onTick?.();
34101
+ const ta = tickAudioRef.current;
34102
+ if (ta) {
34103
+ try {
34104
+ ta.currentTime = 0;
34105
+ void ta.play().catch(() => {
34106
+ });
34107
+ } catch {
34108
+ }
34109
+ }
34110
+ };
34111
+ if (physicsDriven) {
34112
+ const from = mode === "reel" ? activeReelY : activeRotation;
34113
+ const to = mode === "reel" ? finalReelY : finalRotation;
34114
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
34115
+ if (mode === "reel") visualReelYRef.current = from;
34116
+ else visualRotationRef.current = from;
34117
+ let crossings = 0;
34118
+ const t0 = performance.now();
34119
+ const step = (now) => {
34120
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34121
+ const value = from + (to - from) * physicsRampDown(t);
34122
+ if (mode === "reel") {
34123
+ visualReelYRef.current = value;
34124
+ const el = reelTapeRef.current;
34125
+ if (el) {
34126
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34127
+ }
34128
+ } else {
34129
+ visualRotationRef.current = value;
34130
+ const el = wheelSvgRef.current;
34131
+ if (el) el.style.transform = `rotate(${value}deg)`;
34132
+ }
34133
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34134
+ if (crossed > crossings) {
34135
+ crossings = crossed;
34136
+ fireTick();
34137
+ }
34138
+ if (t < 1) {
34139
+ rafRef.current = requestAnimationFrame(step);
34140
+ } else {
34141
+ rafRef.current = null;
34142
+ visualRotationRef.current = null;
34143
+ visualReelYRef.current = null;
34144
+ }
34145
+ };
34146
+ rafRef.current = requestAnimationFrame(step);
34147
+ }
34148
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
34064
34149
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
34065
34150
  const totalDelta = deltaMod + revolutions * 360;
34066
34151
  const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
@@ -34078,16 +34163,7 @@ function RandomPlayerPicker({
34078
34163
  const eased = 1 - Math.pow(1 - progress, 3);
34079
34164
  const interval = startInterval + (endInterval - startInterval) * eased;
34080
34165
  tickInterval.current = setTimeout(() => {
34081
- onTick?.();
34082
- const ta = tickAudioRef.current;
34083
- if (ta) {
34084
- try {
34085
- ta.currentTime = 0;
34086
- void ta.play().catch(() => {
34087
- });
34088
- } catch {
34089
- }
34090
- }
34166
+ fireTick();
34091
34167
  elapsed += interval;
34092
34168
  schedule();
34093
34169
  }, interval);
@@ -34146,8 +34222,19 @@ function RandomPlayerPicker({
34146
34222
  popTimer.current = null;
34147
34223
  pendingPopIdxRef.current = null;
34148
34224
  if (!removeWinnersRef.current) return;
34149
- setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34150
- setWinner(null);
34225
+ const survivors = activePool.filter((_, i) => i !== finalWinnerIdx);
34226
+ const survivor = survivors.length === 1 ? survivors[0] : void 0;
34227
+ if (survivor) {
34228
+ const promoted = [survivor, ...nextHistory].slice(0, 20);
34229
+ setPool([]);
34230
+ setWinner(survivor);
34231
+ setRevealKey((k) => k + 1);
34232
+ setHistory(promoted);
34233
+ onPick?.(survivor, promoted, { lastStanding: true });
34234
+ } else {
34235
+ setPool(survivors);
34236
+ setWinner(null);
34237
+ }
34151
34238
  setRotation(0);
34152
34239
  setReelY(0);
34153
34240
  }, 2800);
@@ -34170,6 +34257,7 @@ function RandomPlayerPicker({
34170
34257
  maxRevolutions,
34171
34258
  history,
34172
34259
  reducedMotion,
34260
+ physicsDriven,
34173
34261
  enableHaptics,
34174
34262
  intensityCfg.particles,
34175
34263
  intensityCfg.durationMult,
@@ -34181,12 +34269,20 @@ function RandomPlayerPicker({
34181
34269
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34182
34270
  };
34183
34271
  }, [spin, spinRef]);
34272
+ const onPoolChangeRef = useRef42(onPoolChange);
34273
+ useEffect57(() => {
34274
+ onPoolChangeRef.current = onPoolChange;
34275
+ });
34276
+ useEffect57(() => {
34277
+ onPoolChangeRef.current?.(pool);
34278
+ }, [pool]);
34184
34279
  useEffect57(
34185
34280
  () => () => {
34186
34281
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34187
34282
  if (particleTimer.current) clearTimeout(particleTimer.current);
34188
34283
  if (popTimer.current) clearTimeout(popTimer.current);
34189
34284
  if (tickInterval.current) clearTimeout(tickInterval.current);
34285
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34190
34286
  },
34191
34287
  []
34192
34288
  );
@@ -34216,6 +34312,12 @@ function RandomPlayerPicker({
34216
34312
  clearTimeout(popTimer.current);
34217
34313
  popTimer.current = null;
34218
34314
  }
34315
+ if (rafRef.current) {
34316
+ cancelAnimationFrame(rafRef.current);
34317
+ rafRef.current = null;
34318
+ }
34319
+ visualRotationRef.current = null;
34320
+ visualReelYRef.current = null;
34219
34321
  pendingPopIdxRef.current = null;
34220
34322
  setSpinning(false);
34221
34323
  setPool(participants);
@@ -34225,9 +34327,17 @@ function RandomPlayerPicker({
34225
34327
  setReelY(0);
34226
34328
  setParticles([]);
34227
34329
  }, [participants]);
34330
+ useEffect57(() => {
34331
+ if (resetRef) resetRef.current = reset;
34332
+ return () => {
34333
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34334
+ };
34335
+ }, [reset, resetRef]);
34228
34336
  const isIdle = n < 2;
34337
+ const drainedByDraft = isIdle && participants.length >= 2;
34338
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34229
34339
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34230
- if (isIdle && placeholderMode === "message") {
34340
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34231
34341
  return /* @__PURE__ */ jsx117(
34232
34342
  "div",
34233
34343
  {
@@ -34249,6 +34359,8 @@ function RandomPlayerPicker({
34249
34359
  const shakeKeyframe = `rppShake_${intensity}`;
34250
34360
  const glowKeyframe = `rppGlow_${intensity}`;
34251
34361
  const pulseKeyframe = `rppPulse_${intensity}`;
34362
+ const displayRotation = visualRotationRef.current ?? rotation;
34363
+ const displayReelY = visualReelYRef.current ?? reelY;
34252
34364
  return /* @__PURE__ */ jsxs89(
34253
34365
  "div",
34254
34366
  {
@@ -34295,13 +34407,33 @@ function RandomPlayerPicker({
34295
34407
  [data-rpp-anim] { animation: none !important; }
34296
34408
  }
34297
34409
  ` }),
34298
- mode === "reel" ? /* @__PURE__ */ jsx117(
34410
+ isIdle && placeholderMode === "message" ? (
34411
+ /* Drained draft in "message" mode — compact box in the wheel's
34412
+ place; the winner card / stats / controls / history below stay
34413
+ mounted so the endgame isn't a dead end. */
34414
+ /* @__PURE__ */ jsx117(
34415
+ "div",
34416
+ {
34417
+ style: {
34418
+ padding: "2rem 1rem",
34419
+ textAlign: "center",
34420
+ color: c.dim,
34421
+ borderRadius: 12,
34422
+ border: `1px dashed ${c.border}`
34423
+ },
34424
+ children: effectiveIdleCaption
34425
+ }
34426
+ )
34427
+ ) : mode === "reel" ? /* @__PURE__ */ jsx117(
34299
34428
  ReelStage,
34300
34429
  {
34301
34430
  pool,
34302
34431
  winner,
34303
34432
  spinning,
34304
34433
  reelY,
34434
+ displayY: displayReelY,
34435
+ cssTransition: !physicsDriven,
34436
+ tapeRef: reelTapeRef,
34305
34437
  reelVisibleRows,
34306
34438
  reelRowHeight,
34307
34439
  size,
@@ -34310,7 +34442,7 @@ function RandomPlayerPicker({
34310
34442
  dark,
34311
34443
  isIdle,
34312
34444
  placeholderMode,
34313
- placeholderCaption,
34445
+ placeholderCaption: effectiveIdleCaption,
34314
34446
  placeholderDiscColor,
34315
34447
  spinDuration,
34316
34448
  intensityCfg,
@@ -34367,6 +34499,7 @@ function RandomPlayerPicker({
34367
34499
  /* @__PURE__ */ jsxs89(
34368
34500
  "svg",
34369
34501
  {
34502
+ ref: wheelSvgRef,
34370
34503
  width: size,
34371
34504
  height: size,
34372
34505
  "aria-hidden": "true",
@@ -34375,8 +34508,8 @@ function RandomPlayerPicker({
34375
34508
  top: 36,
34376
34509
  left: 0,
34377
34510
  transformOrigin: `${cx}px ${cy}px`,
34378
- transform: `rotate(${rotation}deg)`,
34379
- transition: spinning ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34511
+ transform: `rotate(${displayRotation}deg)`,
34512
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34380
34513
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34381
34514
  },
34382
34515
  children: [
@@ -34529,7 +34662,7 @@ function RandomPlayerPicker({
34529
34662
  pointerEvents: "none",
34530
34663
  zIndex: 3
34531
34664
  },
34532
- children: placeholderCaption
34665
+ children: effectiveIdleCaption
34533
34666
  }
34534
34667
  ),
34535
34668
  particles.map((p) => /* @__PURE__ */ jsx117(
@@ -38723,6 +38856,33 @@ function GameTimer({
38723
38856
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
38724
38857
  };
38725
38858
  }, [running, tick]);
38859
+ useEffect61(() => {
38860
+ if (!running || mode !== "countdown") return;
38861
+ let timeoutId = null;
38862
+ const disarm = () => {
38863
+ if (timeoutId !== null) {
38864
+ clearTimeout(timeoutId);
38865
+ timeoutId = null;
38866
+ }
38867
+ };
38868
+ const arm = () => {
38869
+ disarm();
38870
+ const remainingUntilDone = durationMs - elapsedRef.current;
38871
+ if (remainingUntilDone > 0) {
38872
+ timeoutId = setTimeout(tick, remainingUntilDone + 50);
38873
+ }
38874
+ };
38875
+ const onVisibility = () => {
38876
+ if (document.hidden) arm();
38877
+ else disarm();
38878
+ };
38879
+ document.addEventListener("visibilitychange", onVisibility);
38880
+ if (document.hidden) arm();
38881
+ return () => {
38882
+ disarm();
38883
+ document.removeEventListener("visibilitychange", onVisibility);
38884
+ };
38885
+ }, [running, mode, durationMs, tick]);
38726
38886
  const start = useCallback51(() => {
38727
38887
  if (completed && mode === "countdown") return;
38728
38888
  setRunning(true);
@@ -40246,8 +40406,10 @@ function TeamGenerator({
40246
40406
  minPlayersPerTeam = 2,
40247
40407
  maxPlayers = Number.POSITIVE_INFINITY,
40248
40408
  defaultNames = [],
40409
+ defaultTeams,
40249
40410
  onNamesChange,
40250
40411
  onGenerate,
40412
+ onTeamsChange,
40251
40413
  onTeamCountChange,
40252
40414
  labels,
40253
40415
  ariaLabel = "Team generator",
@@ -40262,12 +40424,17 @@ function TeamGenerator({
40262
40424
  const [names, setNames] = useState79(defaultNames);
40263
40425
  const [input, setInput] = useState79("");
40264
40426
  const [teamCount, setTeamCount] = useState79(initialCount);
40265
- const [teams, setTeams] = useState79(null);
40427
+ const [teams, setTeams] = useState79(
40428
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40429
+ );
40266
40430
  const inputRef = useRef48(null);
40267
40431
  const minNames = teamCount * minPlayersPerTeam;
40268
40432
  useEffect63(() => {
40269
40433
  onNamesChange?.(names);
40270
40434
  }, [names, onNamesChange]);
40435
+ useEffect63(() => {
40436
+ onTeamsChange?.(teams);
40437
+ }, [teams, onTeamsChange]);
40271
40438
  const addName = useCallback53(() => {
40272
40439
  const trimmed = input.trim();
40273
40440
  if (!trimmed) return;