@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.
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);
@@ -34060,7 +34083,56 @@ function RandomPlayerPicker({
34060
34083
  } else {
34061
34084
  setRotation(finalRotation);
34062
34085
  }
34063
- if ((onTick || tickAudioRef.current) && !reducedMotion) {
34086
+ const fireTick = () => {
34087
+ onTick?.();
34088
+ const ta = tickAudioRef.current;
34089
+ if (ta) {
34090
+ try {
34091
+ ta.currentTime = 0;
34092
+ void ta.play().catch(() => {
34093
+ });
34094
+ } catch {
34095
+ }
34096
+ }
34097
+ };
34098
+ if (physicsDriven) {
34099
+ const from = mode === "reel" ? activeReelY : activeRotation;
34100
+ const to = mode === "reel" ? finalReelY : finalRotation;
34101
+ const crossUnit = mode === "reel" ? reelRowHeight : activeAnglePerWedge * 180 / Math.PI;
34102
+ if (mode === "reel") visualReelYRef.current = from;
34103
+ else visualRotationRef.current = from;
34104
+ let crossings = 0;
34105
+ const t0 = performance.now();
34106
+ const step = (now) => {
34107
+ const t = Math.min(1, (now - t0) / effectiveDuration);
34108
+ const value = from + (to - from) * physicsRampDown(t);
34109
+ if (mode === "reel") {
34110
+ visualReelYRef.current = value;
34111
+ const el = reelTapeRef.current;
34112
+ if (el) {
34113
+ el.style.transform = `translateY(${-(value + REEL_LEAD_IN * reelRowHeight)}px)`;
34114
+ }
34115
+ } else {
34116
+ visualRotationRef.current = value;
34117
+ const el = wheelSvgRef.current;
34118
+ if (el) el.style.transform = `rotate(${value}deg)`;
34119
+ }
34120
+ const crossed = Math.floor(Math.abs(value - from) / crossUnit);
34121
+ if (crossed > crossings) {
34122
+ crossings = crossed;
34123
+ fireTick();
34124
+ }
34125
+ if (t < 1) {
34126
+ rafRef.current = requestAnimationFrame(step);
34127
+ } else {
34128
+ rafRef.current = null;
34129
+ visualRotationRef.current = null;
34130
+ visualReelYRef.current = null;
34131
+ }
34132
+ };
34133
+ rafRef.current = requestAnimationFrame(step);
34134
+ }
34135
+ if (!physicsDriven && (onTick || tickAudioRef.current) && !reducedMotion) {
34064
34136
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
34065
34137
  const totalDelta = deltaMod + revolutions * 360;
34066
34138
  const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
@@ -34078,16 +34150,7 @@ function RandomPlayerPicker({
34078
34150
  const eased = 1 - Math.pow(1 - progress, 3);
34079
34151
  const interval = startInterval + (endInterval - startInterval) * eased;
34080
34152
  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
- }
34153
+ fireTick();
34091
34154
  elapsed += interval;
34092
34155
  schedule();
34093
34156
  }, interval);
@@ -34147,7 +34210,7 @@ function RandomPlayerPicker({
34147
34210
  pendingPopIdxRef.current = null;
34148
34211
  if (!removeWinnersRef.current) return;
34149
34212
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34150
- setWinner(null);
34213
+ if (activePool.length - 1 >= 2) setWinner(null);
34151
34214
  setRotation(0);
34152
34215
  setReelY(0);
34153
34216
  }, 2800);
@@ -34170,6 +34233,7 @@ function RandomPlayerPicker({
34170
34233
  maxRevolutions,
34171
34234
  history,
34172
34235
  reducedMotion,
34236
+ physicsDriven,
34173
34237
  enableHaptics,
34174
34238
  intensityCfg.particles,
34175
34239
  intensityCfg.durationMult,
@@ -34181,12 +34245,20 @@ function RandomPlayerPicker({
34181
34245
  if (spinRef && spinRef.current === spin) spinRef.current = null;
34182
34246
  };
34183
34247
  }, [spin, spinRef]);
34248
+ const onPoolChangeRef = useRef42(onPoolChange);
34249
+ useEffect57(() => {
34250
+ onPoolChangeRef.current = onPoolChange;
34251
+ });
34252
+ useEffect57(() => {
34253
+ onPoolChangeRef.current?.(pool);
34254
+ }, [pool]);
34184
34255
  useEffect57(
34185
34256
  () => () => {
34186
34257
  if (winnerTimer.current) clearTimeout(winnerTimer.current);
34187
34258
  if (particleTimer.current) clearTimeout(particleTimer.current);
34188
34259
  if (popTimer.current) clearTimeout(popTimer.current);
34189
34260
  if (tickInterval.current) clearTimeout(tickInterval.current);
34261
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
34190
34262
  },
34191
34263
  []
34192
34264
  );
@@ -34216,6 +34288,12 @@ function RandomPlayerPicker({
34216
34288
  clearTimeout(popTimer.current);
34217
34289
  popTimer.current = null;
34218
34290
  }
34291
+ if (rafRef.current) {
34292
+ cancelAnimationFrame(rafRef.current);
34293
+ rafRef.current = null;
34294
+ }
34295
+ visualRotationRef.current = null;
34296
+ visualReelYRef.current = null;
34219
34297
  pendingPopIdxRef.current = null;
34220
34298
  setSpinning(false);
34221
34299
  setPool(participants);
@@ -34225,9 +34303,17 @@ function RandomPlayerPicker({
34225
34303
  setReelY(0);
34226
34304
  setParticles([]);
34227
34305
  }, [participants]);
34306
+ useEffect57(() => {
34307
+ if (resetRef) resetRef.current = reset;
34308
+ return () => {
34309
+ if (resetRef && resetRef.current === reset) resetRef.current = null;
34310
+ };
34311
+ }, [reset, resetRef]);
34228
34312
  const isIdle = n < 2;
34313
+ const drainedByDraft = isIdle && participants.length >= 2;
34314
+ const effectiveIdleCaption = drainedByDraft ? draftCompleteCaption : placeholderCaption;
34229
34315
  const placeholderDiscColor = dark ? "#2a3344" : "#e2e8f0";
34230
- if (isIdle && placeholderMode === "message") {
34316
+ if (isIdle && !drainedByDraft && placeholderMode === "message") {
34231
34317
  return /* @__PURE__ */ jsx117(
34232
34318
  "div",
34233
34319
  {
@@ -34249,6 +34335,8 @@ function RandomPlayerPicker({
34249
34335
  const shakeKeyframe = `rppShake_${intensity}`;
34250
34336
  const glowKeyframe = `rppGlow_${intensity}`;
34251
34337
  const pulseKeyframe = `rppPulse_${intensity}`;
34338
+ const displayRotation = visualRotationRef.current ?? rotation;
34339
+ const displayReelY = visualReelYRef.current ?? reelY;
34252
34340
  return /* @__PURE__ */ jsxs89(
34253
34341
  "div",
34254
34342
  {
@@ -34295,13 +34383,33 @@ function RandomPlayerPicker({
34295
34383
  [data-rpp-anim] { animation: none !important; }
34296
34384
  }
34297
34385
  ` }),
34298
- mode === "reel" ? /* @__PURE__ */ jsx117(
34386
+ isIdle && placeholderMode === "message" ? (
34387
+ /* Drained draft in "message" mode — compact box in the wheel's
34388
+ place; the winner card / stats / controls / history below stay
34389
+ mounted so the endgame isn't a dead end. */
34390
+ /* @__PURE__ */ jsx117(
34391
+ "div",
34392
+ {
34393
+ style: {
34394
+ padding: "2rem 1rem",
34395
+ textAlign: "center",
34396
+ color: c.dim,
34397
+ borderRadius: 12,
34398
+ border: `1px dashed ${c.border}`
34399
+ },
34400
+ children: effectiveIdleCaption
34401
+ }
34402
+ )
34403
+ ) : mode === "reel" ? /* @__PURE__ */ jsx117(
34299
34404
  ReelStage,
34300
34405
  {
34301
34406
  pool,
34302
34407
  winner,
34303
34408
  spinning,
34304
34409
  reelY,
34410
+ displayY: displayReelY,
34411
+ cssTransition: !physicsDriven,
34412
+ tapeRef: reelTapeRef,
34305
34413
  reelVisibleRows,
34306
34414
  reelRowHeight,
34307
34415
  size,
@@ -34310,7 +34418,7 @@ function RandomPlayerPicker({
34310
34418
  dark,
34311
34419
  isIdle,
34312
34420
  placeholderMode,
34313
- placeholderCaption,
34421
+ placeholderCaption: effectiveIdleCaption,
34314
34422
  placeholderDiscColor,
34315
34423
  spinDuration,
34316
34424
  intensityCfg,
@@ -34367,6 +34475,7 @@ function RandomPlayerPicker({
34367
34475
  /* @__PURE__ */ jsxs89(
34368
34476
  "svg",
34369
34477
  {
34478
+ ref: wheelSvgRef,
34370
34479
  width: size,
34371
34480
  height: size,
34372
34481
  "aria-hidden": "true",
@@ -34375,8 +34484,8 @@ function RandomPlayerPicker({
34375
34484
  top: 36,
34376
34485
  left: 0,
34377
34486
  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",
34487
+ transform: `rotate(${displayRotation}deg)`,
34488
+ transition: spinning && !physicsDriven ? `transform ${reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult)}ms ${intensityCfg.easing}` : "none",
34380
34489
  filter: `drop-shadow(0 8px 28px ${dark ? "rgba(0,0,0,0.55)" : "rgba(0,0,0,0.15)"})`
34381
34490
  },
34382
34491
  children: [
@@ -34529,7 +34638,7 @@ function RandomPlayerPicker({
34529
34638
  pointerEvents: "none",
34530
34639
  zIndex: 3
34531
34640
  },
34532
- children: placeholderCaption
34641
+ children: effectiveIdleCaption
34533
34642
  }
34534
34643
  ),
34535
34644
  particles.map((p) => /* @__PURE__ */ jsx117(
@@ -40246,8 +40355,10 @@ function TeamGenerator({
40246
40355
  minPlayersPerTeam = 2,
40247
40356
  maxPlayers = Number.POSITIVE_INFINITY,
40248
40357
  defaultNames = [],
40358
+ defaultTeams,
40249
40359
  onNamesChange,
40250
40360
  onGenerate,
40361
+ onTeamsChange,
40251
40362
  onTeamCountChange,
40252
40363
  labels,
40253
40364
  ariaLabel = "Team generator",
@@ -40262,12 +40373,17 @@ function TeamGenerator({
40262
40373
  const [names, setNames] = useState79(defaultNames);
40263
40374
  const [input, setInput] = useState79("");
40264
40375
  const [teamCount, setTeamCount] = useState79(initialCount);
40265
- const [teams, setTeams] = useState79(null);
40376
+ const [teams, setTeams] = useState79(
40377
+ defaultTeams && defaultTeams.length > 0 ? defaultTeams : null
40378
+ );
40266
40379
  const inputRef = useRef48(null);
40267
40380
  const minNames = teamCount * minPlayersPerTeam;
40268
40381
  useEffect63(() => {
40269
40382
  onNamesChange?.(names);
40270
40383
  }, [names, onNamesChange]);
40384
+ useEffect63(() => {
40385
+ onTeamsChange?.(teams);
40386
+ }, [teams, onTeamsChange]);
40271
40387
  const addName = useCallback53(() => {
40272
40388
  const trimmed = input.trim();
40273
40389
  if (!trimmed) return;