@digilogiclabs/saas-factory-ui 2.3.0 → 2.4.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.d.mts CHANGED
@@ -4626,7 +4626,9 @@ declare function TiltCard({ accent, accentSecondary, topLabel, icon, title, subt
4626
4626
  * - Three trigger modes: built-in button / click anywhere / external
4627
4627
  * imperative handle via `rollRef.current()`
4628
4628
  * - Optional roll history strip
4629
- * - Honors `prefers-reduced-motion` (skips physics animation)
4629
+ * - Deliberately tumbles even under `prefers-reduced-motion` a dice
4630
+ * roll is a safe animation and skipping it leaves no feedback at all
4631
+ * (see the note inside `roll()`)
4630
4632
  * - Zero Tailwind dependency — all styles inline
4631
4633
  *
4632
4634
  * Ported to TypeScript and generalized for saas-factory-ui from a
@@ -5728,6 +5730,14 @@ interface RoundRobinSchedulerProps {
5728
5730
  darkMode?: boolean;
5729
5731
  /** Minimum teams required to generate a schedule. Default 3. */
5730
5732
  minTeams?: number;
5733
+ /** Maximum roster size. Adds/imports beyond this are ignored. Unlimited
5734
+ * by default — consumers persisting the roster against a capped schema
5735
+ * should set this to the schema cap. */
5736
+ maxTeams?: number;
5737
+ /** Max characters per team name. Longer typed/imported names are
5738
+ * truncated (matching native input maxLength paste behavior).
5739
+ * Unlimited by default. */
5740
+ nameMaxLength?: number;
5731
5741
  /** Initial roster. Uncontrolled. */
5732
5742
  defaultTeams?: string[];
5733
5743
  /**
@@ -5753,7 +5763,7 @@ interface RoundRobinSchedulerProps {
5753
5763
  * roster from a spreadsheet. Default `true`. */
5754
5764
  enableImport?: boolean;
5755
5765
  }
5756
- declare function RoundRobinScheduler({ theme, darkMode, minTeams, defaultTeams, resumeRounds, onTeamsChange, onGenerate, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
5766
+ declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
5757
5767
  declare namespace RoundRobinScheduler {
5758
5768
  var displayName: string;
5759
5769
  }
@@ -5812,6 +5822,13 @@ interface ScoreboardProps {
5812
5822
  onRoundAdvance?: (snapshot: ScoreboardRoundSnapshot) => void;
5813
5823
  /** Called when the user presses "New Game". */
5814
5824
  onNewGame?: () => void;
5825
+ /** Called when the user presses "Reset" (scores zeroed, round counter and
5826
+ * history cleared, names kept). Consumers persisting round/history via
5827
+ * `onRoundAdvance` MUST clear that state here or their saved copy
5828
+ * diverges from the screen. */
5829
+ onReset?: () => void;
5830
+ /** Max characters accepted in a team-name input. Unlimited by default. */
5831
+ nameMaxLength?: number;
5815
5832
  /** Localized / customized labels. */
5816
5833
  labels?: ScoreboardLabels;
5817
5834
  /** Accessible label on the root region. */
@@ -5819,7 +5836,7 @@ interface ScoreboardProps {
5819
5836
  /** CSS class on root element. */
5820
5837
  className?: string;
5821
5838
  }
5822
- declare function Scoreboard({ theme, darkMode, initialTeamCount, minTeams, maxTeams, scoreButtons, defaultTeams, initialRound, initialHistory, onTeamsChange, onRoundAdvance, onNewGame, labels, ariaLabel, className, }: ScoreboardProps): react_jsx_runtime.JSX.Element;
5839
+ 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;
5823
5840
  declare namespace Scoreboard {
5824
5841
  var displayName: string;
5825
5842
  }
package/dist/index.d.ts CHANGED
@@ -4626,7 +4626,9 @@ declare function TiltCard({ accent, accentSecondary, topLabel, icon, title, subt
4626
4626
  * - Three trigger modes: built-in button / click anywhere / external
4627
4627
  * imperative handle via `rollRef.current()`
4628
4628
  * - Optional roll history strip
4629
- * - Honors `prefers-reduced-motion` (skips physics animation)
4629
+ * - Deliberately tumbles even under `prefers-reduced-motion` a dice
4630
+ * roll is a safe animation and skipping it leaves no feedback at all
4631
+ * (see the note inside `roll()`)
4630
4632
  * - Zero Tailwind dependency — all styles inline
4631
4633
  *
4632
4634
  * Ported to TypeScript and generalized for saas-factory-ui from a
@@ -5728,6 +5730,14 @@ interface RoundRobinSchedulerProps {
5728
5730
  darkMode?: boolean;
5729
5731
  /** Minimum teams required to generate a schedule. Default 3. */
5730
5732
  minTeams?: number;
5733
+ /** Maximum roster size. Adds/imports beyond this are ignored. Unlimited
5734
+ * by default — consumers persisting the roster against a capped schema
5735
+ * should set this to the schema cap. */
5736
+ maxTeams?: number;
5737
+ /** Max characters per team name. Longer typed/imported names are
5738
+ * truncated (matching native input maxLength paste behavior).
5739
+ * Unlimited by default. */
5740
+ nameMaxLength?: number;
5731
5741
  /** Initial roster. Uncontrolled. */
5732
5742
  defaultTeams?: string[];
5733
5743
  /**
@@ -5753,7 +5763,7 @@ interface RoundRobinSchedulerProps {
5753
5763
  * roster from a spreadsheet. Default `true`. */
5754
5764
  enableImport?: boolean;
5755
5765
  }
5756
- declare function RoundRobinScheduler({ theme, darkMode, minTeams, defaultTeams, resumeRounds, onTeamsChange, onGenerate, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
5766
+ declare function RoundRobinScheduler({ theme, darkMode, minTeams, maxTeams, nameMaxLength, defaultTeams, resumeRounds, onTeamsChange, onGenerate, labels, ariaLabel, className, enableImport, }: RoundRobinSchedulerProps): react_jsx_runtime.JSX.Element;
5757
5767
  declare namespace RoundRobinScheduler {
5758
5768
  var displayName: string;
5759
5769
  }
@@ -5812,6 +5822,13 @@ interface ScoreboardProps {
5812
5822
  onRoundAdvance?: (snapshot: ScoreboardRoundSnapshot) => void;
5813
5823
  /** Called when the user presses "New Game". */
5814
5824
  onNewGame?: () => void;
5825
+ /** Called when the user presses "Reset" (scores zeroed, round counter and
5826
+ * history cleared, names kept). Consumers persisting round/history via
5827
+ * `onRoundAdvance` MUST clear that state here or their saved copy
5828
+ * diverges from the screen. */
5829
+ onReset?: () => void;
5830
+ /** Max characters accepted in a team-name input. Unlimited by default. */
5831
+ nameMaxLength?: number;
5815
5832
  /** Localized / customized labels. */
5816
5833
  labels?: ScoreboardLabels;
5817
5834
  /** Accessible label on the root region. */
@@ -5819,7 +5836,7 @@ interface ScoreboardProps {
5819
5836
  /** CSS class on root element. */
5820
5837
  className?: string;
5821
5838
  }
5822
- declare function Scoreboard({ theme, darkMode, initialTeamCount, minTeams, maxTeams, scoreButtons, defaultTeams, initialRound, initialHistory, onTeamsChange, onRoundAdvance, onNewGame, labels, ariaLabel, className, }: ScoreboardProps): react_jsx_runtime.JSX.Element;
5839
+ 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;
5823
5840
  declare namespace Scoreboard {
5824
5841
  var displayName: string;
5825
5842
  }
package/dist/index.js CHANGED
@@ -30719,7 +30719,6 @@ function DiceRoller({
30719
30719
  }) {
30720
30720
  const clampedCount = Math.max(1, Math.min(MAX_COUNT, count2));
30721
30721
  const dark = useDarkMode(darkMode);
30722
- const reducedMotion = usePrefersReducedMotion();
30723
30722
  const [rolling, setRolling] = (0, import_react68.useState)(false);
30724
30723
  const [results, setResults] = (0, import_react68.useState)(null);
30725
30724
  const [history, setHistory] = (0, import_react68.useState)([]);
@@ -30856,6 +30855,14 @@ function DiceRoller({
30856
30855
  []
30857
30856
  );
30858
30857
  (0, import_react68.useEffect)(() => {
30858
+ if (timersRef.current.length > 0) {
30859
+ timersRef.current.forEach((t) => clearTimeout(t));
30860
+ timersRef.current = [];
30861
+ }
30862
+ if (rollingRef.current) {
30863
+ rollingRef.current = false;
30864
+ setRolling(false);
30865
+ }
30859
30866
  const rawW = arenaRef.current?.offsetWidth ?? 0;
30860
30867
  const rawH = arenaRef.current?.offsetHeight ?? 0;
30861
30868
  const w = rawW > 20 ? rawW : arena.w > 20 ? arena.w : 400;
@@ -31012,7 +31019,6 @@ function DiceRoller({
31012
31019
  defaultPositions,
31013
31020
  effectiveSize,
31014
31021
  half,
31015
- reducedMotion,
31016
31022
  onRoll,
31017
31023
  writeCubeRotation,
31018
31024
  writeDiePosition,
@@ -33025,6 +33031,9 @@ function CoinFlipper({
33025
33031
  }, [flipping, onFlip, historyMax]);
33026
33032
  (0, import_react70.useEffect)(() => {
33027
33033
  if (flipRef) flipRef.current = flip;
33034
+ return () => {
33035
+ if (flipRef && flipRef.current === flip) flipRef.current = null;
33036
+ };
33028
33037
  }, [flip, flipRef]);
33029
33038
  (0, import_react70.useEffect)(() => {
33030
33039
  if (autoFlip) {
@@ -33916,6 +33925,8 @@ function RandomPlayerPicker({
33916
33925
  const particleTimer = (0, import_react71.useRef)(null);
33917
33926
  const popTimer = (0, import_react71.useRef)(null);
33918
33927
  const tickInterval = (0, import_react71.useRef)(null);
33928
+ const pendingPopIdxRef = (0, import_react71.useRef)(null);
33929
+ const removeWinnersRef = (0, import_react71.useRef)(removeWinners);
33919
33930
  const tickAudioRef = (0, import_react71.useRef)(null);
33920
33931
  const winAudioRef = (0, import_react71.useRef)(null);
33921
33932
  const containerRef = (0, import_react71.useRef)(null);
@@ -33954,10 +33965,27 @@ function RandomPlayerPicker({
33954
33965
  winAudioRef.current = null;
33955
33966
  };
33956
33967
  }, [winSoundUrl, soundVolume]);
33968
+ (0, import_react71.useEffect)(() => {
33969
+ removeWinnersRef.current = removeWinners;
33970
+ }, [removeWinners]);
33957
33971
  (0, import_react71.useEffect)(() => {
33958
33972
  const ids = participants.map((p) => p.id).join("|");
33959
33973
  const structuralChange = ids !== prevIdsRef.current;
33960
33974
  if (structuralChange) {
33975
+ if (winnerTimer.current) {
33976
+ clearTimeout(winnerTimer.current);
33977
+ winnerTimer.current = null;
33978
+ }
33979
+ if (tickInterval.current) {
33980
+ clearTimeout(tickInterval.current);
33981
+ tickInterval.current = null;
33982
+ }
33983
+ if (popTimer.current) {
33984
+ clearTimeout(popTimer.current);
33985
+ popTimer.current = null;
33986
+ }
33987
+ pendingPopIdxRef.current = null;
33988
+ setSpinning(false);
33961
33989
  setPool(participants);
33962
33990
  setHistory([]);
33963
33991
  setWinner(null);
@@ -34014,33 +34042,50 @@ function RandomPlayerPicker({
34014
34042
  [dark]
34015
34043
  );
34016
34044
  const spin = (0, import_react71.useCallback)(() => {
34017
- if (spinning || n < 2) return;
34045
+ if (spinning) return;
34046
+ let activePool = pool;
34047
+ if (popTimer.current) {
34048
+ clearTimeout(popTimer.current);
34049
+ popTimer.current = null;
34050
+ const pendingIdx = pendingPopIdxRef.current;
34051
+ pendingPopIdxRef.current = null;
34052
+ if (pendingIdx != null && pendingIdx >= 0 && pendingIdx < pool.length && removeWinnersRef.current) {
34053
+ activePool = pool.filter((_, i) => i !== pendingIdx);
34054
+ setPool(activePool);
34055
+ setWinner(null);
34056
+ }
34057
+ }
34058
+ const activeN = activePool.length;
34059
+ if (activeN < 2) return;
34060
+ const activeAnglePerWedge = 2 * Math.PI / activeN;
34061
+ const activeRotation = rotation;
34062
+ const activeReelY = reelY;
34018
34063
  onSpinStart?.();
34019
- const winnerIdx = weightedIndex(pool);
34064
+ const winnerIdx = weightedIndex(activePool);
34020
34065
  const minRev = minRevolutions + intensityCfg.revsBoost;
34021
34066
  const maxRev = maxRevolutions + intensityCfg.revsBoost;
34022
34067
  const minFullRevs = Math.ceil(minRev);
34023
34068
  const maxFullRevs = Math.max(minFullRevs, Math.floor(maxRev));
34024
34069
  const revolutions = minFullRevs + Math.floor(Math.random() * (maxFullRevs - minFullRevs + 1));
34025
- const wedgeMidRad = winnerIdx * anglePerWedge + anglePerWedge / 2;
34070
+ const wedgeMidRad = winnerIdx * activeAnglePerWedge + activeAnglePerWedge / 2;
34026
34071
  const wedgeMidDeg = wedgeMidRad * 180 / Math.PI;
34027
- const wheelWobble = (Math.random() - 0.5) * (anglePerWedge * 180 / Math.PI) * 0.6;
34028
- const currentMod = (rotation % 360 + 360) % 360;
34072
+ const wheelWobble = (Math.random() - 0.5) * (activeAnglePerWedge * 180 / Math.PI) * 0.6;
34073
+ const currentMod = (activeRotation % 360 + 360) % 360;
34029
34074
  const targetMod = ((-wedgeMidDeg + wheelWobble) % 360 + 360) % 360;
34030
34075
  let deltaMod = targetMod - currentMod;
34031
34076
  if (deltaMod < 0) deltaMod += 360;
34032
- const finalRotation = rotation + deltaMod + revolutions * 360;
34077
+ const finalRotation = activeRotation + deltaMod + revolutions * 360;
34033
34078
  const centerOffsetRows = (reelVisibleRows - 1) / 2;
34034
34079
  const centerOffsetPx = centerOffsetRows * reelRowHeight;
34035
34080
  const currentCenterRow = Math.round(
34036
- (reelY + centerOffsetPx) / reelRowHeight
34081
+ (activeReelY + centerOffsetPx) / reelRowHeight
34037
34082
  );
34038
- const currentCenterMod = mod(currentCenterRow, n);
34039
- const targetRowOffset = (winnerIdx - currentCenterMod + n) % n;
34040
- const reelRevRows = Math.round(revolutions * n);
34041
- const finalReelY = reelY + (reelRevRows + targetRowOffset) * reelRowHeight;
34042
- const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, n) : wheelIndexAtPointer(finalRotation, n);
34043
- const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < pool.length ? settledWinnerIdx : winnerIdx;
34083
+ const currentCenterMod = mod(currentCenterRow, activeN);
34084
+ const targetRowOffset = (winnerIdx - currentCenterMod + activeN) % activeN;
34085
+ const reelRevRows = Math.round(revolutions * activeN);
34086
+ const finalReelY = activeReelY + (reelRevRows + targetRowOffset) * reelRowHeight;
34087
+ const settledWinnerIdx = mode === "reel" ? reelIndexAtCenter(finalReelY, centerOffsetPx, reelRowHeight, activeN) : wheelIndexAtPointer(finalRotation, activeN);
34088
+ const finalWinnerIdx = settledWinnerIdx >= 0 && settledWinnerIdx < activePool.length ? settledWinnerIdx : winnerIdx;
34044
34089
  const effectiveDuration = reducedMotion ? 250 : Math.round(spinDuration * intensityCfg.durationMult);
34045
34090
  setSpinning(true);
34046
34091
  setWinner(null);
@@ -34053,7 +34098,7 @@ function RandomPlayerPicker({
34053
34098
  if ((onTick || tickAudioRef.current) && !reducedMotion) {
34054
34099
  const wedges = mode === "reel" ? Math.max(1, reelRevRows + targetRowOffset) : (() => {
34055
34100
  const totalDelta = deltaMod + revolutions * 360;
34056
- const wedgeDeg = anglePerWedge * 180 / Math.PI;
34101
+ const wedgeDeg = activeAnglePerWedge * 180 / Math.PI;
34057
34102
  return Math.max(
34058
34103
  1,
34059
34104
  Math.round(totalDelta / Math.max(wedgeDeg, 1))
@@ -34085,7 +34130,7 @@ function RandomPlayerPicker({
34085
34130
  schedule();
34086
34131
  }
34087
34132
  winnerTimer.current = setTimeout(() => {
34088
- const picked = pool[finalWinnerIdx];
34133
+ const picked = activePool[finalWinnerIdx];
34089
34134
  if (!picked) {
34090
34135
  setSpinning(false);
34091
34136
  return;
@@ -34131,7 +34176,11 @@ function RandomPlayerPicker({
34131
34176
  particleTimer.current = setTimeout(() => setParticles([]), 2400);
34132
34177
  }
34133
34178
  if (removeWinners) {
34179
+ pendingPopIdxRef.current = finalWinnerIdx;
34134
34180
  popTimer.current = setTimeout(() => {
34181
+ popTimer.current = null;
34182
+ pendingPopIdxRef.current = null;
34183
+ if (!removeWinnersRef.current) return;
34135
34184
  setPool((p) => p.filter((_, i) => i !== finalWinnerIdx));
34136
34185
  setWinner(null);
34137
34186
  setRotation(0);
@@ -34142,8 +34191,6 @@ function RandomPlayerPicker({
34142
34191
  }, [
34143
34192
  pool,
34144
34193
  spinning,
34145
- n,
34146
- anglePerWedge,
34147
34194
  rotation,
34148
34195
  reelY,
34149
34196
  mode,
@@ -34179,6 +34226,7 @@ function RandomPlayerPicker({
34179
34226
  []
34180
34227
  );
34181
34228
  (0, import_react71.useEffect)(() => {
34229
+ if (trigger === "external") return;
34182
34230
  const el = containerRef.current;
34183
34231
  if (!el) return;
34184
34232
  const h = (e) => {
@@ -34189,8 +34237,22 @@ function RandomPlayerPicker({
34189
34237
  };
34190
34238
  el.addEventListener("keydown", h);
34191
34239
  return () => el.removeEventListener("keydown", h);
34192
- }, [spin]);
34240
+ }, [spin, trigger]);
34193
34241
  const reset = (0, import_react71.useCallback)(() => {
34242
+ if (winnerTimer.current) {
34243
+ clearTimeout(winnerTimer.current);
34244
+ winnerTimer.current = null;
34245
+ }
34246
+ if (tickInterval.current) {
34247
+ clearTimeout(tickInterval.current);
34248
+ tickInterval.current = null;
34249
+ }
34250
+ if (popTimer.current) {
34251
+ clearTimeout(popTimer.current);
34252
+ popTimer.current = null;
34253
+ }
34254
+ pendingPopIdxRef.current = null;
34255
+ setSpinning(false);
34194
34256
  setPool(participants);
34195
34257
  setHistory([]);
34196
34258
  setWinner(null);
@@ -38643,27 +38705,28 @@ function GameTimer({
38643
38705
  const now = performance.now();
38644
38706
  const delta = now - lastTickRef.current;
38645
38707
  lastTickRef.current = now;
38646
- setElapsedMs((prev) => {
38647
- const next = prev + delta;
38648
- if (mode === "countdown" && next >= durationMs) {
38649
- setRunning(false);
38650
- setCompleted(true);
38651
- onComplete?.();
38652
- return durationMs;
38708
+ const next = elapsedRef.current + delta;
38709
+ if (mode === "countdown" && next >= durationMs) {
38710
+ elapsedRef.current = durationMs;
38711
+ setElapsedMs(durationMs);
38712
+ setRunning(false);
38713
+ setCompleted(true);
38714
+ onComplete?.();
38715
+ return;
38716
+ }
38717
+ if (mode === "countdown") {
38718
+ const secLeft = Math.ceil((durationMs - next) / 1e3);
38719
+ if (secLeft <= warningAt && !warningFiredRef.current) {
38720
+ warningFiredRef.current = true;
38721
+ onWarning?.();
38653
38722
  }
38654
- if (mode === "countdown") {
38655
- const secLeft = Math.ceil((durationMs - next) / 1e3);
38656
- if (secLeft <= warningAt && !warningFiredRef.current) {
38657
- warningFiredRef.current = true;
38658
- onWarning?.();
38659
- }
38660
- if (secLeft <= dangerAt && !dangerFiredRef.current) {
38661
- dangerFiredRef.current = true;
38662
- onDanger?.();
38663
- }
38723
+ if (secLeft <= dangerAt && !dangerFiredRef.current) {
38724
+ dangerFiredRef.current = true;
38725
+ onDanger?.();
38664
38726
  }
38665
- return next;
38666
- });
38727
+ }
38728
+ elapsedRef.current = next;
38729
+ setElapsedMs(next);
38667
38730
  rafRef.current = requestAnimationFrame(tick);
38668
38731
  }, [mode, durationMs, warningAt, dangerAt, onComplete, onWarning, onDanger]);
38669
38732
  (0, import_react74.useEffect)(() => {
@@ -38684,6 +38747,7 @@ function GameTimer({
38684
38747
  }, []);
38685
38748
  const reset = (0, import_react74.useCallback)(() => {
38686
38749
  setRunning(false);
38750
+ elapsedRef.current = 0;
38687
38751
  setElapsedMs(0);
38688
38752
  setCompleted(false);
38689
38753
  setLaps([]);
@@ -40828,6 +40892,8 @@ function RoundRobinScheduler({
40828
40892
  theme: theme5 = "slate",
40829
40893
  darkMode,
40830
40894
  minTeams = 3,
40895
+ maxTeams,
40896
+ nameMaxLength,
40831
40897
  defaultTeams = [],
40832
40898
  resumeRounds,
40833
40899
  onTeamsChange,
@@ -40850,39 +40916,41 @@ function RoundRobinScheduler({
40850
40916
  (0, import_react77.useEffect)(() => {
40851
40917
  onTeamsChange?.(teams);
40852
40918
  }, [teams, onTeamsChange]);
40853
- const addTeams = (0, import_react77.useCallback)((raw) => {
40854
- const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40855
- if (names.length === 0) return;
40856
- setTeams((prev) => {
40857
- const existing = new Set(prev.map((t) => t.toLowerCase()));
40858
- const next = [...prev];
40859
- for (const name of names) {
40860
- if (!existing.has(name.toLowerCase())) {
40861
- existing.add(name.toLowerCase());
40862
- next.push(name);
40863
- }
40864
- }
40865
- return next;
40866
- });
40867
- setInput("");
40868
- setRounds([]);
40869
- }, []);
40870
- const importTeams = (0, import_react77.useCallback)((incoming) => {
40871
- if (incoming.length === 0) return;
40872
- setTeams((prev) => {
40919
+ const mergeNames = (0, import_react77.useCallback)(
40920
+ (prev, incoming) => {
40873
40921
  const existing = new Set(prev.map((t) => t.toLowerCase()));
40874
40922
  const next = [...prev];
40875
- for (const name of incoming) {
40923
+ for (const rawName of incoming) {
40924
+ if (maxTeams != null && next.length >= maxTeams) break;
40925
+ const name = nameMaxLength != null ? rawName.slice(0, nameMaxLength) : rawName;
40876
40926
  const key = name.toLowerCase();
40877
- if (!existing.has(key)) {
40927
+ if (name.length > 0 && !existing.has(key)) {
40878
40928
  existing.add(key);
40879
40929
  next.push(name);
40880
40930
  }
40881
40931
  }
40882
40932
  return next;
40883
- });
40884
- setRounds([]);
40885
- }, []);
40933
+ },
40934
+ [maxTeams, nameMaxLength]
40935
+ );
40936
+ const addTeams = (0, import_react77.useCallback)(
40937
+ (raw) => {
40938
+ const names = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
40939
+ if (names.length === 0) return;
40940
+ setTeams((prev) => mergeNames(prev, names));
40941
+ setInput("");
40942
+ setRounds([]);
40943
+ },
40944
+ [mergeNames]
40945
+ );
40946
+ const importTeams = (0, import_react77.useCallback)(
40947
+ (incoming) => {
40948
+ if (incoming.length === 0) return;
40949
+ setTeams((prev) => mergeNames(prev, incoming));
40950
+ setRounds([]);
40951
+ },
40952
+ [mergeNames]
40953
+ );
40886
40954
  const handleKeyDown = (0, import_react77.useCallback)(
40887
40955
  (e) => {
40888
40956
  if (e.key === "Enter") {
@@ -41371,6 +41439,8 @@ function Scoreboard({
41371
41439
  onTeamsChange,
41372
41440
  onRoundAdvance,
41373
41441
  onNewGame,
41442
+ onReset,
41443
+ nameMaxLength,
41374
41444
  labels,
41375
41445
  ariaLabel = "Scoreboard",
41376
41446
  className
@@ -41445,7 +41515,8 @@ function Scoreboard({
41445
41515
  setRound(1);
41446
41516
  setHistory([]);
41447
41517
  setHistoryOpen(false);
41448
- }, []);
41518
+ onReset?.();
41519
+ }, [onReset]);
41449
41520
  const newGame = (0, import_react78.useCallback)(() => {
41450
41521
  setTeams(createDefaultTeams(seedCount, L.teamNamePrefix));
41451
41522
  setRound(1);
@@ -41667,6 +41738,7 @@ function Scoreboard({
41667
41738
  {
41668
41739
  type: "text",
41669
41740
  value: team.name,
41741
+ maxLength: nameMaxLength,
41670
41742
  onChange: (e) => updateName(index, e.target.value),
41671
41743
  style: nameInputStyle,
41672
41744
  "aria-label": `Team ${index + 1} name`