@digilogiclabs/saas-factory-ui 2.7.0 → 2.7.1

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.
@@ -5242,6 +5242,17 @@ interface DiceRollerProps {
5242
5242
  showHistory?: boolean;
5243
5243
  /** Max entries in the history strip. Default 8. */
5244
5244
  historyMax?: number;
5245
+ /**
5246
+ * Seed the history strip from a persisted session — newest first, the
5247
+ * same entries consumers capture from `onRoll`. One-shot: read on
5248
+ * mount, ignored afterwards; clamped to `historyMax`. Only the strip is
5249
+ * seeded — the dice arena and result chips stay in their pre-roll state
5250
+ * (a settled pose can't be faithfully reproduced).
5251
+ */
5252
+ initialHistory?: Array<{
5253
+ vals: number[];
5254
+ total: number;
5255
+ }>;
5245
5256
  /**
5246
5257
  * Imperative handle — assign a ref and call `rollRef.current?.()`
5247
5258
  * to trigger a roll from outside the component. Works with any
@@ -5285,7 +5296,7 @@ declare function d6LandingRotation(value: number): {
5285
5296
  x: number;
5286
5297
  y: number;
5287
5298
  };
5288
- declare function DiceRoller({ count, sides, theme, darkMode, size, minSize, responsive, autoRoll, onRoll, trigger, label, iconEmoji, showHistory, historyMax, rollRef, arenaHeight: arenaHeightProp, hideResults, className, style, ariaLabel, }: DiceRollerProps): ReactNode;
5299
+ declare function DiceRoller({ count, sides, theme, darkMode, size, minSize, responsive, autoRoll, onRoll, trigger, label, iconEmoji, showHistory, historyMax, initialHistory, rollRef, arenaHeight: arenaHeightProp, hideResults, className, style, ariaLabel, }: DiceRollerProps): ReactNode;
5289
5300
  declare namespace DiceRoller {
5290
5301
  var displayName: string;
5291
5302
  }
@@ -5463,6 +5474,25 @@ interface CoinFlipperProps {
5463
5474
  showHistory?: boolean;
5464
5475
  /** Max entries in history. Default 20. */
5465
5476
  historyMax?: number;
5477
+ /**
5478
+ * Seed the history strip from a persisted session — newest first, the
5479
+ * results consumers capture from `onFlip`. One-shot: read on mount,
5480
+ * ignored afterwards; clamped to `historyMax`. The coin face stays in
5481
+ * its pre-flip pose.
5482
+ */
5483
+ initialHistory?: CoinResult[];
5484
+ /**
5485
+ * Seed the H/T counters from a persisted session. Kept separate from
5486
+ * `initialHistory` because stats count ALL flips while the history
5487
+ * strip is capped at `historyMax`. One-shot: read on mount. When
5488
+ * omitted alongside `initialHistory`, the counters are derived from the
5489
+ * seeded strip instead (a lower bound if the strip was capped) — the
5490
+ * history panel only renders while the counters are non-zero.
5491
+ */
5492
+ initialStats?: {
5493
+ heads: number;
5494
+ tails: number;
5495
+ };
5466
5496
  /** Imperative handle — call `flipRef.current?.()` to flip externally. */
5467
5497
  flipRef?: MutableRefObject<(() => void) | null>;
5468
5498
  /** Accessible label. */
@@ -5470,7 +5500,7 @@ interface CoinFlipperProps {
5470
5500
  /** CSS class on root element. */
5471
5501
  className?: string;
5472
5502
  }
5473
- declare function CoinFlipper({ theme, darkMode, size, autoFlip, onFlip, trigger, label, headsLabel, tailsLabel, showHistory, historyMax, flipRef, ariaLabel, className, }: CoinFlipperProps): react_jsx_runtime.JSX.Element;
5503
+ declare function CoinFlipper({ theme, darkMode, size, autoFlip, onFlip, trigger, label, headsLabel, tailsLabel, showHistory, historyMax, initialHistory, initialStats, flipRef, ariaLabel, className, }: CoinFlipperProps): react_jsx_runtime.JSX.Element;
5474
5504
 
5475
5505
  interface RandomPlayerParticipant {
5476
5506
  /** Stable identifier — required for keying and winner equality. */
@@ -5576,6 +5606,17 @@ interface RandomPlayerPickerProps {
5576
5606
  label?: string;
5577
5607
  /** Pop each winner from the pool after reveal. Default false. */
5578
5608
  removeWinners?: boolean;
5609
+ /**
5610
+ * Resume a draft from a persisted session: participant ids already
5611
+ * drawn, newest first (the history consumers capture from `onPick`).
5612
+ * One-shot: read on mount, ignored afterwards. Only applied when
5613
+ * `removeWinners` is true — the drawn participants are removed from the
5614
+ * starting pool, seeded into the pick history, and the newest one is
5615
+ * shown on the winner card. Fail-closed: if any id doesn't resolve
5616
+ * against `participants` (or ids repeat), the whole seed is ignored
5617
+ * rather than half-restoring a stale draft.
5618
+ */
5619
+ initialDrawnIds?: string[];
5579
5620
  /** Spin animation duration in ms. Default 5000. */
5580
5621
  spinDuration?: number;
5581
5622
  /** Lower bound on full revolutions. Default 4. */
@@ -5665,7 +5706,7 @@ interface RandomPlayerPickerProps {
5665
5706
  /** Accessible label for the wheel root. */
5666
5707
  ariaLabel?: string;
5667
5708
  }
5668
- declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, resetRef, onPoolChange, draftCompleteCaption, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
5709
+ declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, initialDrawnIds, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, resetRef, onPoolChange, draftCompleteCaption, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
5669
5710
 
5670
5711
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5671
5712
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -6328,6 +6369,15 @@ interface RoundRobinSchedulerProps {
6328
6369
  onTeamsChange?: (teams: string[]) => void;
6329
6370
  /** Called when a new schedule is generated. */
6330
6371
  onGenerate?: (rounds: RoundRobinRound[]) => void;
6372
+ /**
6373
+ * Gate for roster edits (add / import / remove / shuffle / clear) that
6374
+ * would discard a generated schedule. Called synchronously BEFORE the
6375
+ * edit, and only while a schedule is on screen; return `false` to abort
6376
+ * the edit and keep the schedule (e.g. after a declined
6377
+ * `window.confirm`). Absent = edits discard the schedule silently, as
6378
+ * before. Explicit "Generate Schedule" regenerations are never gated.
6379
+ */
6380
+ onBeforeScheduleDiscard?: (action: "add" | "import" | "remove" | "shuffle" | "clear") => boolean;
6331
6381
  /** Localized / customized labels. */
6332
6382
  labels?: RoundRobinLabels;
6333
6383
  /** Accessible label on the root region. */
@@ -6338,7 +6388,7 @@ interface RoundRobinSchedulerProps {
6338
6388
  * roster from a spreadsheet. Default `true`. */
6339
6389
  enableImport?: boolean;
6340
6390
  }
6341
- declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
6391
+ declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, onBeforeScheduleDiscard, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
6342
6392
  declare namespace RoundRobinScheduler {
6343
6393
  var displayName: string;
6344
6394
  }
@@ -6402,6 +6452,14 @@ interface ScoreboardProps {
6402
6452
  * `onRoundAdvance` MUST clear that state here or their saved copy
6403
6453
  * diverges from the screen. */
6404
6454
  onReset?: () => void;
6455
+ /** Gate for the built-in Reset button. Called synchronously BEFORE any
6456
+ * state is cleared; return `false` to abort (e.g. after a declined
6457
+ * `window.confirm` when recorded rounds would be discarded). Absent =
6458
+ * Reset applies immediately, as before. */
6459
+ onBeforeReset?: () => boolean;
6460
+ /** Gate for the built-in New Game button — same contract as
6461
+ * `onBeforeReset`. */
6462
+ onBeforeNewGame?: () => boolean;
6405
6463
  /** Max characters accepted in a team-name input. Unlimited by default. */
6406
6464
  nameMaxLength?: number;
6407
6465
  /** Localized / customized labels. */
@@ -6411,7 +6469,7 @@ interface ScoreboardProps {
6411
6469
  /** CSS class on root element. */
6412
6470
  className?: string;
6413
6471
  }
6414
- declare function Scoreboard({ theme, darkMode, initialTeamCount, minTeams, maxTeams, scoreButtons, defaultTeams, initialRound, initialHistory, onTeamsChange, onRoundAdvance, onNewGame, onReset, nameMaxLength, labels, ariaLabel, className, }: ScoreboardProps): react_jsx_runtime.JSX.Element;
6472
+ declare function Scoreboard({ theme, darkMode, initialTeamCount, minTeams, maxTeams, scoreButtons, defaultTeams, initialRound, initialHistory, onTeamsChange, onRoundAdvance, onNewGame, onReset, onBeforeReset, onBeforeNewGame, nameMaxLength, labels, ariaLabel, className, }: ScoreboardProps): react_jsx_runtime.JSX.Element;
6415
6473
  declare namespace Scoreboard {
6416
6474
  var displayName: string;
6417
6475
  }
@@ -5242,6 +5242,17 @@ interface DiceRollerProps {
5242
5242
  showHistory?: boolean;
5243
5243
  /** Max entries in the history strip. Default 8. */
5244
5244
  historyMax?: number;
5245
+ /**
5246
+ * Seed the history strip from a persisted session — newest first, the
5247
+ * same entries consumers capture from `onRoll`. One-shot: read on
5248
+ * mount, ignored afterwards; clamped to `historyMax`. Only the strip is
5249
+ * seeded — the dice arena and result chips stay in their pre-roll state
5250
+ * (a settled pose can't be faithfully reproduced).
5251
+ */
5252
+ initialHistory?: Array<{
5253
+ vals: number[];
5254
+ total: number;
5255
+ }>;
5245
5256
  /**
5246
5257
  * Imperative handle — assign a ref and call `rollRef.current?.()`
5247
5258
  * to trigger a roll from outside the component. Works with any
@@ -5285,7 +5296,7 @@ declare function d6LandingRotation(value: number): {
5285
5296
  x: number;
5286
5297
  y: number;
5287
5298
  };
5288
- declare function DiceRoller({ count, sides, theme, darkMode, size, minSize, responsive, autoRoll, onRoll, trigger, label, iconEmoji, showHistory, historyMax, rollRef, arenaHeight: arenaHeightProp, hideResults, className, style, ariaLabel, }: DiceRollerProps): ReactNode;
5299
+ declare function DiceRoller({ count, sides, theme, darkMode, size, minSize, responsive, autoRoll, onRoll, trigger, label, iconEmoji, showHistory, historyMax, initialHistory, rollRef, arenaHeight: arenaHeightProp, hideResults, className, style, ariaLabel, }: DiceRollerProps): ReactNode;
5289
5300
  declare namespace DiceRoller {
5290
5301
  var displayName: string;
5291
5302
  }
@@ -5463,6 +5474,25 @@ interface CoinFlipperProps {
5463
5474
  showHistory?: boolean;
5464
5475
  /** Max entries in history. Default 20. */
5465
5476
  historyMax?: number;
5477
+ /**
5478
+ * Seed the history strip from a persisted session — newest first, the
5479
+ * results consumers capture from `onFlip`. One-shot: read on mount,
5480
+ * ignored afterwards; clamped to `historyMax`. The coin face stays in
5481
+ * its pre-flip pose.
5482
+ */
5483
+ initialHistory?: CoinResult[];
5484
+ /**
5485
+ * Seed the H/T counters from a persisted session. Kept separate from
5486
+ * `initialHistory` because stats count ALL flips while the history
5487
+ * strip is capped at `historyMax`. One-shot: read on mount. When
5488
+ * omitted alongside `initialHistory`, the counters are derived from the
5489
+ * seeded strip instead (a lower bound if the strip was capped) — the
5490
+ * history panel only renders while the counters are non-zero.
5491
+ */
5492
+ initialStats?: {
5493
+ heads: number;
5494
+ tails: number;
5495
+ };
5466
5496
  /** Imperative handle — call `flipRef.current?.()` to flip externally. */
5467
5497
  flipRef?: MutableRefObject<(() => void) | null>;
5468
5498
  /** Accessible label. */
@@ -5470,7 +5500,7 @@ interface CoinFlipperProps {
5470
5500
  /** CSS class on root element. */
5471
5501
  className?: string;
5472
5502
  }
5473
- declare function CoinFlipper({ theme, darkMode, size, autoFlip, onFlip, trigger, label, headsLabel, tailsLabel, showHistory, historyMax, flipRef, ariaLabel, className, }: CoinFlipperProps): react_jsx_runtime.JSX.Element;
5503
+ declare function CoinFlipper({ theme, darkMode, size, autoFlip, onFlip, trigger, label, headsLabel, tailsLabel, showHistory, historyMax, initialHistory, initialStats, flipRef, ariaLabel, className, }: CoinFlipperProps): react_jsx_runtime.JSX.Element;
5474
5504
 
5475
5505
  interface RandomPlayerParticipant {
5476
5506
  /** Stable identifier — required for keying and winner equality. */
@@ -5576,6 +5606,17 @@ interface RandomPlayerPickerProps {
5576
5606
  label?: string;
5577
5607
  /** Pop each winner from the pool after reveal. Default false. */
5578
5608
  removeWinners?: boolean;
5609
+ /**
5610
+ * Resume a draft from a persisted session: participant ids already
5611
+ * drawn, newest first (the history consumers capture from `onPick`).
5612
+ * One-shot: read on mount, ignored afterwards. Only applied when
5613
+ * `removeWinners` is true — the drawn participants are removed from the
5614
+ * starting pool, seeded into the pick history, and the newest one is
5615
+ * shown on the winner card. Fail-closed: if any id doesn't resolve
5616
+ * against `participants` (or ids repeat), the whole seed is ignored
5617
+ * rather than half-restoring a stale draft.
5618
+ */
5619
+ initialDrawnIds?: string[];
5579
5620
  /** Spin animation duration in ms. Default 5000. */
5580
5621
  spinDuration?: number;
5581
5622
  /** Lower bound on full revolutions. Default 4. */
@@ -5665,7 +5706,7 @@ interface RandomPlayerPickerProps {
5665
5706
  /** Accessible label for the wheel root. */
5666
5707
  ariaLabel?: string;
5667
5708
  }
5668
- declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, resetRef, onPoolChange, draftCompleteCaption, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
5709
+ declare function RandomPlayerPicker({ participants, mode, reelVisibleRows, reelRowHeight, theme, darkMode, size, onPick, onSpinStart, onTick, trigger, label, removeWinners, initialDrawnIds, spinDuration, minRevolutions, maxRevolutions, showHistory, showControls, showStats, renderWinner, spinRef, resetRef, onPoolChange, draftCompleteCaption, intensity, enableHaptics, tickSoundUrl, winSoundUrl, soundVolume, placeholderMode, placeholderCaption, className, style, ariaLabel, }: RandomPlayerPickerProps): react_jsx_runtime.JSX.Element;
5669
5710
 
5670
5711
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5671
5712
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -6328,6 +6369,15 @@ interface RoundRobinSchedulerProps {
6328
6369
  onTeamsChange?: (teams: string[]) => void;
6329
6370
  /** Called when a new schedule is generated. */
6330
6371
  onGenerate?: (rounds: RoundRobinRound[]) => void;
6372
+ /**
6373
+ * Gate for roster edits (add / import / remove / shuffle / clear) that
6374
+ * would discard a generated schedule. Called synchronously BEFORE the
6375
+ * edit, and only while a schedule is on screen; return `false` to abort
6376
+ * the edit and keep the schedule (e.g. after a declined
6377
+ * `window.confirm`). Absent = edits discard the schedule silently, as
6378
+ * before. Explicit "Generate Schedule" regenerations are never gated.
6379
+ */
6380
+ onBeforeScheduleDiscard?: (action: "add" | "import" | "remove" | "shuffle" | "clear") => boolean;
6331
6381
  /** Localized / customized labels. */
6332
6382
  labels?: RoundRobinLabels;
6333
6383
  /** Accessible label on the root region. */
@@ -6338,7 +6388,7 @@ interface RoundRobinSchedulerProps {
6338
6388
  * roster from a spreadsheet. Default `true`. */
6339
6389
  enableImport?: boolean;
6340
6390
  }
6341
- declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
6391
+ declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, onBeforeScheduleDiscard, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
6342
6392
  declare namespace RoundRobinScheduler {
6343
6393
  var displayName: string;
6344
6394
  }
@@ -6402,6 +6452,14 @@ interface ScoreboardProps {
6402
6452
  * `onRoundAdvance` MUST clear that state here or their saved copy
6403
6453
  * diverges from the screen. */
6404
6454
  onReset?: () => void;
6455
+ /** Gate for the built-in Reset button. Called synchronously BEFORE any
6456
+ * state is cleared; return `false` to abort (e.g. after a declined
6457
+ * `window.confirm` when recorded rounds would be discarded). Absent =
6458
+ * Reset applies immediately, as before. */
6459
+ onBeforeReset?: () => boolean;
6460
+ /** Gate for the built-in New Game button — same contract as
6461
+ * `onBeforeReset`. */
6462
+ onBeforeNewGame?: () => boolean;
6405
6463
  /** Max characters accepted in a team-name input. Unlimited by default. */
6406
6464
  nameMaxLength?: number;
6407
6465
  /** Localized / customized labels. */
@@ -6411,7 +6469,7 @@ interface ScoreboardProps {
6411
6469
  /** CSS class on root element. */
6412
6470
  className?: string;
6413
6471
  }
6414
- declare function Scoreboard({ theme, darkMode, initialTeamCount, minTeams, maxTeams, scoreButtons, defaultTeams, initialRound, initialHistory, onTeamsChange, onRoundAdvance, onNewGame, onReset, nameMaxLength, labels, ariaLabel, className, }: ScoreboardProps): react_jsx_runtime.JSX.Element;
6472
+ declare function Scoreboard({ theme, darkMode, initialTeamCount, minTeams, maxTeams, scoreButtons, defaultTeams, initialRound, initialHistory, onTeamsChange, onRoundAdvance, onNewGame, onReset, onBeforeReset, onBeforeNewGame, nameMaxLength, labels, ariaLabel, className, }: ScoreboardProps): react_jsx_runtime.JSX.Element;
6415
6473
  declare namespace Scoreboard {
6416
6474
  var displayName: string;
6417
6475
  }
package/dist/web/index.js CHANGED
@@ -30539,6 +30539,7 @@ function DiceRoller({
30539
30539
  iconEmoji = "\u{1F3B2}",
30540
30540
  showHistory = false,
30541
30541
  historyMax = 8,
30542
+ initialHistory,
30542
30543
  rollRef,
30543
30544
  arenaHeight: arenaHeightProp,
30544
30545
  hideResults = false,
@@ -30550,7 +30551,9 @@ function DiceRoller({
30550
30551
  const dark = useDarkMode(darkMode);
30551
30552
  const [rolling, setRolling] = (0, import_react69.useState)(false);
30552
30553
  const [results, setResults] = (0, import_react69.useState)(null);
30553
- const [history, setHistory] = (0, import_react69.useState)([]);
30554
+ const [history, setHistory] = (0, import_react69.useState)(
30555
+ () => initialHistory && initialHistory.length > 0 ? initialHistory.slice(0, historyMax) : []
30556
+ );
30554
30557
  const dieWrapperRefs = (0, import_react69.useRef)([]);
30555
30558
  const cubeRefs = (0, import_react69.useRef)([]);
30556
30559
  const shadowRefs = (0, import_react69.useRef)([]);
@@ -30724,7 +30727,10 @@ function DiceRoller({
30724
30727
  defaultPositions,
30725
30728
  half
30726
30729
  ]);
30730
+ const prevSidesRef = (0, import_react69.useRef)(sides);
30727
30731
  (0, import_react69.useEffect)(() => {
30732
+ if (prevSidesRef.current === sides) return;
30733
+ prevSidesRef.current = sides;
30728
30734
  setHistory([]);
30729
30735
  }, [sides]);
30730
30736
  const roll = (0, import_react69.useCallback)(() => {
@@ -32793,6 +32799,8 @@ function CoinFlipper({
32793
32799
  tailsLabel = "Tails",
32794
32800
  showHistory = false,
32795
32801
  historyMax = 20,
32802
+ initialHistory,
32803
+ initialStats,
32796
32804
  flipRef,
32797
32805
  ariaLabel,
32798
32806
  className = ""
@@ -32801,8 +32809,23 @@ function CoinFlipper({
32801
32809
  const tc = typeof theme5 === "string" ? PRESETS3[theme5] || PRESETS3.gold : theme5;
32802
32810
  const [result, setResult] = (0, import_react71.useState)(null);
32803
32811
  const [flipping, setFlipping] = (0, import_react71.useState)(false);
32804
- const [history, setHistory] = (0, import_react71.useState)([]);
32805
- const [stats, setStats] = (0, import_react71.useState)({ heads: 0, tails: 0 });
32812
+ const [history, setHistory] = (0, import_react71.useState)(
32813
+ () => initialHistory && initialHistory.length > 0 ? initialHistory.slice(0, historyMax).map((result2, i) => ({ result: result2, id: -(i + 1) })) : []
32814
+ );
32815
+ const [stats, setStats] = (0, import_react71.useState)(() => {
32816
+ if (initialStats) {
32817
+ return {
32818
+ heads: Math.max(0, Math.floor(initialStats.heads)),
32819
+ tails: Math.max(0, Math.floor(initialStats.tails))
32820
+ };
32821
+ }
32822
+ if (initialHistory && initialHistory.length > 0) {
32823
+ const seeded = initialHistory.slice(0, historyMax);
32824
+ const heads = seeded.filter((r) => r === "heads").length;
32825
+ return { heads, tails: seeded.length - heads };
32826
+ }
32827
+ return { heads: 0, tails: 0 };
32828
+ });
32806
32829
  const flipIdRef = (0, import_react71.useRef)(0);
32807
32830
  const coinRef = (0, import_react71.useRef)(null);
32808
32831
  const shadowRef = (0, import_react71.useRef)(null);
@@ -33731,6 +33754,23 @@ function ReelStage({
33731
33754
  }
33732
33755
  );
33733
33756
  }
33757
+ function computeDraftSeed(participants, drawnIds, removeWinners) {
33758
+ if (!removeWinners || !drawnIds || drawnIds.length === 0) return null;
33759
+ if (new Set(drawnIds).size !== drawnIds.length) return null;
33760
+ const byId = new Map(participants.map((p) => [p.id, p]));
33761
+ const drawn = [];
33762
+ for (const id of drawnIds) {
33763
+ const p = byId.get(id);
33764
+ if (!p) return null;
33765
+ drawn.push(p);
33766
+ }
33767
+ const drawnSet = new Set(drawnIds);
33768
+ return {
33769
+ pool: participants.filter((p) => !drawnSet.has(p.id)),
33770
+ // Same cap as the live history (see the pop/promote paths).
33771
+ history: drawn.slice(0, 20)
33772
+ };
33773
+ }
33734
33774
  function RandomPlayerPicker({
33735
33775
  participants,
33736
33776
  mode = "wheel",
@@ -33745,6 +33785,7 @@ function RandomPlayerPicker({
33745
33785
  trigger = "button",
33746
33786
  label = "Spin",
33747
33787
  removeWinners = false,
33788
+ initialDrawnIds,
33748
33789
  spinDuration = 5e3,
33749
33790
  minRevolutions = 4,
33750
33791
  maxRevolutions = 7,
@@ -33771,12 +33812,21 @@ function RandomPlayerPicker({
33771
33812
  const reducedMotion = usePrefersReducedMotion();
33772
33813
  const intensityCfg = INTENSITY_CONFIG[intensity];
33773
33814
  const physicsDriven = intensity === "dramatic" && !reducedMotion;
33774
- const [pool, setPool] = (0, import_react73.useState)(participants);
33815
+ const [draftSeed] = (0, import_react73.useState)(
33816
+ () => computeDraftSeed(participants, initialDrawnIds, removeWinners)
33817
+ );
33818
+ const [pool, setPool] = (0, import_react73.useState)(
33819
+ draftSeed ? draftSeed.pool : participants
33820
+ );
33775
33821
  const [rotation, setRotation] = (0, import_react73.useState)(0);
33776
33822
  const [reelY, setReelY] = (0, import_react73.useState)(0);
33777
33823
  const [spinning, setSpinning] = (0, import_react73.useState)(false);
33778
- const [winner, setWinner] = (0, import_react73.useState)(null);
33779
- const [history, setHistory] = (0, import_react73.useState)([]);
33824
+ const [winner, setWinner] = (0, import_react73.useState)(
33825
+ draftSeed ? draftSeed.history[0] ?? null : null
33826
+ );
33827
+ const [history, setHistory] = (0, import_react73.useState)(
33828
+ draftSeed ? draftSeed.history : []
33829
+ );
33780
33830
  const [revealKey, setRevealKey] = (0, import_react73.useState)(0);
33781
33831
  const [particles, setParticles] = (0, import_react73.useState)([]);
33782
33832
  const winnerTimer = (0, import_react73.useRef)(null);
@@ -33788,7 +33838,9 @@ function RandomPlayerPicker({
33788
33838
  const tickAudioRef = (0, import_react73.useRef)(null);
33789
33839
  const winAudioRef = (0, import_react73.useRef)(null);
33790
33840
  const containerRef = (0, import_react73.useRef)(null);
33791
- const prevIdsRef = (0, import_react73.useRef)("");
33841
+ const prevIdsRef = (0, import_react73.useRef)(
33842
+ draftSeed ? participants.map((p) => p.id).join("|") : ""
33843
+ );
33792
33844
  const wheelSvgRef = (0, import_react73.useRef)(null);
33793
33845
  const reelTapeRef = (0, import_react73.useRef)(null);
33794
33846
  const rafRef = (0, import_react73.useRef)(null);
@@ -40706,6 +40758,7 @@ function RoundRobinScheduler({
40706
40758
  resumeRounds,
40707
40759
  onTeamsChange,
40708
40760
  onGenerate,
40761
+ onBeforeScheduleDiscard,
40709
40762
  labels,
40710
40763
  ariaLabel = "Round robin scheduler",
40711
40764
  className,
@@ -40741,23 +40794,29 @@ function RoundRobinScheduler({
40741
40794
  },
40742
40795
  [maxTeams, nameMaxLength]
40743
40796
  );
40797
+ const gateDiscard = (0, import_react79.useCallback)(
40798
+ (action) => rounds.length === 0 || !onBeforeScheduleDiscard || onBeforeScheduleDiscard(action),
40799
+ [rounds.length, onBeforeScheduleDiscard]
40800
+ );
40744
40801
  const addTeams = (0, import_react79.useCallback)(
40745
40802
  (raw) => {
40746
40803
  const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40747
40804
  if (names.length === 0) return;
40805
+ if (!gateDiscard("add")) return;
40748
40806
  setTeams((prev) => mergeNames(prev, names));
40749
40807
  setInput("");
40750
40808
  setRounds([]);
40751
40809
  },
40752
- [mergeNames]
40810
+ [mergeNames, gateDiscard]
40753
40811
  );
40754
40812
  const importTeams = (0, import_react79.useCallback)(
40755
40813
  (incoming) => {
40756
40814
  if (incoming.length === 0) return;
40815
+ if (!gateDiscard("import")) return;
40757
40816
  setTeams((prev) => mergeNames(prev, incoming));
40758
40817
  setRounds([]);
40759
40818
  },
40760
- [mergeNames]
40819
+ [mergeNames, gateDiscard]
40761
40820
  );
40762
40821
  const handleKeyDown = (0, import_react79.useCallback)(
40763
40822
  (e) => {
@@ -40768,10 +40827,14 @@ function RoundRobinScheduler({
40768
40827
  },
40769
40828
  [addTeams, input]
40770
40829
  );
40771
- const removeTeam = (0, import_react79.useCallback)((idx) => {
40772
- setTeams((prev) => prev.filter((_, i) => i !== idx));
40773
- setRounds([]);
40774
- }, []);
40830
+ const removeTeam = (0, import_react79.useCallback)(
40831
+ (idx) => {
40832
+ if (!gateDiscard("remove")) return;
40833
+ setTeams((prev) => prev.filter((_, i) => i !== idx));
40834
+ setRounds([]);
40835
+ },
40836
+ [gateDiscard]
40837
+ );
40775
40838
  const handleGenerate = (0, import_react79.useCallback)(() => {
40776
40839
  if (teams.length < minTeams) return;
40777
40840
  const result = generateSchedule(teams, L.roundLabel);
@@ -40779,15 +40842,17 @@ function RoundRobinScheduler({
40779
40842
  onGenerate?.(result);
40780
40843
  }, [teams, minTeams, L.roundLabel, onGenerate]);
40781
40844
  const handleShuffle = (0, import_react79.useCallback)(() => {
40845
+ if (!gateDiscard("shuffle")) return;
40782
40846
  setTeams((prev) => shuffle(prev));
40783
40847
  setRounds([]);
40784
- }, []);
40848
+ }, [gateDiscard]);
40785
40849
  const handleClear = (0, import_react79.useCallback)(() => {
40850
+ if (!gateDiscard("clear")) return;
40786
40851
  setTeams([]);
40787
40852
  setRounds([]);
40788
40853
  setInput("");
40789
40854
  inputRef.current?.focus();
40790
- }, []);
40855
+ }, [gateDiscard]);
40791
40856
  const canGenerate = teams.length >= minTeams;
40792
40857
  const rootStyle = {
40793
40858
  borderRadius: 16,
@@ -41248,6 +41313,8 @@ function Scoreboard({
41248
41313
  onRoundAdvance,
41249
41314
  onNewGame,
41250
41315
  onReset,
41316
+ onBeforeReset,
41317
+ onBeforeNewGame,
41251
41318
  nameMaxLength,
41252
41319
  labels,
41253
41320
  ariaLabel = "Scoreboard",
@@ -41319,19 +41386,21 @@ function Scoreboard({
41319
41386
  [minTeams]
41320
41387
  );
41321
41388
  const resetScores = (0, import_react80.useCallback)(() => {
41389
+ if (onBeforeReset && !onBeforeReset()) return;
41322
41390
  setTeams((prev) => prev.map((t) => ({ ...t, score: 0 })));
41323
41391
  setRound(1);
41324
41392
  setHistory([]);
41325
41393
  setHistoryOpen(false);
41326
41394
  onReset?.();
41327
- }, [onReset]);
41395
+ }, [onReset, onBeforeReset]);
41328
41396
  const newGame = (0, import_react80.useCallback)(() => {
41397
+ if (onBeforeNewGame && !onBeforeNewGame()) return;
41329
41398
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41330
41399
  setRound(1);
41331
41400
  setHistory([]);
41332
41401
  setHistoryOpen(false);
41333
41402
  onNewGame?.();
41334
- }, [seedCount, L.teamNamePrefix, onNewGame]);
41403
+ }, [seedCount, L.teamNamePrefix, onNewGame, onBeforeNewGame]);
41335
41404
  const nextRound = (0, import_react80.useCallback)(() => {
41336
41405
  const snapshot = {
41337
41406
  round,