@ethlete/bracket 1.0.0-next.1 → 1.0.0-next.2

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.
@@ -1,5 +1,3 @@
1
- // codes 3400-3499 (3400 was LAYOUT_MODE_UNSUPPORTED, retired when layouts became explicit values -
2
- // an unsupported layout×mode combination no longer exists as a factory)
3
1
  const BRACKET_ERROR_CODES = {
4
2
  /** The source has no rounds/matches to render. */
5
3
  SOURCE_EMPTY: 3401,
@@ -27,6 +25,8 @@ const BRACKET_ERROR_CODES = {
27
25
  MISSING_MATCH_NORMALIZER: 3412,
28
26
  /** No registered {@link BracketLayout} matches the source's tournament `mode`. */
29
27
  LAYOUT_NOT_REGISTERED: 3413,
28
+ /** A cell the bracket draws has no card - neither an input, the layout, nor the config names one. */
29
+ CARD_NOT_REGISTERED: 3414,
30
30
  };
31
31
 
32
32
  class BracketRuntimeError extends Error {
@@ -101,6 +101,11 @@ const SWISS_BRACKET_ROUND_TYPE = {
101
101
  const GROUP_BRACKET_ROUND_TYPE = {
102
102
  GROUP: 'group',
103
103
  };
104
+ const TERMINAL_ROUND_SORT_PRIORITY = {
105
+ [COMMON_BRACKET_ROUND_TYPE.FINAL]: 1,
106
+ [DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL]: 2,
107
+ [COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE]: 3,
108
+ };
104
109
  const createRoundsMapBase = (source, options) => {
105
110
  const map = new BracketMap();
106
111
  const shouldSplitRoundsInTwo = options.layout === BRACKET_DATA_LAYOUT.MIRRORED;
@@ -112,12 +117,7 @@ const createRoundsMapBase = (source, options) => {
112
117
  // of one that has no side. Tracked per bracket half, because the two converge separately.
113
118
  const foldClosed = { upper: false, lower: false };
114
119
  const splitRoundsRest = [];
115
- const terminalRoundSortPriority = {
116
- [COMMON_BRACKET_ROUND_TYPE.FINAL]: 1,
117
- [DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL]: 2,
118
- [COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE]: 3,
119
- };
120
- const orderedRounds = [...source.rounds].sort((a, b) => (terminalRoundSortPriority[a.type] ?? 0) - (terminalRoundSortPriority[b.type] ?? 0));
120
+ const orderedRounds = [...source.rounds].sort((a, b) => (TERMINAL_ROUND_SORT_PRIORITY[a.type] ?? 0) - (TERMINAL_ROUND_SORT_PRIORITY[b.type] ?? 0));
121
121
  for (const [roundIndex, round] of orderedRounds.entries()) {
122
122
  const isUpperBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET;
123
123
  const isLowerBracket = round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET;
@@ -213,9 +213,14 @@ const createRoundsMapBase = (source, options) => {
213
213
  return map;
214
214
  };
215
215
 
216
- const createNewMatchParticipantBase = (source, participantId, match, rounds, matchRoundId, participants) => {
216
+ const createNewMatchParticipantBase = (source, participantId, match, rounds, matchRoundId, participants, sourceMatchesById) => {
217
217
  if (!participantId)
218
218
  return null;
219
+ const findSourceMatch = (matchId) => matchId === undefined
220
+ ? undefined
221
+ : sourceMatchesById
222
+ ? sourceMatchesById.get(matchId)
223
+ : source.matches.find((m) => m.id === matchId);
219
224
  const participantBase = participants.getOrThrow(participantId);
220
225
  const roundBase = rounds.getOrThrow(matchRoundId);
221
226
  const matchIndex = participantBase.matchIds.indexOf(match.id);
@@ -230,7 +235,7 @@ const createNewMatchParticipantBase = (source, participantId, match, rounds, mat
230
235
  let tiesTilNow = isTie ? 1 : 0;
231
236
  for (let i = 0; i < matchIndex; i++) {
232
237
  const previousMatchId = participantBase.matchIds[i];
233
- const previousMatch = source.matches.find((m) => m.id === previousMatchId);
238
+ const previousMatch = findSourceMatch(previousMatchId);
234
239
  const myPreviousMatchSide = previousMatch?.home === participantId ? 'home' : previousMatch?.away === participantId ? 'away' : null;
235
240
  if (!previousMatch || !myPreviousMatchSide)
236
241
  continue;
@@ -250,9 +255,7 @@ const createNewMatchParticipantBase = (source, participantId, match, rounds, mat
250
255
  const hasElimination = source.mode === TOURNAMENT_MODE.SINGLE_ELIMINATION ||
251
256
  source.mode === TOURNAMENT_MODE.DOUBLE_ELIMINATION ||
252
257
  source.mode === TOURNAMENT_MODE.SWISS_WITH_ELIMINATION;
253
- // Means the current match is loss and it's the last match of the participant
254
258
  let isEliminated = false;
255
- // Always true for single elimination, never for e.g. groups, depends on the round for double elimination, depends on the loss count for swiss with elimination
256
259
  let isEliminationMatch = false;
257
260
  if (hasElimination) {
258
261
  switch (source.mode) {
@@ -278,7 +281,7 @@ const createNewMatchParticipantBase = (source, participantId, match, rounds, mat
278
281
  break;
279
282
  const previousMatchIndex = currentMatchIndex - 1;
280
283
  const previousMatchId = participantBase.matchIds[previousMatchIndex];
281
- const previousMatch = source.matches.find((m) => m.id === previousMatchId);
284
+ const previousMatch = findSourceMatch(previousMatchId);
282
285
  if (!previousMatch)
283
286
  throw new BracketRuntimeError(BRACKET_ERROR_CODES.MATCH_RELATION_INVALID, `Previous match with id ${previousMatchId} not found`);
284
287
  const previousRound = rounds.getOrThrow(previousMatch.roundId);
@@ -311,6 +314,7 @@ const createNewMatchParticipantBase = (source, participantId, match, rounds, mat
311
314
 
312
315
  const createMatchesMapBase = (source, rounds, participants) => {
313
316
  const map = new BracketMap();
317
+ const sourceMatchesById = new Map(source.matches.map((match) => [match.id, match]));
314
318
  for (const match of source.matches) {
315
319
  const genericRound = rounds.get(match.roundId);
316
320
  const splitRound = rounds.get(`${match.roundId}--half-1`);
@@ -336,8 +340,8 @@ const createMatchesMapBase = (source, rounds, participants) => {
336
340
  : -1;
337
341
  if (indexInRound === -1)
338
342
  throw new BracketRuntimeError(BRACKET_ERROR_CODES.MATCH_RELATION_INVALID, `Match with id ${match.id} not found in round with id ${roundToUse.id}`);
339
- const home = createNewMatchParticipantBase(source, match.home, match, rounds, roundToUse.id, participants);
340
- const away = createNewMatchParticipantBase(source, match.away, match, rounds, roundToUse.id, participants);
343
+ const home = createNewMatchParticipantBase(source, match.home, match, rounds, roundToUse.id, participants, sourceMatchesById);
344
+ const away = createNewMatchParticipantBase(source, match.away, match, rounds, roundToUse.id, participants, sourceMatchesById);
341
345
  const winner = match.winner === 'home' ? home : match.winner === 'away' ? away : null;
342
346
  const matchBase = {
343
347
  home,
@@ -365,24 +369,27 @@ const createParticipantsMapBase = (source) => {
365
369
  .map((m) => [m.home, m.away])
366
370
  .flat()
367
371
  .filter((p) => !!p);
372
+ const matchIdsByParticipant = new Map();
373
+ for (const match of source.matches) {
374
+ for (const participantId of new Set([match.home, match.away])) {
375
+ if (!participantId)
376
+ continue;
377
+ const matchIds = matchIdsByParticipant.get(participantId) ?? [];
378
+ matchIds.push(match.id);
379
+ matchIdsByParticipant.set(participantId, matchIds);
380
+ }
381
+ }
368
382
  for (const [index, participantId] of participantIds.entries()) {
369
383
  const participantBase = {
370
384
  id: participantId,
371
385
  shortId: `p${index}`,
372
- matchIds: source.matches
373
- .filter((m) => m.home === participantId || m.away === participantId)
374
- .map((m) => m.id),
386
+ matchIds: matchIdsByParticipant.get(participantId) ?? [],
375
387
  };
376
388
  map.set(participantId, participantBase);
377
389
  }
378
390
  return map;
379
391
  };
380
392
 
381
- const TERMINAL_ROUND_SORT_PRIORITY = {
382
- final: 1,
383
- 'reverse-final': 2,
384
- 'third-place': 3,
385
- };
386
393
  const sortSourceMatchesByRoundOrder = (source) => {
387
394
  const orderedRoundIds = [...source.rounds]
388
395
  .sort((a, b) => (TERMINAL_ROUND_SORT_PRIORITY[a.type] ?? 0) - (TERMINAL_ROUND_SORT_PRIORITY[b.type] ?? 0))
@@ -391,7 +398,26 @@ const sortSourceMatchesByRoundOrder = (source) => {
391
398
  const sortedMatches = [...source.matches].sort((a, b) => (roundIndexMap.get(a.roundId) ?? 0) - (roundIndexMap.get(b.roundId) ?? 0));
392
399
  return { ...source, matches: sortedMatches };
393
400
  };
401
+ const assertUniqueIds = (options) => {
402
+ const seen = new Set();
403
+ for (const id of options.ids) {
404
+ if (seen.has(id)) {
405
+ throw new BracketRuntimeError(options.code, `${options.kind} with id ${id} exists more than once in the source.`);
406
+ }
407
+ seen.add(id);
408
+ }
409
+ };
394
410
  const createBracketBase = (source, options) => {
411
+ assertUniqueIds({
412
+ ids: source.rounds.map((round) => round.id),
413
+ code: BRACKET_ERROR_CODES.DUPLICATE_ROUND,
414
+ kind: 'Round',
415
+ });
416
+ assertUniqueIds({
417
+ ids: source.matches.map((match) => match.id),
418
+ code: BRACKET_ERROR_CODES.DUPLICATE_MATCH,
419
+ kind: 'Match',
420
+ });
395
421
  const normalizedSource = sortSourceMatchesByRoundOrder(source);
396
422
  const participants = createParticipantsMapBase(normalizedSource);
397
423
  const rounds = createRoundsMapBase(normalizedSource, options);
@@ -421,34 +447,29 @@ const path = (d, options) => {
421
447
 
422
448
  const curvePath = (from, to, direction, options) => {
423
449
  const inverted = options.inverted ?? false;
424
- // Inline/block coordinates depending on direction and inversion
425
450
  const fromInline = inverted ? from.inline.start : from.inline.end;
426
451
  const toInline = inverted ? to.inline.end : to.inline.start;
427
452
  const fromBlock = from.block.center;
428
453
  const toBlock = to.block.center;
429
- // Both bends have to fit inside the block distance between the two cards, or the line turns past its
430
- // target and comes back. Scaling them down instead - to nothing, for two cards on the same row - is
431
- // also what keeps the six commands below identical for every connector: a CSS `d` transition
432
- // interpolates only between two paths of the same shape.
454
+ // Both bends have to fit inside the block and the inline distance between the two cards, or the line
455
+ // turns past its target and comes back. Scaling them down instead - to nothing, for two cards on the
456
+ // same row - is also what keeps the six commands below identical for every connector: a CSS `d`
457
+ // transition interpolates only between two paths of the same shape.
433
458
  const requestedCurve = options.lineStartingCurveAmount + options.lineEndingCurveAmount;
434
- const blockDistance = Math.abs(toBlock - fromBlock);
435
- const curveScale = requestedCurve > blockDistance ? blockDistance / requestedCurve : 1;
459
+ const totalInline = Math.abs(toInline - fromInline);
460
+ const room = Math.min(Math.abs(toBlock - fromBlock), totalInline);
461
+ const curveScale = requestedCurve > room ? room / requestedCurve : 1;
436
462
  const startCurve = options.lineStartingCurveAmount * curveScale;
437
463
  const endCurve = options.lineEndingCurveAmount * curveScale;
438
- const totalInline = Math.abs(toInline - fromInline);
439
464
  const straightLength = (totalInline - startCurve - endCurve) / 2;
440
- // Calculate key points for the path
441
465
  const straightEnd = inverted ? fromInline - straightLength : fromInline + straightLength;
442
466
  const straightStart = inverted ? toInline + straightLength : toInline - straightLength;
443
- // First curve (from start)
444
467
  const firstCurveEndX = inverted ? straightEnd - startCurve : straightEnd + startCurve;
445
468
  const firstCurveEndY = direction === 'down' ? fromBlock + startCurve : fromBlock - startCurve;
446
- // Second curve (to end)
447
469
  const secondCurveStartY = direction === 'down' ? toBlock - endCurve : toBlock + endCurve;
448
470
  const secondCurveEndX = straightStart;
449
471
  const secondCurveEndY = toBlock;
450
472
  const secondCurveBezierX = inverted ? straightStart + endCurve : straightStart - endCurve;
451
- // SVG path string
452
473
  const d = [
453
474
  `M ${fromInline} ${fromBlock}`,
454
475
  `H ${straightEnd}`,
@@ -460,20 +481,14 @@ const curvePath = (from, to, direction, options) => {
460
481
  return path(d, options.path);
461
482
  };
462
483
 
463
- const createBracketElementPart = (config) => {
464
- const { elementPartHeight } = config;
465
- const newElementPart = {
466
- dimensions: {
467
- width: 0,
468
- height: elementPartHeight,
469
- top: 0,
470
- left: 0,
471
- },
472
- };
473
- return {
474
- elementPart: newElementPart,
475
- };
476
- };
484
+ const createBracketElementPart = (config) => ({
485
+ dimensions: {
486
+ width: 0,
487
+ height: config.elementPartHeight,
488
+ top: 0,
489
+ left: 0,
490
+ },
491
+ });
477
492
 
478
493
  const createBracketElement = (config) => {
479
494
  const { type, area, elementHeight, partHeights } = config;
@@ -532,18 +547,10 @@ const createBracketElement = (config) => {
532
547
  throw new BracketRuntimeError(BRACKET_ERROR_CODES.GRID_INVALID, `Unknown element type: ${type}`);
533
548
  }
534
549
  })();
535
- const pushPart = (...newParts) => {
536
- parts.push(...newParts);
537
- };
538
550
  for (const partHeight of partHeights) {
539
- const { elementPart } = createBracketElementPart({
540
- elementPartHeight: partHeight,
541
- });
542
- pushPart(elementPart);
551
+ parts.push(createBracketElementPart({ elementPartHeight: partHeight }));
543
552
  }
544
- return {
545
- element: newElement,
546
- };
553
+ return newElement;
547
554
  };
548
555
 
549
556
  const finalizeBracketGrid = (grid) => {
@@ -625,8 +632,7 @@ const finalizeBracketGrid = (grid) => {
625
632
 
626
633
  const createBracketGrid = (config) => {
627
634
  const masterColumns = [];
628
- const spannedWidthCache = new Map();
629
- const spanStartLeftCache = new Map();
635
+ const contentWidths = new WeakMap();
630
636
  const newGrid = {
631
637
  dimensions: { width: 0, height: 0, top: 0, left: 0 },
632
638
  masterColumns,
@@ -642,7 +648,9 @@ const createBracketGrid = (config) => {
642
648
  const { padding } = masterColumn;
643
649
  masterColumn.dimensions.left = currentMasterColumnLeft;
644
650
  masterColumn.dimensions.top = 0;
645
- masterColumn.dimensions.width += padding.left + padding.right;
651
+ const contentWidth = contentWidths.get(masterColumn) ?? masterColumn.dimensions.width;
652
+ contentWidths.set(masterColumn, contentWidth);
653
+ masterColumn.dimensions.width = contentWidth + padding.left + padding.right;
646
654
  const sections = masterColumn.sections;
647
655
  let runningTop = 0;
648
656
  const firstElement = sections[0]?.subColumns[0]?.elements[0];
@@ -660,10 +668,10 @@ const createBracketGrid = (config) => {
660
668
  section.dimensions.left = masterColumn.dimensions.left;
661
669
  section.dimensions.top = runningTop;
662
670
  runningTop += sectionPaddingTop;
663
- const contentWidth = masterColumn.dimensions.width - sectionPadding.left - sectionPadding.right;
671
+ const sectionContentWidth = masterColumn.dimensions.width - sectionPadding.left - sectionPadding.right;
664
672
  const subColumns = section.subColumns;
665
673
  const totalSubColumns = subColumns.length;
666
- const subColumnWidth = contentWidth / totalSubColumns;
674
+ const subColumnWidth = sectionContentWidth / totalSubColumns;
667
675
  let currentSubColumnLeft = masterColumn.dimensions.left + sectionPadding.left;
668
676
  let maxSectionHeight = 0;
669
677
  for (const subColumn of subColumns) {
@@ -726,8 +734,7 @@ const createBracketGrid = (config) => {
726
734
  const isStartPosition = mcIdx === span.masterColumnStart && secIdx === span.sectionStart && scIdx === span.subColumnStart;
727
735
  const spanKey = `${span.masterColumnStart}-${span.masterColumnEnd}-${span.sectionStart}-${span.sectionEnd}-${span.subColumnStart}-${span.subColumnEnd}`;
728
736
  if (isStartPosition && !spanDimensions.has(spanKey)) {
729
- const totalSpannedWidth = calculateSpannedWidth(span, masterCols);
730
- const spanStartLeft = calculateSpanStartLeft(span, masterCols);
737
+ const { left: spanStartLeft, width: totalSpannedWidth } = calculateSpanExtent(span, masterCols);
731
738
  const width = config.spanElementWidth;
732
739
  // Winner bracket rounds span two lower bracket columns. Left-align them within that
733
740
  // span (factor 0) instead of centering (factor 0.5) so each winner round n sits above
@@ -742,75 +749,18 @@ const createBracketGrid = (config) => {
742
749
  if (storedDimensions) {
743
750
  element.dimensions.width = storedDimensions.width;
744
751
  element.dimensions.left = storedDimensions.left;
745
- element.isHidden = !isStartPosition;
746
752
  }
747
753
  }
748
754
  }
749
755
  }
750
756
  }
751
757
  };
752
- const calculateSpannedWidth = (span, masterColumns) => {
753
- const key = `${span.masterColumnStart}-${span.masterColumnEnd}-${span.sectionStart}-${span.sectionEnd}-${span.subColumnStart}-${span.subColumnEnd}`;
754
- const cachedWidth = spannedWidthCache.get(key);
755
- if (cachedWidth !== undefined)
756
- return cachedWidth;
757
- if (span.masterColumnStart === span.masterColumnEnd) {
758
- const masterColumn = masterColumns[span.masterColumnStart];
759
- if (masterColumn) {
760
- const section = masterColumn.sections[span.sectionStart];
761
- if (section) {
762
- const subColumnWidth = section.dimensions.width / section.subColumns.length;
763
- const totalWidth = subColumnWidth * (span.subColumnEnd - span.subColumnStart + 1);
764
- spannedWidthCache.set(key, totalWidth);
765
- return totalWidth;
766
- }
767
- }
768
- spannedWidthCache.set(key, 0);
769
- return 0;
770
- }
771
- let totalWidth = 0;
772
- for (let mcIdx = span.masterColumnStart; mcIdx <= span.masterColumnEnd; mcIdx++) {
773
- const masterColumn = masterColumns[mcIdx];
774
- if (!masterColumn)
775
- continue;
776
- if (mcIdx === span.masterColumnStart) {
777
- const section = masterColumn.sections[span.sectionStart];
778
- if (section) {
779
- const subColumnWidth = section.dimensions.width / section.subColumns.length;
780
- totalWidth += subColumnWidth * (section.subColumns.length - span.subColumnStart);
781
- }
782
- }
783
- else if (mcIdx === span.masterColumnEnd) {
784
- const section = masterColumn.sections[span.sectionEnd];
785
- if (section) {
786
- const subColumnWidth = section.dimensions.width / section.subColumns.length;
787
- totalWidth += subColumnWidth * (span.subColumnEnd + 1);
788
- }
789
- }
790
- else {
791
- totalWidth += masterColumn.dimensions.width;
792
- }
793
- }
794
- spannedWidthCache.set(key, totalWidth);
795
- return totalWidth;
796
- };
797
- const calculateSpanStartLeft = (span, masterColumns) => {
798
- const key = `${span.masterColumnStart}-${span.sectionStart}-${span.subColumnStart}`;
799
- const cachedLeft = spanStartLeftCache.get(key);
800
- if (cachedLeft !== undefined)
801
- return cachedLeft;
802
- const startMasterColumn = masterColumns[span.masterColumnStart];
803
- if (!startMasterColumn) {
804
- spanStartLeftCache.set(key, 0);
805
- return 0;
806
- }
807
- let startLeft = startMasterColumn.dimensions.left;
808
- const section = startMasterColumn.sections[span.sectionStart];
809
- if (section) {
810
- startLeft += (section.dimensions.width / section.subColumns.length) * span.subColumnStart;
811
- }
812
- spanStartLeftCache.set(key, startLeft);
813
- return startLeft;
758
+ const calculateSpanExtent = (span, masterColumns) => {
759
+ const start = masterColumns[span.masterColumnStart]?.sections[span.sectionStart]?.subColumns[span.subColumnStart];
760
+ const end = masterColumns[span.masterColumnEnd]?.sections[span.sectionEnd]?.subColumns[span.subColumnEnd];
761
+ if (!start || !end)
762
+ return { left: start?.dimensions.left ?? 0, width: 0 };
763
+ return { left: start.dimensions.left, width: end.dimensions.left + end.dimensions.width - start.dimensions.left };
814
764
  };
815
765
  const setupElementSpans = () => {
816
766
  const masterCols = newGrid.masterColumns;
@@ -957,11 +907,16 @@ const createBracketContinueMasterColumn = (config) => {
957
907
  throw new BracketRuntimeError(BRACKET_ERROR_CODES.GRID_INVALID, 'No last master column found in existing master columns');
958
908
  }
959
909
  // Dimensions are not calculated yet at this point, but all part heights are static, so the
960
- // total column height is the sum of the tallest sub column of every section.
961
- const totalHeight = lastMasterColumn.sections.reduce((height, section) => {
910
+ // total column height is the sum of the tallest sub column of every section plus its padding - the
911
+ // same sum `calculateDimensions` makes, which skips the top padding of a leading header section.
912
+ const firstSectionIsHeader = lastMasterColumn.sections[0]?.subColumns[0]?.elements[0]?.type === 'header';
913
+ let totalHeight = 0;
914
+ for (const [sectionIndex, section] of lastMasterColumn.sections.entries()) {
915
+ const padding = section.padding ?? lastMasterColumn.padding;
916
+ const paddingTop = sectionIndex === 0 && firstSectionIsHeader ? 0 : padding.top;
962
917
  const sectionHeight = Math.max(0, ...section.subColumns.map((subColumn) => subColumn.elements.reduce((subColumnHeight, element) => subColumnHeight + element.parts.reduce((sum, part) => sum + part.dimensions.height, 0), 0)));
963
- return height + sectionHeight;
964
- }, 0);
918
+ totalHeight += paddingTop + sectionHeight + padding.bottom;
919
+ }
965
920
  const { masterColumn, pushSection } = createBracketMasterColumn({
966
921
  columnWidth,
967
922
  padding: {
@@ -981,7 +936,7 @@ const createBracketContinueMasterColumn = (config) => {
981
936
  },
982
937
  });
983
938
  if (headerOffset > 0) {
984
- const { element: headerGapElement } = createBracketElement({
939
+ const headerGapElement = createBracketElement({
985
940
  area: '.',
986
941
  type: 'colGap',
987
942
  elementHeight: headerOffset,
@@ -990,7 +945,7 @@ const createBracketContinueMasterColumn = (config) => {
990
945
  pushElement(headerGapElement);
991
946
  }
992
947
  const containerHeight = Math.max(elementHeight, totalHeight - headerOffset);
993
- const { element: continueElement } = createBracketElement({
948
+ const continueElement = createBracketElement({
994
949
  area: 'continue',
995
950
  type: 'continue',
996
951
  elementHeight,
@@ -1087,7 +1042,6 @@ const drawMan = (dimensions) => {
1087
1042
  .filter((id) => !!id)
1088
1043
  .join(' ');
1089
1044
  const currentPos = makePos(el.dimensions);
1090
- // No lines for the third place match
1091
1045
  if (el.round.type === COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE)
1092
1046
  continue;
1093
1047
  if (continuePos && isBracketContinueMatch(el.match)) {
@@ -1100,8 +1054,7 @@ const drawMan = (dimensions) => {
1100
1054
  // Crossing the middle of a fold. Every other line here is drawn by the match it flows *into*, but a
1101
1055
  // round in the middle of a mirrored bracket is fed from both sides and its relation names only one
1102
1056
  // of them - so the round on the way back draws its own line. Deliberately outside the switch below:
1103
- // which relation a match has says nothing about which side of a fold it sits on, and keying this to
1104
- // `two-to-one` is why a folded lower bracket used to lose the line into its centre round.
1057
+ // which relation a match has says nothing about which side of a fold it sits on.
1105
1058
  if (el.round.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.RIGHT && 'nextMatch' in el.match.relation) {
1106
1059
  const { nextMatch, nextRound } = el.match.relation;
1107
1060
  if (nextRound.mirrorRoundType === null) {
@@ -1118,6 +1071,7 @@ const drawMan = (dimensions) => {
1118
1071
  }
1119
1072
  }
1120
1073
  switch (el.match.relation.type) {
1074
+ case 'none':
1121
1075
  case 'nothing-to-one': {
1122
1076
  continue;
1123
1077
  }
@@ -1125,8 +1079,8 @@ const drawMan = (dimensions) => {
1125
1079
  case 'one-to-one': {
1126
1080
  const prev = dimensions.bracketGrid.matchElementMap.getOrThrow(el.match.relation.previousMatch.id);
1127
1081
  const prevPos = makePos(prev.dimensions);
1128
- // The winner of the match it comes from, like every other connector - this used to carry both of
1129
- // the *current* match's participants, which lit the line for whoever arrived from somewhere else.
1082
+ // The winner of the match it comes from, like every other connector, or the journey highlight
1083
+ // lights the line for whoever arrived from somewhere else.
1130
1084
  pushEdge(straightPath(prevPos, currentPos, {
1131
1085
  path: {
1132
1086
  ...dimensions.path,
@@ -1160,7 +1114,6 @@ const drawMan = (dimensions) => {
1160
1114
  }));
1161
1115
  }
1162
1116
  else {
1163
- // draw two lines that merge into one in the middle
1164
1117
  pushEdge(mergePath(prevUpperPos, currentPos, 'down', {
1165
1118
  ...curveOptions,
1166
1119
  path: {
@@ -1265,7 +1218,6 @@ const factorialCache = /* @__PURE__ */ new Map();
1265
1218
  const getAvailableSwissGroupsForRound = (roundNumber, totalMatchesInRound) => {
1266
1219
  const advanceWins = SWISS_ADVANCE_WINS;
1267
1220
  const eliminateLosses = SWISS_ELIMINATE_LOSSES;
1268
- // Cache factorial calculations
1269
1221
  const getFactorial = (n) => {
1270
1222
  if (n <= 1)
1271
1223
  return 1;
@@ -1276,11 +1228,9 @@ const getAvailableSwissGroupsForRound = (roundNumber, totalMatchesInRound) => {
1276
1228
  factorialCache.set(n, result);
1277
1229
  return result;
1278
1230
  };
1279
- // Pre-calculate roundFactorial
1280
1231
  const roundFactorial = getFactorial(roundNumber);
1281
1232
  let totalCombinations = 0;
1282
1233
  const validGroups = [];
1283
- // Single loop to gather valid groups and total combinations
1284
1234
  for (let wins = roundNumber; wins >= 0; wins--) {
1285
1235
  const losses = roundNumber - wins;
1286
1236
  const remainingGames = advanceWins + eliminateLosses - (wins + losses) - 1;
@@ -1292,7 +1242,6 @@ const getAvailableSwissGroupsForRound = (roundNumber, totalMatchesInRound) => {
1292
1242
  totalCombinations += combinations;
1293
1243
  validGroups.push({ wins, losses, combinations });
1294
1244
  }
1295
- // Create final groups with calculated proportions
1296
1245
  return validGroups.map(({ wins, losses, combinations }) => ({
1297
1246
  id: `${wins}-${losses}`,
1298
1247
  name: `${wins}-${losses}`,
@@ -1388,9 +1337,8 @@ const collectSwissRoundGeometries = (bracketGrid) => {
1388
1337
  if (!group || !round)
1389
1338
  continue;
1390
1339
  const [wins, losses] = group.id.split('-').map(Number);
1391
- if (wins === undefined || losses === undefined || isNaN(wins) || isNaN(losses)) {
1392
- throw new BracketRuntimeError(BRACKET_ERROR_CODES.SWISS_GROUPING_FAILED, `Unable to parse wins and losses from Swiss group id: ${group.id}`);
1393
- }
1340
+ if (wins === undefined || losses === undefined || isNaN(wins) || isNaN(losses))
1341
+ continue;
1394
1342
  const matchesTop = Math.min(...matchElements.map((element) => element.dimensions.top));
1395
1343
  const matchesBottom = Math.max(...matchElements.map((element) => element.dimensions.top + element.dimensions.height));
1396
1344
  const winnerShortIds = [];
@@ -1536,42 +1484,20 @@ const drawSwissMan = (dimensions) => {
1536
1484
  };
1537
1485
 
1538
1486
  const calculateUpperLowerRatio = (upperRoundsCount, lowerRoundsCount) => {
1539
- // In double elimination, after the first aligned column,
1540
- // we need to distribute the remaining upper rounds across the remaining lower rounds
1541
- const remainingUpperRounds = upperRoundsCount - 1; // Subtract the first round
1542
- const remainingLowerRounds = lowerRoundsCount - 1; // Subtract the first round
1543
- if (remainingUpperRounds === 0) {
1544
- return 1; // Edge case: only one upper round
1545
- }
1487
+ const remainingUpperRounds = upperRoundsCount - 1;
1488
+ const remainingLowerRounds = lowerRoundsCount - 1;
1489
+ if (remainingUpperRounds === 0)
1490
+ return 1;
1546
1491
  return remainingLowerRounds / remainingUpperRounds;
1547
1492
  };
1548
- const calculateColumnSplitFactor = (upperToLowerRatio) => {
1549
- // The split factor determines how many sub-columns each match column will have
1550
- // For example, if the ratio is 1.5, we split each match column
1551
- // into 2 sub-columns so that matches can be centered correctly
1552
- if (upperToLowerRatio === 1.5) {
1553
- return 2; // Split into 2 sub-columns for 1.5 ratio
1554
- }
1555
- if (upperToLowerRatio === 2) {
1556
- return 1; // Split into 1 sub-columns for 2 ratio
1557
- }
1558
- return 1;
1559
- };
1493
+ const calculateColumnSplitFactor = (upperToLowerRatio) => (upperToLowerRatio === 1.5 ? 2 : 1);
1560
1494
  const calculateLowerRoundIndex = (subColumnIndex, splitFactor) => Math.floor(subColumnIndex / splitFactor);
1561
1495
  // eslint-disable-next-line max-params -- index math over positional grid coordinates
1562
1496
  const calculateUpperRoundIndex = (subColumnIndex, upperToLowerRatio, splitFactor) => {
1563
- // Calculate which complete column we're in
1564
1497
  const completeColumnIndex = Math.floor(subColumnIndex / splitFactor);
1565
- // For the first complete column (index 0), always use the first upper round (index 0)
1566
- if (completeColumnIndex === 0) {
1498
+ if (completeColumnIndex === 0)
1567
1499
  return 0;
1568
- }
1569
- // For subsequent columns, map based on the ratio
1570
- // We subtract 1 because the first column is handled separately
1571
- const adjustedColumnIndex = completeColumnIndex - 1;
1572
- // Calculate which upper round this column should use
1573
- // We add 1 because we start from the second upper round (index 1)
1574
- return Math.floor(adjustedColumnIndex / upperToLowerRatio) + 1;
1500
+ return Math.floor((completeColumnIndex - 1) / upperToLowerRatio) + 1;
1575
1501
  };
1576
1502
 
1577
1503
  const createRoundBracketSubColumnRelativeToFirstRound = (config) => {
@@ -1588,7 +1514,6 @@ const createRoundBracketSubColumnRelativeToFirstRound = (config) => {
1588
1514
  ? Math.max(options.roundHeaderGap, options.finalRoundHeaderGap ?? 0)
1589
1515
  : options.roundHeaderGap;
1590
1516
  const elementsToCreate = [];
1591
- // Only include a header row if headers exist
1592
1517
  if (options.roundHeaderHeight > 0) {
1593
1518
  elementsToCreate.push({
1594
1519
  type: 'header',
@@ -1605,7 +1530,6 @@ const createRoundBracketSubColumnRelativeToFirstRound = (config) => {
1605
1530
  elementHeight: roundHeaderGap,
1606
1531
  });
1607
1532
  }
1608
- // Add match elements to create
1609
1533
  for (const [matchIndex, match] of matches.entries()) {
1610
1534
  const isLastMatch = matchIndex === matches.length - 1;
1611
1535
  // One row per match this one is fed by. A fractional factor has no whole feeder row to align to,
@@ -1649,16 +1573,14 @@ const createRoundBracketSubColumnRelativeToFirstRound = (config) => {
1649
1573
  elementHeight: config.bottomPadding,
1650
1574
  });
1651
1575
  }
1652
- // Create all elements at once
1653
1576
  for (const elementData of elementsToCreate) {
1654
- const { element } = createBracketElement(elementData);
1577
+ const element = createBracketElement(elementData);
1655
1578
  pushElement(element);
1656
1579
  }
1657
1580
  return subColumn;
1658
1581
  };
1659
1582
 
1660
1583
  const heightOfElement = (element) => element.parts.reduce((total, part) => total + part.dimensions.height, 0);
1661
- /** Read off the built rows rather than re-derived, so the two stay in step. */
1662
1584
  const cardGeometry = (subColumn) => {
1663
1585
  let height = 0;
1664
1586
  let cardTop = null;
@@ -1721,6 +1643,7 @@ const createBracketGapMasterColumn = (config) => {
1721
1643
  for (const section of lastMasterColumn.sections) {
1722
1644
  const { masterColumnSection, pushSubColumn } = createBracketMasterColumnSection({
1723
1645
  type: 'gap',
1646
+ padding: section.padding && { ...section.padding, left: 0, right: 0 },
1724
1647
  });
1725
1648
  const lastSubColumn = section.subColumns[section.subColumns.length - 1];
1726
1649
  if (!lastSubColumn) {
@@ -1780,7 +1703,7 @@ const createBracketGapMasterColumn = (config) => {
1780
1703
  partHeights,
1781
1704
  };
1782
1705
  })();
1783
- pushElement(createBracketElement(elementToCreate).element);
1706
+ pushElement(createBracketElement(elementToCreate));
1784
1707
  }
1785
1708
  pushSubColumn(subColumn);
1786
1709
  pushSection(masterColumnSection);
@@ -1798,8 +1721,8 @@ const resolveBracketGridRowSpan = (bracket, options) => {
1798
1721
  const createDoubleEliminationGrid = (bracketData, options, components) => {
1799
1722
  const resolvedOptions = resolveBracketGridRowSpan(bracketData, options);
1800
1723
  const grid = createBracketGrid({ spanElementWidth: resolvedOptions.columnWidth });
1801
- const presentUpperBracketRounds = Array.from(bracketData.roundsByType.getOrThrow(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET).values());
1802
- const lowerBracketRounds = Array.from(bracketData.roundsByType.getOrThrow(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET).values());
1724
+ const presentUpperBracketRounds = Array.from(bracketData.roundsByType.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.UPPER_BRACKET)?.values() ?? []);
1725
+ const lowerBracketRounds = Array.from(bracketData.roundsByType.get(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET)?.values() ?? []);
1803
1726
  // A complete winner bracket has (lowerRounds / 2) + 1 rounds. If fewer are present, the winner
1804
1727
  // bracket starts later than round 1 - e.g. a small double elimination where lower-round 1 is
1805
1728
  // seeded directly from a group phase and has no winner round feeding it. Pad the front of the
@@ -1844,7 +1767,7 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
1844
1767
  firstUpperRound.matchCount * options.matchHeight +
1845
1768
  Math.max(0, firstUpperRound.matchCount - 1) * options.rowGap;
1846
1769
  const { subColumn, pushElement } = createBracketSubColumn({ span: config.span });
1847
- const { element } = createBracketElement({
1770
+ const element = createBracketElement({
1848
1771
  area: '.',
1849
1772
  type: 'colGap',
1850
1773
  elementHeight: emptyUpperHeight,
@@ -1858,7 +1781,7 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
1858
1781
  const { subColumn, pushElement } = createBracketSubColumn({
1859
1782
  span: { isStart: true, isEnd: true },
1860
1783
  });
1861
- const { element } = createBracketElement({
1784
+ const element = createBracketElement({
1862
1785
  area: '.',
1863
1786
  type: 'roundGap',
1864
1787
  elementHeight: options.rowRoundGap,
@@ -1908,17 +1831,14 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
1908
1831
  }
1909
1832
  // May be null for a leading winner slot that does not exist (front-truncated winner bracket).
1910
1833
  const upperRound = upperBracketRounds[currentUpperRoundIndex];
1911
- // For upper bracket spans - check if this round is different from the previous occurrence
1912
1834
  const isUpperSpanStart = isFirstSubColumnInMasterColumn
1913
1835
  ? lowerRoundIndex === 0 || lastRoundLastSubColumnUpperIndex !== currentUpperRoundIndex
1914
1836
  : previousUpperRoundIndex !== currentUpperRoundIndex;
1915
- // For upper bracket spans - check if this round will be different in the next occurrence
1916
1837
  const isUpperSpanEnd = isLastSubColumnInMasterColumn
1917
1838
  ? isLastLowerRound ||
1918
1839
  calculateUpperRoundIndex((lowerRoundIndex + 1) * columnSplitFactor, upperToLowerRatio, columnSplitFactor) !==
1919
1840
  currentUpperRoundIndex
1920
1841
  : nextUpperRoundIndex !== currentUpperRoundIndex;
1921
- // For lower bracket spans - similar logic
1922
1842
  const isLowerSpanStart = isFirstSubColumnInMasterColumn
1923
1843
  ? lowerRoundIndex === 0 || lastRoundLastSubColumnLowerIndex !== currentLowerRoundIndex
1924
1844
  : previousLowerRoundIndex !== currentLowerRoundIndex;
@@ -2008,7 +1928,7 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
2008
1928
  elementHeight: options.rowRoundGap,
2009
1929
  partHeights: [options.rowRoundGap],
2010
1930
  });
2011
- upperLowerGapSubColumn.pushElement(upperLowerGapElement.element);
1931
+ upperLowerGapSubColumn.pushElement(upperLowerGapElement);
2012
1932
  pushUpperLowerSubColumn(upperLowerGapSubColumn.subColumn);
2013
1933
  if (thirdPlaceRound) {
2014
1934
  const lowerSubColumn = createRoundBracketSubColumnRelativeToFirstRound({
@@ -2047,7 +1967,7 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
2047
1967
  elementHeight: element.dimensions.height,
2048
1968
  partHeights: element.parts.map((p) => p.dimensions.height),
2049
1969
  });
2050
- lowerSubColumn.pushElement(el.element);
1970
+ lowerSubColumn.pushElement(el);
2051
1971
  }
2052
1972
  pushLowerSubColumn(lowerSubColumn.subColumn);
2053
1973
  }
@@ -2087,35 +2007,22 @@ const createDoubleEliminationGrid = (bracketData, options, components) => {
2087
2007
  };
2088
2008
  };
2089
2009
 
2090
- /**
2091
- * Where a round that never folded sits in its block's centre chain. A bracket's own rounds go first, by
2092
- * depth; the rounds that decide the tournament follow in the order they are played.
2093
- */
2094
2010
  const CENTRE_CHAIN_ORDER = {
2095
2011
  [COMMON_BRACKET_ROUND_TYPE.FINAL]: 1,
2096
2012
  [DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL]: 2,
2097
2013
  [COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE]: 3,
2098
2014
  };
2099
- /** The card drawn for a round's matches - the deciding one is the taller `finalMatch`. */
2100
2015
  const isFinalMatchRound = (round, hasReverseFinal) => hasReverseFinal
2101
2016
  ? round.type === DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL
2102
2017
  : round.type === COMMON_BRACKET_ROUND_TYPE.FINAL;
2103
- /**
2104
- * A chain round takes exactly the room its cards need. Every other column reserves the same height for
2105
- * every round (`createRoundBracketSubColumnRelativeToFirstRound`), which the chain cannot do - it stacks
2106
- * several rounds in one column.
2107
- */
2108
2018
  const chainRoundHeight = (config) => {
2109
2019
  const { round, options, hasReverseFinal } = config;
2110
2020
  const cardHeight = isFinalMatchRound(round, hasReverseFinal) ? options.finalMatchHeight : options.matchHeight;
2111
2021
  return round.matchCount * cardHeight + Math.max(0, round.matchCount - 1) * options.rowGap;
2112
2022
  };
2113
- /**
2114
- * Splits one bracket's rounds into the fold's three parts and works out the heights that keep the two
2115
- * blocks aligned. `null` when the bracket has no rounds at all.
2116
- */
2117
2023
  const createStackedBlock = (config) => {
2118
- const { rounds, options, hasReverseFinal } = config;
2024
+ const { rounds, hasReverseFinal } = config;
2025
+ const options = resolveBracketGridRowSpan({ rounds }, config.options);
2119
2026
  const halves = rounds.filter((round) => !!round.mirrorRoundType);
2120
2027
  const left = halves
2121
2028
  .filter((round) => round.mirrorRoundType === BRACKET_ROUND_MIRROR_TYPE.LEFT)
@@ -2136,7 +2043,10 @@ const createStackedBlock = (config) => {
2136
2043
  const firstRound = leftSlots[0] ?? chain[0];
2137
2044
  if (!firstRound)
2138
2045
  return null;
2139
- const content = firstRound.matchCount * options.matchHeight + Math.max(0, firstRound.matchCount - 1) * options.rowGap;
2046
+ const rows = options.rowSpanMatchCount ?? firstRound.matchCount;
2047
+ const rowsHeight = (count) => count * options.matchHeight + Math.max(0, count - 1) * options.rowGap;
2048
+ const content = rowsHeight(rows);
2049
+ const columnContent = (round) => rowsHeight(Math.max(rows, round.matchCount));
2140
2050
  let chainHeight = 0;
2141
2051
  for (const [index, round] of chain.entries()) {
2142
2052
  chainHeight += (index ? options.rowRoundGap : 0) + chainRoundHeight({ round, options, hasReverseFinal });
@@ -2147,15 +2057,17 @@ const createStackedBlock = (config) => {
2147
2057
  // there grows the block rather than sliding up: an anchor a few px off centre turns every one of those
2148
2058
  // connectors into a diagonal, in a drawing where nothing else is.
2149
2059
  const spacerTop = Math.max(0, (content - anchorHeight) / 2);
2060
+ const body = Math.max(spacerTop + chainHeight, ...[...leftSlots, ...right].map(columnContent));
2150
2061
  return {
2151
2062
  firstRound,
2152
2063
  left: leftSlots,
2153
2064
  right,
2154
2065
  chain,
2155
- content,
2156
- bottomPadding: Math.max(0, spacerTop + chainHeight - content),
2066
+ options,
2067
+ body,
2068
+ columnContent,
2157
2069
  spacerTop,
2158
- spacerBottom: Math.max(0, content - spacerTop - chainHeight),
2070
+ spacerBottom: Math.max(0, body - spacerTop - chainHeight),
2159
2071
  };
2160
2072
  };
2161
2073
  /**
@@ -2166,8 +2078,7 @@ const createStackedBlock = (config) => {
2166
2078
  * @internal
2167
2079
  */
2168
2080
  const createStackedDoubleEliminationGrid = (bracketData, options, components) => {
2169
- const resolvedOptions = resolveBracketGridRowSpan(bracketData, options);
2170
- const grid = createBracketGrid({ spanElementWidth: resolvedOptions.columnWidth });
2081
+ const grid = createBracketGrid({ spanElementWidth: options.columnWidth });
2171
2082
  const roundsOfType = (type) => Array.from(bracketData.roundsByType.get(type)?.values() ?? []);
2172
2083
  const hasReverseFinal = !!roundsOfType(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL).length;
2173
2084
  const winners = createStackedBlock({
@@ -2176,7 +2087,7 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
2176
2087
  ...roundsOfType(COMMON_BRACKET_ROUND_TYPE.FINAL),
2177
2088
  ...roundsOfType(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.REVERSE_FINAL),
2178
2089
  ],
2179
- options: resolvedOptions,
2090
+ options,
2180
2091
  hasReverseFinal,
2181
2092
  });
2182
2093
  const losers = createStackedBlock({
@@ -2184,7 +2095,7 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
2184
2095
  ...roundsOfType(DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE.LOWER_BRACKET),
2185
2096
  ...roundsOfType(COMMON_BRACKET_ROUND_TYPE.THIRD_PLACE),
2186
2097
  ],
2187
- options: resolvedOptions,
2098
+ options,
2188
2099
  hasReverseFinal,
2189
2100
  });
2190
2101
  if (!winners || !losers) {
@@ -2192,12 +2103,9 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
2192
2103
  }
2193
2104
  const blocks = [winners, losers];
2194
2105
  const headerBlock = options.roundHeaderHeight > 0 ? options.roundHeaderHeight + options.roundHeaderGap : 0;
2195
- const blockHeight = (block) => headerBlock + block.content + block.bottomPadding;
2196
- // Both blocks put their centre in the same master column, so the chains - and the grand final's line
2197
- // down to the losers champion - run up one strip rather than two.
2106
+ const blockHeight = (block) => headerBlock + block.body;
2198
2107
  const centreIndex = Math.max(...blocks.map((block) => block.left.length));
2199
2108
  const columnCount = centreIndex + 1 + Math.max(...blocks.map((block) => block.right.length));
2200
- /** A column this block does not reach into, holding the height its rounds would have taken. */
2201
2109
  const createPlaceholderSubColumn = (block) => {
2202
2110
  const { subColumn, pushElement } = createBracketSubColumn({
2203
2111
  span: { isStart: true, isEnd: true },
@@ -2208,7 +2116,7 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
2208
2116
  type: 'colGap',
2209
2117
  elementHeight: height,
2210
2118
  partHeights: [height],
2211
- }).element);
2119
+ }));
2212
2120
  return subColumn;
2213
2121
  };
2214
2122
  const createChainSubColumn = (block) => {
@@ -2286,11 +2194,10 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
2286
2194
  });
2287
2195
  }
2288
2196
  for (const elementData of elementsToCreate) {
2289
- pushElement(createBracketElement(elementData).element);
2197
+ pushElement(createBracketElement(elementData));
2290
2198
  }
2291
2199
  return subColumn;
2292
2200
  };
2293
- /** What this block draws in the column at `columnIndex`, centred on the shared middle column. */
2294
2201
  const createBlockSubColumn = (block, columnIndex) => {
2295
2202
  const slotIndex = columnIndex - (centreIndex - block.left.length);
2296
2203
  if (slotIndex === block.left.length)
@@ -2303,14 +2210,13 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
2303
2210
  return createRoundBracketSubColumnRelativeToFirstRound({
2304
2211
  firstRound: block.firstRound,
2305
2212
  round,
2306
- options: resolvedOptions,
2213
+ options: block.options,
2307
2214
  hasReverseFinal,
2308
- bottomPadding: block.bottomPadding,
2215
+ bottomPadding: block.body - block.columnContent(round),
2309
2216
  span: { isStart: true, isEnd: true },
2310
2217
  components,
2311
2218
  });
2312
2219
  };
2313
- /** The band the print reference leaves empty: the two blocks are independent, so nothing crosses it. */
2314
2220
  const createBandSubColumn = () => {
2315
2221
  const { subColumn, pushElement } = createBracketSubColumn({
2316
2222
  span: { isStart: true, isEnd: true },
@@ -2320,7 +2226,7 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
2320
2226
  type: 'roundGap',
2321
2227
  elementHeight: options.rowRoundGap,
2322
2228
  partHeights: [options.rowRoundGap],
2323
- }).element);
2229
+ }));
2324
2230
  return subColumn;
2325
2231
  };
2326
2232
  for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
@@ -2343,7 +2249,6 @@ const createStackedDoubleEliminationGrid = (bracketData, options, components) =>
2343
2249
  }));
2344
2250
  }
2345
2251
  }
2346
- // No spans: a stacked block draws one round per column, so nothing reaches across its neighbours.
2347
2252
  grid.calculateDimensions();
2348
2253
  const finalizedGrid = finalizeBracketGrid(grid);
2349
2254
  return {
@@ -2451,32 +2356,45 @@ const createSingleEliminationGrid = (bracketData, options, components) => {
2451
2356
 
2452
2357
  const FALLBACK_MATCH_RELATION_POSITION = -1;
2453
2358
  const generateMatchPosition = (match, factor) => Math.ceil(match.position * factor);
2359
+ const generatePreviousRoundMatchPositions = (relation, match) => {
2360
+ const feederFactor = relation.currentRound.matchCount / relation.previousRound.matchCount;
2361
+ const feederPositions = [];
2362
+ for (const feeder of relation.previousRound.matches.values()) {
2363
+ if (generateMatchPosition(feeder, feederFactor) === match.position)
2364
+ feederPositions.push(feeder.position);
2365
+ }
2366
+ const first = feederPositions[0];
2367
+ const last = feederPositions[feederPositions.length - 1];
2368
+ if (first !== undefined && last !== undefined) {
2369
+ return { previousUpperRoundMatchPosition: first, previousLowerRoundMatchPosition: last };
2370
+ }
2371
+ const double = relation.previousRoundMatchFactor === 2 ? 1 : 0;
2372
+ return {
2373
+ previousUpperRoundMatchPosition: (generateMatchPosition(match, relation.previousRoundMatchFactor) -
2374
+ double),
2375
+ previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousRoundMatchFactor),
2376
+ };
2377
+ };
2454
2378
  const generateMatchRelationPositions = (relation, match) => {
2455
2379
  switch (relation.type) {
2380
+ case 'none':
2381
+ return null;
2456
2382
  case 'nothing-to-one':
2457
2383
  return {
2458
2384
  nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
2459
2385
  previousUpperRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
2460
2386
  previousLowerRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
2461
2387
  };
2462
- case 'one-to-nothing': {
2463
- const double = relation.previousRoundMatchFactor === 2 ? 1 : 0;
2388
+ case 'one-to-nothing':
2464
2389
  return {
2465
2390
  nextRoundMatchPosition: FALLBACK_MATCH_RELATION_POSITION,
2466
- previousUpperRoundMatchPosition: (generateMatchPosition(match, relation.previousRoundMatchFactor) -
2467
- double),
2468
- previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousRoundMatchFactor),
2391
+ ...generatePreviousRoundMatchPositions(relation, match),
2469
2392
  };
2470
- }
2471
- case 'one-to-one': {
2472
- const double = relation.previousRoundMatchFactor === 2 ? 1 : 0;
2393
+ case 'one-to-one':
2473
2394
  return {
2474
2395
  nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
2475
- previousUpperRoundMatchPosition: (generateMatchPosition(match, relation.previousRoundMatchFactor) -
2476
- double),
2477
- previousLowerRoundMatchPosition: generateMatchPosition(match, relation.previousRoundMatchFactor),
2396
+ ...generatePreviousRoundMatchPositions(relation, match),
2478
2397
  };
2479
- }
2480
2398
  case 'two-to-one':
2481
2399
  return {
2482
2400
  nextRoundMatchPosition: generateMatchPosition(match, relation.nextRoundMatchFactor),
@@ -2620,7 +2538,7 @@ const createTwoToNothingRelation$1 = (params) => {
2620
2538
  };
2621
2539
  const generateMatchRelationsNew = (bracketData, options = {}) => {
2622
2540
  const sourceMatches = options.source?.matches ?? [];
2623
- const hasDeclaredGraph = options.previousMatchIds !== undefined || sourceMatches.some((match) => defaultPreviousMatchIds(match).length > 0);
2541
+ const hasDeclaredGraph = options.previousMatchIds !== undefined || sourceMatches.some((match) => matchOutcomeSlots(match).length > 0);
2624
2542
  if (hasDeclaredGraph) {
2625
2543
  return generateDeclaredMatchRelations({
2626
2544
  bracket: bracketData,
@@ -2637,9 +2555,6 @@ const generateMatchRelationsNew = (bracketData, options = {}) => {
2637
2555
  for (const match of bracketData.matches.values()) {
2638
2556
  const relation = match.round.relation;
2639
2557
  const positions = generateMatchRelationPositions(relation, match);
2640
- // A round without a resolved relation (e.g. a truncated bracket where a round has no
2641
- // successor/predecessor to wire up) keeps the placeholder relation from createBracket.
2642
- // Skip it so the match renders without connector lines instead of crashing the whole bracket.
2643
2558
  if (!positions)
2644
2559
  continue;
2645
2560
  const { nextRoundMatchPosition, previousUpperRoundMatchPosition, previousLowerRoundMatchPosition } = positions;
@@ -2694,9 +2609,10 @@ const generateMatchRelationsNew = (bracketData, options = {}) => {
2694
2609
  }
2695
2610
  return matchRelations;
2696
2611
  };
2697
- const defaultPreviousMatchIds = (match) => [match.homeSource, match.awaySource]
2698
- .filter((slot) => slot?.kind === 'match-outcome' && slot.matchId !== null)
2699
- .map((slot) => slot?.matchId);
2612
+ const matchOutcomeSlots = (match) => [match.homeSource, match.awaySource].filter((slot) => slot?.kind === 'match-outcome' && !!slot.matchId);
2613
+ const defaultPreviousMatchIds = (match) => matchOutcomeSlots(match)
2614
+ .filter((slot) => slot.role !== 'loser')
2615
+ .map((slot) => slot.matchId);
2700
2616
  const generateDeclaredMatchRelations = (options) => {
2701
2617
  const { bracket, sourceMatches, previousMatchIds } = options;
2702
2618
  const previousByMatchId = new Map(sourceMatches.map((match) => [match.id, previousMatchIds(match)]));
@@ -2927,43 +2843,28 @@ const handleFinalRound = (params) => {
2927
2843
  firstLowerRound,
2928
2844
  }));
2929
2845
  }
2930
- if (isAsyncBracket) {
2931
- const preFinalLowerRound = lowerRounds[lowerRounds.length - 2];
2932
- const prePreFinalLowerRound = lowerRounds[lowerRounds.length - 3] || null;
2933
- if (!preFinalLowerRound)
2934
- throw new BracketRuntimeError(BRACKET_ERROR_CODES.ROUND_RELATION_INVALID, 'preFinalLowerRound is null');
2935
- relations.push(createOneToOneRelation({
2936
- currentRound: finalLowerRound,
2937
- previousRound: preFinalLowerRound,
2938
- nextRound: currentUpperRound,
2939
- rootRound: firstLowerRound,
2940
- }));
2941
- if (prePreFinalLowerRound) {
2942
- relations.push(createOneToOneRelation({
2943
- currentRound: preFinalLowerRound,
2944
- previousRound: prePreFinalLowerRound,
2945
- nextRound: finalLowerRound,
2946
- rootRound: firstLowerRound,
2947
- }));
2948
- }
2949
- else {
2950
- relations.push(createNothingToOneRelation({
2951
- currentRound: preFinalLowerRound,
2952
- nextRound: finalLowerRound,
2953
- }));
2954
- }
2846
+ const preFinalLowerRound = lowerRounds[lowerRounds.length - 2] || null;
2847
+ if (!preFinalLowerRound) {
2848
+ relations.push(createNothingToOneRelation({ currentRound: finalLowerRound, nextRound: currentUpperRound }));
2849
+ return;
2955
2850
  }
2956
- else {
2957
- const previousLowerRound = lowerRounds[lowerRounds.length - 2] || null;
2958
- if (!previousLowerRound)
2959
- throw new BracketRuntimeError(BRACKET_ERROR_CODES.ROUND_RELATION_INVALID, 'previousLowerRound is null');
2960
- relations.push(createOneToOneRelation({
2961
- currentRound: finalLowerRound,
2962
- previousRound: previousLowerRound,
2963
- nextRound: currentUpperRound,
2851
+ relations.push(createOneToOneRelation({
2852
+ currentRound: finalLowerRound,
2853
+ previousRound: preFinalLowerRound,
2854
+ nextRound: currentUpperRound,
2855
+ rootRound: firstLowerRound,
2856
+ }));
2857
+ if (!isAsyncBracket)
2858
+ return;
2859
+ const prePreFinalLowerRound = lowerRounds[lowerRounds.length - 3] || null;
2860
+ relations.push(prePreFinalLowerRound
2861
+ ? createOneToOneRelation({
2862
+ currentRound: preFinalLowerRound,
2863
+ previousRound: prePreFinalLowerRound,
2864
+ nextRound: finalLowerRound,
2964
2865
  rootRound: firstLowerRound,
2965
- }));
2966
- }
2866
+ })
2867
+ : createNothingToOneRelation({ currentRound: preFinalLowerRound, nextRound: finalLowerRound }));
2967
2868
  };
2968
2869
  const handleFirstRound = (params) => {
2969
2870
  const { relations, currentUpperRound, nextUpperRound, lowerRounds, currentUpperRoundIndex } = params;
@@ -3137,11 +3038,9 @@ const createBracket = (source, options) => {
3137
3038
  const rounds = new BracketMap();
3138
3039
  const roundsByType = new BracketMap();
3139
3040
  for (const roundBase of bracketNewBase.rounds.values()) {
3140
- const newRound = {
3141
- ...roundBase,
3142
- matches: new BracketMap(),
3143
- relation: { type: 'dummy' },
3144
- };
3041
+ const relation = { type: 'none' };
3042
+ const newRound = { ...roundBase, matches: new BracketMap(), relation };
3043
+ relation.currentRound = newRound;
3145
3044
  rounds.set(roundBase.id, newRound);
3146
3045
  if (!roundsByType.has(roundBase.type)) {
3147
3046
  roundsByType.set(roundBase.type, new BracketMap());
@@ -3164,14 +3063,16 @@ const createBracket = (source, options) => {
3164
3063
  const awayParticipant = matchBase.away
3165
3064
  ? { ...matchBase.away, matches: new BracketMap() }
3166
3065
  : null;
3066
+ const relation = { type: 'none', currentRound: round };
3167
3067
  const newMatch = {
3168
3068
  ...matchBase,
3169
3069
  home: homeParticipant,
3170
3070
  away: awayParticipant,
3171
3071
  winner: null,
3172
3072
  round,
3173
- relation: { type: 'dummy' },
3073
+ relation,
3174
3074
  };
3075
+ relation.currentMatch = newMatch;
3175
3076
  if (matchBase.winner) {
3176
3077
  const winnerParticipant = homeParticipant?.id === matchBase.winner.id ? homeParticipant : awayParticipant;
3177
3078
  if (!winnerParticipant)
@@ -3278,26 +3179,30 @@ const resolveSlotOccupant = (options) => {
3278
3179
  const resolveSlot = (options) => {
3279
3180
  const { walk, match, side } = options;
3280
3181
  const visitKey = `${match.id}:${side}`;
3281
- if (walk.visited.has(visitKey)) {
3282
- walk.cycleHits++;
3182
+ const openDepth = walk.visitDepths.get(visitKey);
3183
+ if (openDepth !== undefined) {
3184
+ walk.shallowestCycleHit = Math.min(walk.shallowestCycleHit, openDepth);
3283
3185
  return null;
3284
3186
  }
3285
3187
  const resolved = walk.resolved.get(visitKey);
3286
3188
  if (resolved !== undefined)
3287
3189
  return resolved;
3288
- walk.visited.add(visitKey);
3289
- const cycleHitsBefore = walk.cycleHits;
3190
+ const depth = walk.visitDepths.size;
3191
+ const shallowestCycleHitBefore = walk.shallowestCycleHit;
3192
+ walk.visitDepths.set(visitKey, depth);
3193
+ walk.shallowestCycleHit = Infinity;
3290
3194
  try {
3291
3195
  const result = resolveSlotOccupant({ walk, match, side });
3292
- // Only a result the visit guard never interfered with is a function of the slot alone. One that
3293
- // did hit the guard depends on the path that reached it, so caching it would answer a later path
3294
- // with a cycle's `null`.
3295
- if (walk.cycleHits === cycleHitsBefore)
3196
+ // A result whose cycle guard only fired on this slot or below it is a function of the slot alone.
3197
+ // One that hit a slot above it depends on the path that reached it, so caching it would answer a
3198
+ // later path with a cycle's `null`.
3199
+ if (walk.shallowestCycleHit >= depth)
3296
3200
  walk.resolved.set(visitKey, result);
3297
3201
  return result;
3298
3202
  }
3299
3203
  finally {
3300
- walk.visited.delete(visitKey);
3204
+ walk.visitDepths.delete(visitKey);
3205
+ walk.shallowestCycleHit = Math.min(walk.shallowestCycleHit, shallowestCycleHitBefore);
3301
3206
  }
3302
3207
  };
3303
3208
  const createResolveWalk = (options) => ({
@@ -3305,15 +3210,23 @@ const createResolveWalk = (options) => ({
3305
3210
  picks: options.picks,
3306
3211
  realParticipantOutranksPick: options.realParticipantOutranksPick ?? (() => false),
3307
3212
  keepPickWhileFeederSideIsOpen: options.keepPickWhileFeederSideIsOpen ?? false,
3308
- visited: new Set(),
3213
+ visitDepths: new Map(),
3309
3214
  resolved: new Map(),
3310
- cycleHits: 0,
3215
+ shallowestCycleHit: Infinity,
3311
3216
  });
3312
- /** Who the viewer's own picks put in a slot, or `null` while their picks do not reach it. */
3313
- const resolveBracketSlot = (options) => {
3314
- const match = options.bracket.matches.get(options.matchId);
3315
- return match ? resolveSlot({ walk: createResolveWalk(options), match, side: options.side }) : null;
3217
+ /**
3218
+ * @internal A {@link resolveBracketSlot} that shares one memo across its calls. Valid only while the
3219
+ * picks it was created with do not change.
3220
+ */
3221
+ const createBracketSlotResolver = (options) => {
3222
+ const walk = createResolveWalk(options);
3223
+ return (matchId, side) => {
3224
+ const match = options.bracket.matches.get(matchId);
3225
+ return match ? resolveSlot({ walk, match, side }) : null;
3226
+ };
3316
3227
  };
3228
+ /** Who the viewer's own picks put in a slot, or `null` while their picks do not reach it. */
3229
+ const resolveBracketSlot = (options) => createBracketSlotResolver(options)(options.matchId, options.side);
3317
3230
 
3318
3231
  // Grouped by type and depth rather than by round id: a mirrored layout splits one round into two
3319
3232
  // half-rounds to draw either side of the fold, and a pick has to be able to travel across it. Ordered
@@ -3378,16 +3291,14 @@ const migrateBracketPicks = (options) => {
3378
3291
  const travelling = [];
3379
3292
  const strandedHere = new Set();
3380
3293
  const sidesOf = (matchId) => sidesByMatchId.get(matchId) ?? [null, null];
3294
+ const resolve = createBracketSlotResolver({
3295
+ bracket,
3296
+ picks: settled,
3297
+ realParticipantOutranksPick: options.realParticipantOutranksPick,
3298
+ keepPickWhileFeederSideIsOpen: options.keepPickWhileFeederSideIsOpen,
3299
+ });
3381
3300
  for (const match of round) {
3382
- const resolve = (side) => resolveBracketSlot({
3383
- bracket,
3384
- picks: settled,
3385
- matchId: match.id,
3386
- side,
3387
- realParticipantOutranksPick: options.realParticipantOutranksPick,
3388
- keepPickWhileFeederSideIsOpen: options.keepPickWhileFeederSideIsOpen,
3389
- });
3390
- sidesByMatchId.set(match.id, [resolve('home'), resolve('away')]);
3301
+ sidesByMatchId.set(match.id, [resolve(match.id, 'home'), resolve(match.id, 'away')]);
3391
3302
  }
3392
3303
  for (const match of round) {
3393
3304
  const pick = pickAsMade(match.id);
@@ -3522,7 +3433,7 @@ const createSwissGrid = (bracketData, options, components) => {
3522
3433
  partHeights: [options.roundHeaderGap],
3523
3434
  elementHeight: options.roundHeaderGap,
3524
3435
  });
3525
- headerSubColumn.pushElement(headerElement.element, headerGapElement.element);
3436
+ headerSubColumn.pushElement(headerElement, headerGapElement);
3526
3437
  headerColumnSection.pushSubColumn(headerSubColumn.subColumn);
3527
3438
  pushSection(headerColumnSection.masterColumnSection);
3528
3439
  }
@@ -3564,7 +3475,7 @@ const createSwissGrid = (bracketData, options, components) => {
3564
3475
  }
3565
3476
  }
3566
3477
  for (const elementData of elementsToCreate) {
3567
- const { element } = createBracketElement(elementData);
3478
+ const element = createBracketElement(elementData);
3568
3479
  pushElement(element);
3569
3480
  }
3570
3481
  pushSubColumn(subColumn);
@@ -3585,7 +3496,7 @@ const createSwissGrid = (bracketData, options, components) => {
3585
3496
  elementHeight: rowRoundGap,
3586
3497
  partHeights: [rowRoundGap],
3587
3498
  });
3588
- groupGapSubColumn.pushElement(groupGapElement.element);
3499
+ groupGapSubColumn.pushElement(groupGapElement);
3589
3500
  groupGapColumnSection.pushSubColumn(groupGapSubColumn.subColumn);
3590
3501
  pushSection(groupGapColumnSection.masterColumnSection);
3591
3502
  }
@@ -3609,7 +3520,7 @@ const createSwissGrid = (bracketData, options, components) => {
3609
3520
  elementHeight: fillerHeight,
3610
3521
  partHeights: [fillerHeight],
3611
3522
  });
3612
- fillerSubColumn.pushElement(fillerElement.element);
3523
+ fillerSubColumn.pushElement(fillerElement);
3613
3524
  fillerColumnSection.pushSubColumn(fillerSubColumn.subColumn);
3614
3525
  pushSection(fillerColumnSection.masterColumnSection);
3615
3526
  }
@@ -3684,7 +3595,7 @@ const standingPickOutcome = (options) => {
3684
3595
  * The order to show a group table in before anyone moves a row: every stored pick takes the position it was
3685
3596
  * stored on, and the participants without one follow in the order the backend listed them, filling the gaps.
3686
3597
  *
3687
- * A pick the field cannot hold is dropped rather than drawn - a position outside
3598
+ * A pick the field cannot hold is dropped rather than drawn - a position that is not an integer in
3688
3599
  * `1..participantIds.length`, an id that is not in the field, a second pick for a position or for a
3689
3600
  * participant already placed (the lowest position keeps it). The result is therefore always a permutation
3690
3601
  * of `participantIds`.
@@ -3695,7 +3606,8 @@ const standingPickStartOrder = (options) => {
3695
3606
  const byPosition = new Map();
3696
3607
  const placed = new Set();
3697
3608
  for (const pick of [...picks].sort((left, right) => left.position - right.position)) {
3698
- const holds = pick.position >= 1 &&
3609
+ const holds = Number.isInteger(pick.position) &&
3610
+ pick.position >= 1 &&
3699
3611
  pick.position <= participantIds.length &&
3700
3612
  field.has(pick.participantId) &&
3701
3613
  !placed.has(pick.participantId) &&
@@ -3719,5 +3631,5 @@ const standingPickStartOrder = (options) => {
3719
3631
  * Generated bundle index. Do not edit.
3720
3632
  */
3721
3633
 
3722
- export { BRACKET_DATA_LAYOUT, BRACKET_ERROR_CODES, BRACKET_ROUND_MIRROR_TYPE, BRACKET_SWISS_GROUP_COLOR_TYPE, BracketMap, COMMON_BRACKET_ROUND_TYPE, DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE, GROUP_BRACKET_ROUND_TYPE, SINGLE_ELIMINATION_BRACKET_ROUND_TYPE, STANDING_PICK_OUTCOME, SWISS_ADVANCE_WINS, SWISS_BRACKET_ROUND_TYPE, SWISS_ELIMINATE_LOSSES, TOURNAMENT_MODE, bracketGridNaturalWidth, calculateColumnSplitFactor, calculateLowerRoundIndex, calculateUpperLowerRatio, calculateUpperRoundIndex, createBracket, createBracketBase, createBracketContinueMasterColumn, createBracketElement, createBracketElementPart, createBracketGapMasterColumn, createBracketGrid, createBracketMasterColumn, createBracketMasterColumnSection, createBracketSubColumn, createDoubleEliminationGrid, createFoldedThirdPlaceSection, createMatchesMapBase, createNewMatchParticipantBase, createParticipantsMapBase, createRoundBracketSubColumnRelativeToFirstRound, createRoundsMapBase, createSingleEliminationGrid, createStackedDoubleEliminationGrid, createSwissGrid, curvePath, drawMan, drawSwissMan, finalizeBracketGrid, generateBracketRoundSwissGroupMaps, generateMatchPosition, generateMatchRelationPositions, generateMatchRelationsNew, generateRoundRelationsNew, getAvailableSwissGroupsForRound, getBracketContinueMatches, getSwissGroupColorType, gutterPath, isBracketContinueMatch, isBracketSlotPredictable, linePath, migrateBracketPicks, path, resolveBracketGridRowSpan, resolveBracketLayout, resolveBracketSlot, standingPickOutcome, standingPickStartOrder, verticalPath };
3634
+ export { BRACKET_DATA_LAYOUT, BRACKET_ERROR_CODES, BRACKET_ROUND_MIRROR_TYPE, BRACKET_SWISS_GROUP_COLOR_TYPE, BracketMap, COMMON_BRACKET_ROUND_TYPE, DOUBLE_ELIMINATION_BRACKET_ROUND_TYPE, GROUP_BRACKET_ROUND_TYPE, SINGLE_ELIMINATION_BRACKET_ROUND_TYPE, STANDING_PICK_OUTCOME, SWISS_ADVANCE_WINS, SWISS_BRACKET_ROUND_TYPE, SWISS_ELIMINATE_LOSSES, TERMINAL_ROUND_SORT_PRIORITY, TOURNAMENT_MODE, bracketGridNaturalWidth, calculateColumnSplitFactor, calculateLowerRoundIndex, calculateUpperLowerRatio, calculateUpperRoundIndex, createBracket, createBracketBase, createBracketContinueMasterColumn, createBracketElement, createBracketElementPart, createBracketGapMasterColumn, createBracketGrid, createBracketMasterColumn, createBracketMasterColumnSection, createBracketSlotResolver, createBracketSubColumn, createDoubleEliminationGrid, createFoldedThirdPlaceSection, createMatchesMapBase, createNewMatchParticipantBase, createParticipantsMapBase, createRoundBracketSubColumnRelativeToFirstRound, createRoundsMapBase, createSingleEliminationGrid, createStackedDoubleEliminationGrid, createSwissGrid, curvePath, drawMan, drawSwissMan, finalizeBracketGrid, generateBracketRoundSwissGroupMaps, generateMatchPosition, generateMatchRelationPositions, generateMatchRelationsNew, generateRoundRelationsNew, getAvailableSwissGroupsForRound, getBracketContinueMatches, getSwissGroupColorType, gutterPath, isBracketContinueMatch, isBracketSlotPredictable, linePath, migrateBracketPicks, path, resolveBracketGridRowSpan, resolveBracketLayout, resolveBracketSlot, standingPickOutcome, standingPickStartOrder, verticalPath };
3723
3635
  //# sourceMappingURL=ethlete-bracket.mjs.map