@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.
package/dist/index.d.mts CHANGED
@@ -4706,6 +4706,17 @@ interface DiceRollerProps {
4706
4706
  showHistory?: boolean;
4707
4707
  /** Max entries in the history strip. Default 8. */
4708
4708
  historyMax?: number;
4709
+ /**
4710
+ * Seed the history strip from a persisted session — newest first, the
4711
+ * same entries consumers capture from `onRoll`. One-shot: read on
4712
+ * mount, ignored afterwards; clamped to `historyMax`. Only the strip is
4713
+ * seeded — the dice arena and result chips stay in their pre-roll state
4714
+ * (a settled pose can't be faithfully reproduced).
4715
+ */
4716
+ initialHistory?: Array<{
4717
+ vals: number[];
4718
+ total: number;
4719
+ }>;
4709
4720
  /**
4710
4721
  * Imperative handle — assign a ref and call `rollRef.current?.()`
4711
4722
  * to trigger a roll from outside the component. Works with any
@@ -4749,7 +4760,7 @@ declare function d6LandingRotation(value: number): {
4749
4760
  x: number;
4750
4761
  y: number;
4751
4762
  };
4752
- 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;
4763
+ 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;
4753
4764
  declare namespace DiceRoller {
4754
4765
  var displayName: string;
4755
4766
  }
@@ -4927,6 +4938,25 @@ interface CoinFlipperProps {
4927
4938
  showHistory?: boolean;
4928
4939
  /** Max entries in history. Default 20. */
4929
4940
  historyMax?: number;
4941
+ /**
4942
+ * Seed the history strip from a persisted session — newest first, the
4943
+ * results consumers capture from `onFlip`. One-shot: read on mount,
4944
+ * ignored afterwards; clamped to `historyMax`. The coin face stays in
4945
+ * its pre-flip pose.
4946
+ */
4947
+ initialHistory?: CoinResult[];
4948
+ /**
4949
+ * Seed the H/T counters from a persisted session. Kept separate from
4950
+ * `initialHistory` because stats count ALL flips while the history
4951
+ * strip is capped at `historyMax`. One-shot: read on mount. When
4952
+ * omitted alongside `initialHistory`, the counters are derived from the
4953
+ * seeded strip instead (a lower bound if the strip was capped) — the
4954
+ * history panel only renders while the counters are non-zero.
4955
+ */
4956
+ initialStats?: {
4957
+ heads: number;
4958
+ tails: number;
4959
+ };
4930
4960
  /** Imperative handle — call `flipRef.current?.()` to flip externally. */
4931
4961
  flipRef?: MutableRefObject<(() => void) | null>;
4932
4962
  /** Accessible label. */
@@ -4934,7 +4964,7 @@ interface CoinFlipperProps {
4934
4964
  /** CSS class on root element. */
4935
4965
  className?: string;
4936
4966
  }
4937
- declare function CoinFlipper({ theme, darkMode, size, autoFlip, onFlip, trigger, label, headsLabel, tailsLabel, showHistory, historyMax, flipRef, ariaLabel, className, }: CoinFlipperProps): react_jsx_runtime.JSX.Element;
4967
+ 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;
4938
4968
 
4939
4969
  interface RandomPlayerParticipant {
4940
4970
  /** Stable identifier — required for keying and winner equality. */
@@ -5040,6 +5070,17 @@ interface RandomPlayerPickerProps {
5040
5070
  label?: string;
5041
5071
  /** Pop each winner from the pool after reveal. Default false. */
5042
5072
  removeWinners?: boolean;
5073
+ /**
5074
+ * Resume a draft from a persisted session: participant ids already
5075
+ * drawn, newest first (the history consumers capture from `onPick`).
5076
+ * One-shot: read on mount, ignored afterwards. Only applied when
5077
+ * `removeWinners` is true — the drawn participants are removed from the
5078
+ * starting pool, seeded into the pick history, and the newest one is
5079
+ * shown on the winner card. Fail-closed: if any id doesn't resolve
5080
+ * against `participants` (or ids repeat), the whole seed is ignored
5081
+ * rather than half-restoring a stale draft.
5082
+ */
5083
+ initialDrawnIds?: string[];
5043
5084
  /** Spin animation duration in ms. Default 5000. */
5044
5085
  spinDuration?: number;
5045
5086
  /** Lower bound on full revolutions. Default 4. */
@@ -5129,7 +5170,7 @@ interface RandomPlayerPickerProps {
5129
5170
  /** Accessible label for the wheel root. */
5130
5171
  ariaLabel?: string;
5131
5172
  }
5132
- 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;
5173
+ 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;
5133
5174
 
5134
5175
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5135
5176
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -5792,6 +5833,15 @@ interface RoundRobinSchedulerProps {
5792
5833
  onTeamsChange?: (teams: string[]) => void;
5793
5834
  /** Called when a new schedule is generated. */
5794
5835
  onGenerate?: (rounds: RoundRobinRound[]) => void;
5836
+ /**
5837
+ * Gate for roster edits (add / import / remove / shuffle / clear) that
5838
+ * would discard a generated schedule. Called synchronously BEFORE the
5839
+ * edit, and only while a schedule is on screen; return `false` to abort
5840
+ * the edit and keep the schedule (e.g. after a declined
5841
+ * `window.confirm`). Absent = edits discard the schedule silently, as
5842
+ * before. Explicit "Generate Schedule" regenerations are never gated.
5843
+ */
5844
+ onBeforeScheduleDiscard?: (action: "add" | "import" | "remove" | "shuffle" | "clear") => boolean;
5795
5845
  /** Localized / customized labels. */
5796
5846
  labels?: RoundRobinLabels;
5797
5847
  /** Accessible label on the root region. */
@@ -5802,7 +5852,7 @@ interface RoundRobinSchedulerProps {
5802
5852
  * roster from a spreadsheet. Default `true`. */
5803
5853
  enableImport?: boolean;
5804
5854
  }
5805
- declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
5855
+ declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, onBeforeScheduleDiscard, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
5806
5856
  declare namespace RoundRobinScheduler {
5807
5857
  var displayName: string;
5808
5858
  }
@@ -5866,6 +5916,14 @@ interface ScoreboardProps {
5866
5916
  * `onRoundAdvance` MUST clear that state here or their saved copy
5867
5917
  * diverges from the screen. */
5868
5918
  onReset?: () => void;
5919
+ /** Gate for the built-in Reset button. Called synchronously BEFORE any
5920
+ * state is cleared; return `false` to abort (e.g. after a declined
5921
+ * `window.confirm` when recorded rounds would be discarded). Absent =
5922
+ * Reset applies immediately, as before. */
5923
+ onBeforeReset?: () => boolean;
5924
+ /** Gate for the built-in New Game button — same contract as
5925
+ * `onBeforeReset`. */
5926
+ onBeforeNewGame?: () => boolean;
5869
5927
  /** Max characters accepted in a team-name input. Unlimited by default. */
5870
5928
  nameMaxLength?: number;
5871
5929
  /** Localized / customized labels. */
@@ -5875,7 +5933,7 @@ interface ScoreboardProps {
5875
5933
  /** CSS class on root element. */
5876
5934
  className?: string;
5877
5935
  }
5878
- 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;
5936
+ 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;
5879
5937
  declare namespace Scoreboard {
5880
5938
  var displayName: string;
5881
5939
  }
package/dist/index.d.ts CHANGED
@@ -4706,6 +4706,17 @@ interface DiceRollerProps {
4706
4706
  showHistory?: boolean;
4707
4707
  /** Max entries in the history strip. Default 8. */
4708
4708
  historyMax?: number;
4709
+ /**
4710
+ * Seed the history strip from a persisted session — newest first, the
4711
+ * same entries consumers capture from `onRoll`. One-shot: read on
4712
+ * mount, ignored afterwards; clamped to `historyMax`. Only the strip is
4713
+ * seeded — the dice arena and result chips stay in their pre-roll state
4714
+ * (a settled pose can't be faithfully reproduced).
4715
+ */
4716
+ initialHistory?: Array<{
4717
+ vals: number[];
4718
+ total: number;
4719
+ }>;
4709
4720
  /**
4710
4721
  * Imperative handle — assign a ref and call `rollRef.current?.()`
4711
4722
  * to trigger a roll from outside the component. Works with any
@@ -4749,7 +4760,7 @@ declare function d6LandingRotation(value: number): {
4749
4760
  x: number;
4750
4761
  y: number;
4751
4762
  };
4752
- 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;
4763
+ 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;
4753
4764
  declare namespace DiceRoller {
4754
4765
  var displayName: string;
4755
4766
  }
@@ -4927,6 +4938,25 @@ interface CoinFlipperProps {
4927
4938
  showHistory?: boolean;
4928
4939
  /** Max entries in history. Default 20. */
4929
4940
  historyMax?: number;
4941
+ /**
4942
+ * Seed the history strip from a persisted session — newest first, the
4943
+ * results consumers capture from `onFlip`. One-shot: read on mount,
4944
+ * ignored afterwards; clamped to `historyMax`. The coin face stays in
4945
+ * its pre-flip pose.
4946
+ */
4947
+ initialHistory?: CoinResult[];
4948
+ /**
4949
+ * Seed the H/T counters from a persisted session. Kept separate from
4950
+ * `initialHistory` because stats count ALL flips while the history
4951
+ * strip is capped at `historyMax`. One-shot: read on mount. When
4952
+ * omitted alongside `initialHistory`, the counters are derived from the
4953
+ * seeded strip instead (a lower bound if the strip was capped) — the
4954
+ * history panel only renders while the counters are non-zero.
4955
+ */
4956
+ initialStats?: {
4957
+ heads: number;
4958
+ tails: number;
4959
+ };
4930
4960
  /** Imperative handle — call `flipRef.current?.()` to flip externally. */
4931
4961
  flipRef?: MutableRefObject<(() => void) | null>;
4932
4962
  /** Accessible label. */
@@ -4934,7 +4964,7 @@ interface CoinFlipperProps {
4934
4964
  /** CSS class on root element. */
4935
4965
  className?: string;
4936
4966
  }
4937
- declare function CoinFlipper({ theme, darkMode, size, autoFlip, onFlip, trigger, label, headsLabel, tailsLabel, showHistory, historyMax, flipRef, ariaLabel, className, }: CoinFlipperProps): react_jsx_runtime.JSX.Element;
4967
+ 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;
4938
4968
 
4939
4969
  interface RandomPlayerParticipant {
4940
4970
  /** Stable identifier — required for keying and winner equality. */
@@ -5040,6 +5070,17 @@ interface RandomPlayerPickerProps {
5040
5070
  label?: string;
5041
5071
  /** Pop each winner from the pool after reveal. Default false. */
5042
5072
  removeWinners?: boolean;
5073
+ /**
5074
+ * Resume a draft from a persisted session: participant ids already
5075
+ * drawn, newest first (the history consumers capture from `onPick`).
5076
+ * One-shot: read on mount, ignored afterwards. Only applied when
5077
+ * `removeWinners` is true — the drawn participants are removed from the
5078
+ * starting pool, seeded into the pick history, and the newest one is
5079
+ * shown on the winner card. Fail-closed: if any id doesn't resolve
5080
+ * against `participants` (or ids repeat), the whole seed is ignored
5081
+ * rather than half-restoring a stale draft.
5082
+ */
5083
+ initialDrawnIds?: string[];
5043
5084
  /** Spin animation duration in ms. Default 5000. */
5044
5085
  spinDuration?: number;
5045
5086
  /** Lower bound on full revolutions. Default 4. */
@@ -5129,7 +5170,7 @@ interface RandomPlayerPickerProps {
5129
5170
  /** Accessible label for the wheel root. */
5130
5171
  ariaLabel?: string;
5131
5172
  }
5132
- 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;
5173
+ 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;
5133
5174
 
5134
5175
  type HeroBannerTransition = "fade" | "slide" | "zoom" | "blur" | "reveal" | "push";
5135
5176
  type HeroBannerImageFilter = "none" | "cinematic" | "dramatic" | "cool" | "warm" | "desaturated" | "noir" | "vibrant";
@@ -5792,6 +5833,15 @@ interface RoundRobinSchedulerProps {
5792
5833
  onTeamsChange?: (teams: string[]) => void;
5793
5834
  /** Called when a new schedule is generated. */
5794
5835
  onGenerate?: (rounds: RoundRobinRound[]) => void;
5836
+ /**
5837
+ * Gate for roster edits (add / import / remove / shuffle / clear) that
5838
+ * would discard a generated schedule. Called synchronously BEFORE the
5839
+ * edit, and only while a schedule is on screen; return `false` to abort
5840
+ * the edit and keep the schedule (e.g. after a declined
5841
+ * `window.confirm`). Absent = edits discard the schedule silently, as
5842
+ * before. Explicit "Generate Schedule" regenerations are never gated.
5843
+ */
5844
+ onBeforeScheduleDiscard?: (action: "add" | "import" | "remove" | "shuffle" | "clear") => boolean;
5795
5845
  /** Localized / customized labels. */
5796
5846
  labels?: RoundRobinLabels;
5797
5847
  /** Accessible label on the root region. */
@@ -5802,7 +5852,7 @@ interface RoundRobinSchedulerProps {
5802
5852
  * roster from a spreadsheet. Default `true`. */
5803
5853
  enableImport?: boolean;
5804
5854
  }
5805
- declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
5855
+ declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, onBeforeScheduleDiscard, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
5806
5856
  declare namespace RoundRobinScheduler {
5807
5857
  var displayName: string;
5808
5858
  }
@@ -5866,6 +5916,14 @@ interface ScoreboardProps {
5866
5916
  * `onRoundAdvance` MUST clear that state here or their saved copy
5867
5917
  * diverges from the screen. */
5868
5918
  onReset?: () => void;
5919
+ /** Gate for the built-in Reset button. Called synchronously BEFORE any
5920
+ * state is cleared; return `false` to abort (e.g. after a declined
5921
+ * `window.confirm` when recorded rounds would be discarded). Absent =
5922
+ * Reset applies immediately, as before. */
5923
+ onBeforeReset?: () => boolean;
5924
+ /** Gate for the built-in New Game button — same contract as
5925
+ * `onBeforeReset`. */
5926
+ onBeforeNewGame?: () => boolean;
5869
5927
  /** Max characters accepted in a team-name input. Unlimited by default. */
5870
5928
  nameMaxLength?: number;
5871
5929
  /** Localized / customized labels. */
@@ -5875,7 +5933,7 @@ interface ScoreboardProps {
5875
5933
  /** CSS class on root element. */
5876
5934
  className?: string;
5877
5935
  }
5878
- 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;
5936
+ 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;
5879
5937
  declare namespace Scoreboard {
5880
5938
  var displayName: string;
5881
5939
  }
package/dist/index.js CHANGED
@@ -30710,6 +30710,7 @@ function DiceRoller({
30710
30710
  iconEmoji = "\u{1F3B2}",
30711
30711
  showHistory = false,
30712
30712
  historyMax = 8,
30713
+ initialHistory,
30713
30714
  rollRef,
30714
30715
  arenaHeight: arenaHeightProp,
30715
30716
  hideResults = false,
@@ -30721,7 +30722,9 @@ function DiceRoller({
30721
30722
  const dark = useDarkMode(darkMode);
30722
30723
  const [rolling, setRolling] = (0, import_react68.useState)(false);
30723
30724
  const [results, setResults] = (0, import_react68.useState)(null);
30724
- const [history, setHistory] = (0, import_react68.useState)([]);
30725
+ const [history, setHistory] = (0, import_react68.useState)(
30726
+ () => initialHistory && initialHistory.length > 0 ? initialHistory.slice(0, historyMax) : []
30727
+ );
30725
30728
  const dieWrapperRefs = (0, import_react68.useRef)([]);
30726
30729
  const cubeRefs = (0, import_react68.useRef)([]);
30727
30730
  const shadowRefs = (0, import_react68.useRef)([]);
@@ -30895,7 +30898,10 @@ function DiceRoller({
30895
30898
  defaultPositions,
30896
30899
  half
30897
30900
  ]);
30901
+ const prevSidesRef = (0, import_react68.useRef)(sides);
30898
30902
  (0, import_react68.useEffect)(() => {
30903
+ if (prevSidesRef.current === sides) return;
30904
+ prevSidesRef.current = sides;
30899
30905
  setHistory([]);
30900
30906
  }, [sides]);
30901
30907
  const roll = (0, import_react68.useCallback)(() => {
@@ -32964,6 +32970,8 @@ function CoinFlipper({
32964
32970
  tailsLabel = "Tails",
32965
32971
  showHistory = false,
32966
32972
  historyMax = 20,
32973
+ initialHistory,
32974
+ initialStats,
32967
32975
  flipRef,
32968
32976
  ariaLabel,
32969
32977
  className = ""
@@ -32972,8 +32980,23 @@ function CoinFlipper({
32972
32980
  const tc = typeof theme5 === "string" ? PRESETS3[theme5] || PRESETS3.gold : theme5;
32973
32981
  const [result, setResult] = (0, import_react70.useState)(null);
32974
32982
  const [flipping, setFlipping] = (0, import_react70.useState)(false);
32975
- const [history, setHistory] = (0, import_react70.useState)([]);
32976
- const [stats, setStats] = (0, import_react70.useState)({ heads: 0, tails: 0 });
32983
+ const [history, setHistory] = (0, import_react70.useState)(
32984
+ () => initialHistory && initialHistory.length > 0 ? initialHistory.slice(0, historyMax).map((result2, i) => ({ result: result2, id: -(i + 1) })) : []
32985
+ );
32986
+ const [stats, setStats] = (0, import_react70.useState)(() => {
32987
+ if (initialStats) {
32988
+ return {
32989
+ heads: Math.max(0, Math.floor(initialStats.heads)),
32990
+ tails: Math.max(0, Math.floor(initialStats.tails))
32991
+ };
32992
+ }
32993
+ if (initialHistory && initialHistory.length > 0) {
32994
+ const seeded = initialHistory.slice(0, historyMax);
32995
+ const heads = seeded.filter((r) => r === "heads").length;
32996
+ return { heads, tails: seeded.length - heads };
32997
+ }
32998
+ return { heads: 0, tails: 0 };
32999
+ });
32977
33000
  const flipIdRef = (0, import_react70.useRef)(0);
32978
33001
  const coinRef = (0, import_react70.useRef)(null);
32979
33002
  const shadowRef = (0, import_react70.useRef)(null);
@@ -33885,6 +33908,23 @@ function ReelStage({
33885
33908
  }
33886
33909
  );
33887
33910
  }
33911
+ function computeDraftSeed(participants, drawnIds, removeWinners) {
33912
+ if (!removeWinners || !drawnIds || drawnIds.length === 0) return null;
33913
+ if (new Set(drawnIds).size !== drawnIds.length) return null;
33914
+ const byId = new Map(participants.map((p) => [p.id, p]));
33915
+ const drawn = [];
33916
+ for (const id of drawnIds) {
33917
+ const p = byId.get(id);
33918
+ if (!p) return null;
33919
+ drawn.push(p);
33920
+ }
33921
+ const drawnSet = new Set(drawnIds);
33922
+ return {
33923
+ pool: participants.filter((p) => !drawnSet.has(p.id)),
33924
+ // Same cap as the live history (see the pop/promote paths).
33925
+ history: drawn.slice(0, 20)
33926
+ };
33927
+ }
33888
33928
  function RandomPlayerPicker({
33889
33929
  participants,
33890
33930
  mode = "wheel",
@@ -33899,6 +33939,7 @@ function RandomPlayerPicker({
33899
33939
  trigger = "button",
33900
33940
  label = "Spin",
33901
33941
  removeWinners = false,
33942
+ initialDrawnIds,
33902
33943
  spinDuration = 5e3,
33903
33944
  minRevolutions = 4,
33904
33945
  maxRevolutions = 7,
@@ -33925,12 +33966,21 @@ function RandomPlayerPicker({
33925
33966
  const reducedMotion = usePrefersReducedMotion();
33926
33967
  const intensityCfg = INTENSITY_CONFIG[intensity];
33927
33968
  const physicsDriven = intensity === "dramatic" && !reducedMotion;
33928
- const [pool, setPool] = (0, import_react71.useState)(participants);
33969
+ const [draftSeed] = (0, import_react71.useState)(
33970
+ () => computeDraftSeed(participants, initialDrawnIds, removeWinners)
33971
+ );
33972
+ const [pool, setPool] = (0, import_react71.useState)(
33973
+ draftSeed ? draftSeed.pool : participants
33974
+ );
33929
33975
  const [rotation, setRotation] = (0, import_react71.useState)(0);
33930
33976
  const [reelY, setReelY] = (0, import_react71.useState)(0);
33931
33977
  const [spinning, setSpinning] = (0, import_react71.useState)(false);
33932
- const [winner, setWinner] = (0, import_react71.useState)(null);
33933
- const [history, setHistory] = (0, import_react71.useState)([]);
33978
+ const [winner, setWinner] = (0, import_react71.useState)(
33979
+ draftSeed ? draftSeed.history[0] ?? null : null
33980
+ );
33981
+ const [history, setHistory] = (0, import_react71.useState)(
33982
+ draftSeed ? draftSeed.history : []
33983
+ );
33934
33984
  const [revealKey, setRevealKey] = (0, import_react71.useState)(0);
33935
33985
  const [particles, setParticles] = (0, import_react71.useState)([]);
33936
33986
  const winnerTimer = (0, import_react71.useRef)(null);
@@ -33942,7 +33992,9 @@ function RandomPlayerPicker({
33942
33992
  const tickAudioRef = (0, import_react71.useRef)(null);
33943
33993
  const winAudioRef = (0, import_react71.useRef)(null);
33944
33994
  const containerRef = (0, import_react71.useRef)(null);
33945
- const prevIdsRef = (0, import_react71.useRef)("");
33995
+ const prevIdsRef = (0, import_react71.useRef)(
33996
+ draftSeed ? participants.map((p) => p.id).join("|") : ""
33997
+ );
33946
33998
  const wheelSvgRef = (0, import_react71.useRef)(null);
33947
33999
  const reelTapeRef = (0, import_react71.useRef)(null);
33948
34000
  const rafRef = (0, import_react71.useRef)(null);
@@ -41065,6 +41117,7 @@ function RoundRobinScheduler({
41065
41117
  resumeRounds,
41066
41118
  onTeamsChange,
41067
41119
  onGenerate,
41120
+ onBeforeScheduleDiscard,
41068
41121
  labels,
41069
41122
  ariaLabel = "Round robin scheduler",
41070
41123
  className,
@@ -41100,23 +41153,29 @@ function RoundRobinScheduler({
41100
41153
  },
41101
41154
  [maxTeams, nameMaxLength]
41102
41155
  );
41156
+ const gateDiscard = (0, import_react77.useCallback)(
41157
+ (action) => rounds.length === 0 || !onBeforeScheduleDiscard || onBeforeScheduleDiscard(action),
41158
+ [rounds.length, onBeforeScheduleDiscard]
41159
+ );
41103
41160
  const addTeams = (0, import_react77.useCallback)(
41104
41161
  (raw) => {
41105
41162
  const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
41106
41163
  if (names.length === 0) return;
41164
+ if (!gateDiscard("add")) return;
41107
41165
  setTeams((prev) => mergeNames(prev, names));
41108
41166
  setInput("");
41109
41167
  setRounds([]);
41110
41168
  },
41111
- [mergeNames]
41169
+ [mergeNames, gateDiscard]
41112
41170
  );
41113
41171
  const importTeams = (0, import_react77.useCallback)(
41114
41172
  (incoming) => {
41115
41173
  if (incoming.length === 0) return;
41174
+ if (!gateDiscard("import")) return;
41116
41175
  setTeams((prev) => mergeNames(prev, incoming));
41117
41176
  setRounds([]);
41118
41177
  },
41119
- [mergeNames]
41178
+ [mergeNames, gateDiscard]
41120
41179
  );
41121
41180
  const handleKeyDown = (0, import_react77.useCallback)(
41122
41181
  (e) => {
@@ -41127,10 +41186,14 @@ function RoundRobinScheduler({
41127
41186
  },
41128
41187
  [addTeams, input]
41129
41188
  );
41130
- const removeTeam = (0, import_react77.useCallback)((idx) => {
41131
- setTeams((prev) => prev.filter((_, i) => i !== idx));
41132
- setRounds([]);
41133
- }, []);
41189
+ const removeTeam = (0, import_react77.useCallback)(
41190
+ (idx) => {
41191
+ if (!gateDiscard("remove")) return;
41192
+ setTeams((prev) => prev.filter((_, i) => i !== idx));
41193
+ setRounds([]);
41194
+ },
41195
+ [gateDiscard]
41196
+ );
41134
41197
  const handleGenerate = (0, import_react77.useCallback)(() => {
41135
41198
  if (teams.length < minTeams) return;
41136
41199
  const result = generateSchedule(teams, L.roundLabel);
@@ -41138,15 +41201,17 @@ function RoundRobinScheduler({
41138
41201
  onGenerate?.(result);
41139
41202
  }, [teams, minTeams, L.roundLabel, onGenerate]);
41140
41203
  const handleShuffle = (0, import_react77.useCallback)(() => {
41204
+ if (!gateDiscard("shuffle")) return;
41141
41205
  setTeams((prev) => shuffle(prev));
41142
41206
  setRounds([]);
41143
- }, []);
41207
+ }, [gateDiscard]);
41144
41208
  const handleClear = (0, import_react77.useCallback)(() => {
41209
+ if (!gateDiscard("clear")) return;
41145
41210
  setTeams([]);
41146
41211
  setRounds([]);
41147
41212
  setInput("");
41148
41213
  inputRef.current?.focus();
41149
- }, []);
41214
+ }, [gateDiscard]);
41150
41215
  const canGenerate = teams.length >= minTeams;
41151
41216
  const rootStyle = {
41152
41217
  borderRadius: 16,
@@ -41607,6 +41672,8 @@ function Scoreboard({
41607
41672
  onRoundAdvance,
41608
41673
  onNewGame,
41609
41674
  onReset,
41675
+ onBeforeReset,
41676
+ onBeforeNewGame,
41610
41677
  nameMaxLength,
41611
41678
  labels,
41612
41679
  ariaLabel = "Scoreboard",
@@ -41678,19 +41745,21 @@ function Scoreboard({
41678
41745
  [minTeams]
41679
41746
  );
41680
41747
  const resetScores = (0, import_react78.useCallback)(() => {
41748
+ if (onBeforeReset && !onBeforeReset()) return;
41681
41749
  setTeams((prev) => prev.map((t) => ({ ...t, score: 0 })));
41682
41750
  setRound(1);
41683
41751
  setHistory([]);
41684
41752
  setHistoryOpen(false);
41685
41753
  onReset?.();
41686
- }, [onReset]);
41754
+ }, [onReset, onBeforeReset]);
41687
41755
  const newGame = (0, import_react78.useCallback)(() => {
41756
+ if (onBeforeNewGame && !onBeforeNewGame()) return;
41688
41757
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41689
41758
  setRound(1);
41690
41759
  setHistory([]);
41691
41760
  setHistoryOpen(false);
41692
41761
  onNewGame?.();
41693
- }, [seedCount, L.teamNamePrefix, onNewGame]);
41762
+ }, [seedCount, L.teamNamePrefix, onNewGame, onBeforeNewGame]);
41694
41763
  const nextRound = (0, import_react78.useCallback)(() => {
41695
41764
  const snapshot = {
41696
41765
  round,