react-bracket-ui
Version:
A modern, feature-rich React component library for displaying single-elimination tournament brackets with drag-drop, zoom/pan, and error validation
1,057 lines (1,050 loc) • 38.7 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Bracket: () => Bracket,
getMatchErrorMessage: () => getMatchErrorMessage,
hasMatchError: () => hasMatchError,
validateBracket: () => validateBracket
});
module.exports = __toCommonJS(index_exports);
// src/components/Bracket.tsx
var import_react3 = __toESM(require("react"));
var import_react_zoom_pan_pinch = require("react-zoom-pan-pinch");
// src/utils/bracketValidation.ts
function validateBracket(matches) {
const errors = [];
if (!matches || matches.length === 0) {
return errors;
}
const matchIds = /* @__PURE__ */ new Set();
matches.forEach((match) => {
if (matchIds.has(match.id)) {
errors.push({
matchId: match.id,
message: "Duplicate match ID detected",
type: "error"
});
}
matchIds.add(match.id);
});
matches.forEach((match) => {
if (match.nextMatchId && !matchIds.has(match.nextMatchId)) {
errors.push({
matchId: match.id,
message: `Invalid nextMatchId: ${match.nextMatchId}`,
type: "error"
});
}
});
matches.forEach((match) => {
if (match.participant1 && !match.participant1.id) {
errors.push({
matchId: match.id,
message: "Participant 1 missing ID",
type: "warning"
});
}
if (match.participant2 && !match.participant2.id) {
errors.push({
matchId: match.id,
message: "Participant 2 missing ID",
type: "warning"
});
}
});
matches.forEach((match) => {
if (match.winner) {
const participant1Id = match.participant1?.id;
const participant2Id = match.participant2?.id;
if (match.winner !== participant1Id && match.winner !== participant2Id) {
errors.push({
matchId: match.id,
message: "Winner ID does not match any participant",
type: "error"
});
}
}
});
return errors;
}
function hasMatchError(matchId, errors) {
return errors.some((error) => error.matchId === matchId);
}
function getMatchErrorMessage(matchId, errors) {
const matchErrors = errors.filter((error) => error.matchId === matchId);
return matchErrors.map((e) => e.message).join(", ");
}
// src/components/MatchComponent.tsx
var import_react = require("react");
var import_jsx_runtime = require("react/jsx-runtime");
var MatchComponent = ({
match,
hasError,
errorMessage,
enableDragDrop,
onDragStart,
onDragEnd,
onDragOver,
onDrop,
colors,
matchWidth,
matchHeight,
forwardedRef,
onScheduleChange,
schedule
}) => {
const isParticipant1Winner = match.winner === match.participant1?.id;
const isParticipant2Winner = match.winner === match.participant2?.id;
const matchStyles = {
border: hasError ? `2px solid ${colors.error}` : `1px solid #ccc`,
borderRadius: "6px",
padding: "8px",
backgroundColor: hasError ? "#ffebee" : "white",
display: "flex",
flexDirection: "column",
minWidth: `${matchWidth}px`,
minHeight: `${matchHeight}px`,
height: `${matchHeight}px`,
boxShadow: hasError ? `0 0 0 3px ${colors.error}33, 0 0 12px ${colors.error}` : "0 1px 3px rgba(0,0,0,0.1)",
transition: "all 0.2s ease",
position: "relative",
overflow: "visible"
// Đảm bảo tooltip không bị cắt
};
const participantStyles = (participant, isWinner, slot) => ({
padding: "8px 12px",
borderBottom: slot === "participant1" ? "1px solid #eee" : "none",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
fontWeight: isWinner ? "bold" : "normal",
backgroundColor: isWinner ? colors.winner : "transparent",
cursor: enableDragDrop && participant ? "grab" : "default",
transition: "background-color 0.2s ease",
borderRadius: "4px",
minHeight: "32px"
});
const renderParticipant = (participant, isWinner, slot) => {
if (!participant) {
const style = participantStyles(null, false, slot);
console.log(`[MatchComponent] Placeholder for match ${match.id} slot ${slot}:`, style);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
className: enableDragDrop ? "draggable-participant" : void 0,
style: {
...style,
fontStyle: "italic",
color: "#999",
cursor: enableDragDrop ? "pointer" : "default",
backgroundColor: enableDragDrop ? "#f5f5f5" : "transparent"
},
onDragOver: enableDragDrop ? onDragOver : void 0,
onDrop: enableDragDrop ? (e) => {
e.preventDefault();
onDrop(match.id, slot);
} : void 0,
children: "TBD"
}
);
}
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
"div",
{
className: enableDragDrop ? "draggable-participant" : void 0,
draggable: enableDragDrop,
onDragStart: enableDragDrop ? (e) => {
e.dataTransfer.effectAllowed = "move";
onDragStart(match.id, participant.id, slot);
} : void 0,
onDragEnd: enableDragDrop && onDragEnd ? onDragEnd : void 0,
onDragOver: enableDragDrop ? onDragOver : void 0,
onDrop: enableDragDrop ? (e) => {
e.preventDefault();
onDrop(match.id, slot);
} : void 0,
style: participantStyles(participant, isWinner, slot),
onMouseEnter: (e) => {
if (enableDragDrop && participant) {
e.currentTarget.style.backgroundColor = isWinner ? colors.winner : "#f0f0f0";
}
},
onMouseLeave: (e) => {
if (enableDragDrop && participant) {
e.currentTarget.style.backgroundColor = isWinner ? colors.winner : "transparent";
}
},
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 }, children: participant.name }),
typeof participant.score === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
fontWeight: "bold",
marginLeft: "8px",
fontSize: "14px",
color: colors.primary
}, children: participant.score })
]
}
);
};
console.log(`[MatchComponent] Render match ${match.id} style:`, matchStyles, {
participant1: match.participant1,
participant2: match.participant2
});
let formattedSchedule = "";
if (schedule) {
let dateObj = null;
if (typeof schedule === "string") {
const parsed = Date.parse(schedule);
if (!isNaN(parsed)) dateObj = new Date(parsed);
} else if (schedule instanceof Date) {
dateObj = schedule;
}
if (dateObj) {
const pad = (n) => n < 10 ? "0" + n : n;
const h = pad(dateObj.getHours());
const m = pad(dateObj.getMinutes());
const d = pad(dateObj.getDate());
const mo = pad(dateObj.getMonth() + 1);
const y = dateObj.getFullYear();
formattedSchedule = `${h}:${m}, ${d}/${mo}/${y}`;
} else if (typeof schedule === "string") {
formattedSchedule = schedule;
}
}
const [hovered, setHovered] = (0, import_react.useState)(false);
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
"div",
{
className: (enableDragDrop ? "match-container " : "") + "relative",
style: { ...matchStyles, zIndex: hasError && hovered ? 1050 : void 0 },
ref: forwardedRef,
onMouseEnter: () => setHovered(true),
onMouseLeave: () => setHovered(false),
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: {
fontSize: "11px",
color: hasError ? colors.error : "#666",
fontWeight: hasError ? "bold" : "normal",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
position: "relative",
gap: 8
}, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: [
"Match ",
match.matchNumber || match.id
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", alignItems: "center", gap: 4, flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"input",
{
type: "datetime-local",
value: (() => {
if (!schedule) return "";
let d = null;
if (typeof schedule === "string") {
const parsed = Date.parse(schedule);
if (!isNaN(parsed)) d = new Date(parsed);
} else if (schedule instanceof Date) {
d = schedule;
}
if (!d) return "";
const pad = (n) => n < 10 ? "0" + n : n;
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
})(),
onChange: (e) => {
if (typeof match.id !== "undefined" && typeof e.target.value === "string" && e.target.value && typeof onScheduleChange === "function") {
onScheduleChange(match.id, e.target.value);
}
},
style: {
fontSize: 12,
color: "#888",
fontWeight: 400,
border: "1px solid #ccc",
borderRadius: 4,
padding: "2px 4px",
background: "#fafbfc",
minWidth: 120,
maxWidth: 150
},
title: "Ch\u1EC9nh s\u1EEDa l\u1ECBch thi \u0111\u1EA5u"
}
) })
] }),
renderParticipant(match.participant1, isParticipant1Winner, "participant1"),
renderParticipant(match.participant2, isParticipant2Winner, "participant2"),
hasError && hovered && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
className: "absolute left-1/2 -translate-x-1/2 top-full mt-2 z-[1051] bg-red-600 text-white px-3 py-2 rounded shadow-lg text-sm",
style: {
pointerEvents: "none",
boxShadow: "0 0 0 2px #d32f2f, 0 4px 16px #d32f2faa",
background: "#d32f2f",
minWidth: 0,
maxWidth: matchWidth - 16,
wordBreak: "break-word",
whiteSpace: "pre-line",
textAlign: "left"
},
children: errorMessage
}
)
]
}
);
};
var MatchComponent_default = (0, import_react.memo)(MatchComponent);
// src/components/BracketRound.tsx
var import_react2 = require("react");
var import_jsx_runtime2 = require("react/jsx-runtime");
var BracketRound = ({
round,
matches,
totalRounds,
renderMatch,
matchHeight,
gap,
roundName,
showRoundNames,
primaryColor,
matchWidth,
onMatchPositionsChange,
zoomScale = 1
}) => {
const matchRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
const containerRef = (0, import_react2.useRef)(null);
(0, import_react2.useEffect)(() => {
if (!onMatchPositionsChange || !containerRef.current) return;
const positions = [];
const containerTop = containerRef.current.getBoundingClientRect().top;
matches.forEach((match) => {
const element = matchRefs.current.get(match.id);
if (element) {
const rect = element.getBoundingClientRect();
console.log(`[BracketRound] round ${round} match ${match.id} measured top:`, (rect.top - containerTop) / zoomScale, "height:", rect.height / zoomScale);
positions.push({
matchId: match.id,
top: (rect.top - containerTop) / zoomScale,
height: rect.height / zoomScale
});
}
});
onMatchPositionsChange(positions);
}, [matches, onMatchPositionsChange, zoomScale]);
const matchSpacing = round === 1 ? gap : (Math.pow(2, round - 1) - 1) * matchHeight + Math.pow(2, round - 1) * gap;
console.log(`[BracketRound] round ${round} matchSpacing:`, matchSpacing, "fixed matchHeight:", matchHeight, "gap:", gap);
const topPadding = round === 1 ? 0 : (matchHeight + gap) * (Math.pow(2, round - 1) - 1) / 2;
console.log(`[BracketRound] round ${round} topPadding:`, topPadding);
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: {
display: "flex",
flexDirection: "column",
minWidth: `${matchWidth + 20}px`
}, children: [
showRoundNames && roundName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h4", { style: {
margin: "0 0 12px 0",
color: primaryColor,
fontSize: "14px",
fontWeight: "600",
textAlign: "center",
height: "30px"
}, children: roundName }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"div",
{
ref: containerRef,
style: {
display: "flex",
flexDirection: "column",
paddingTop: `${topPadding}px`
},
children: matches.map((match, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"div",
{
ref: (el) => {
if (el) {
matchRefs.current.set(match.id, el);
} else {
matchRefs.current.delete(match.id);
}
},
style: {
marginBottom: index < matches.length - 1 ? `${matchSpacing}px` : "0",
visibility: match.isVirtual ? "hidden" : "visible"
},
children: renderMatch(match, index)
},
match.id
))
}
)
] });
};
var BracketRound_default = BracketRound;
// src/components/BracketConnectors.tsx
var import_jsx_runtime3 = require("react/jsx-runtime");
var BracketConnectors = ({
leftRoundMatches,
rightRoundMatches,
leftRound,
rightRound,
matchHeight,
gap,
color,
showRoundNames,
matchPositions,
zoomScale = 1
}) => {
const connectorWidth = 50;
const getActualMatchHeight = (matches) => {
if (matches.length === 0) return matchHeight;
let totalHeight2 = 0;
let count = 0;
matches.forEach((match) => {
const pos = matchPositions.get(match.id);
if (pos && pos.height > 0) {
totalHeight2 += pos.height;
count++;
console.log(`[BracketConnectors] getActualMatchHeight match ${match.id} height:`, pos.height);
}
});
return count > 0 ? totalHeight2 / count : matchHeight;
};
const actualLeftMatchHeight = getActualMatchHeight(leftRoundMatches);
const actualRightMatchHeight = getActualMatchHeight(rightRoundMatches);
const leftMatchSpacing = leftRound === 1 ? gap : (Math.pow(2, leftRound - 1) - 1) * actualLeftMatchHeight + Math.pow(2, leftRound - 1) * gap;
console.log(`[BracketConnectors] leftRound ${leftRound} leftMatchSpacing:`, leftMatchSpacing, "actualLeftMatchHeight:", actualLeftMatchHeight, "gap:", gap);
const leftTopPadding = leftRound === 1 ? 0 : (actualLeftMatchHeight + gap) * (Math.pow(2, leftRound - 1) - 1) / 2;
console.log(`[BracketConnectors] leftRound ${leftRound} leftTopPadding:`, leftTopPadding);
const rightMatchSpacing = rightRound === 1 ? gap : (Math.pow(2, rightRound - 1) - 1) * actualRightMatchHeight + Math.pow(2, rightRound - 1) * gap;
console.log(`[BracketConnectors] rightRound ${rightRound} rightMatchSpacing:`, rightMatchSpacing, "actualRightMatchHeight:", actualRightMatchHeight, "gap:", gap);
const rightTopPadding = rightRound === 1 ? 0 : (actualRightMatchHeight + gap) * (Math.pow(2, rightRound - 1) - 1) / 2;
console.log(`[BracketConnectors] rightRound ${rightRound} rightTopPadding:`, rightTopPadding);
const leftTotalHeight = leftTopPadding + leftRoundMatches.length * actualLeftMatchHeight + (leftRoundMatches.length - 1) * leftMatchSpacing;
const rightTotalHeight = rightTopPadding + rightRoundMatches.length * actualRightMatchHeight + (rightRoundMatches.length - 1) * rightMatchSpacing;
const totalHeight = Math.max(leftTotalHeight, rightTotalHeight);
const connectors = [];
rightRoundMatches.forEach((rightMatch, rightIndex) => {
let leftMatches = leftRoundMatches.filter(
(m) => m.nextMatchId === rightMatch.id
);
if (leftRound === 1) {
leftMatches = leftMatches.filter((m) => !m.isVirtual);
}
if (leftMatches.length === 0) return;
const rightPos = matchPositions.get(rightMatch.id);
if (!rightPos) return;
const rightY = rightPos.top + rightPos.height / 2;
if (leftMatches.length === 1) {
const leftMatch = leftMatches[0];
const leftPos = matchPositions.get(leftMatch.id);
if (!leftPos) return;
const leftY = leftPos.top + leftPos.height / 2;
const meetX = connectorWidth / 2;
connectors.push(
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"line",
{
x1: "0",
y1: leftY,
x2: meetX,
y2: leftY,
stroke: color,
strokeWidth: "2"
},
`connector-h-left-${leftMatch.id}`
)
);
connectors.push(
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"line",
{
x1: meetX,
y1: leftY,
x2: meetX,
y2: rightY,
stroke: color,
strokeWidth: "2"
},
`connector-v-${leftMatch.id}-${rightMatch.id}`
)
);
connectors.push(
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"line",
{
x1: meetX,
y1: rightY,
x2: connectorWidth,
y2: rightY,
stroke: color,
strokeWidth: "2"
},
`connector-h-right-${rightMatch.id}`
)
);
} else {
const leftYPositions = [];
leftMatches.forEach((leftMatch) => {
const leftPos = matchPositions.get(leftMatch.id);
if (!leftPos) return;
const leftY = leftPos.top + leftPos.height / 2;
leftYPositions.push(leftY);
});
if (leftYPositions.length === 0) return;
const meetX = connectorWidth / 2;
const minLeftY = Math.min(...leftYPositions);
const maxLeftY = Math.max(...leftYPositions);
leftMatches.forEach((leftMatch) => {
const leftPos = matchPositions.get(leftMatch.id);
if (!leftPos) return;
const leftY = leftPos.top + leftPos.height / 2;
connectors.push(
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"line",
{
x1: "0",
y1: leftY,
x2: meetX,
y2: leftY,
stroke: color,
strokeWidth: "2"
},
`connector-h-${leftMatch.id}`
)
);
});
connectors.push(
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"line",
{
x1: meetX,
y1: minLeftY,
x2: meetX,
y2: maxLeftY,
stroke: color,
strokeWidth: "2"
},
`connector-v-${rightMatch.id}`
)
);
connectors.push(
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"line",
{
x1: meetX,
y1: rightY,
x2: connectorWidth,
y2: rightY,
stroke: color,
strokeWidth: "2"
},
`connector-r-${rightMatch.id}`
)
);
}
});
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
width: `${connectorWidth}px`,
display: "flex",
flexDirection: "column",
position: "relative"
}, children: [
showRoundNames && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { height: "42px" } }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"svg",
{
width: connectorWidth,
height: totalHeight,
style: {
overflow: "visible"
},
children: connectors
}
)
] });
};
var BracketConnectors_default = BracketConnectors;
// src/components/Bracket.tsx
var import_jsx_runtime4 = require("react/jsx-runtime");
var DEFAULT_COLORS = {
primary: "#1976d2",
secondary: "#424242",
background: "#f9f9f9",
error: "#d32f2f",
warning: "#ff9800",
winner: "#e8f5e9"
};
function Bracket({
matches,
className,
style,
enableDragDrop = false,
onBracketChange,
enableZoomPan = false,
minZoom = 0.5,
maxZoom = 3,
initialZoom = 1,
errors: externalErrors,
onErrorClick,
showRoundNames = true,
roundNames,
matchWidth = 220,
matchHeight = 100,
gap = 20,
colors: customColors,
onScheduleChange,
schedules = {}
}) {
const [internalMatches, setInternalMatches] = (0, import_react3.useState)(matches);
const [isDragging, setIsDragging] = (0, import_react3.useState)(false);
const [matchPositions, setMatchPositions] = (0, import_react3.useState)(/* @__PURE__ */ new Map());
const [currentScale, setCurrentScale] = (0, import_react3.useState)(initialZoom);
const dragDataRef = (0, import_react3.useRef)(null);
const [fixedMatchHeight, setFixedMatchHeight] = (0, import_react3.useState)(matchHeight);
const round1MatchRef = (0, import_react3.useRef)(null);
const bracketContentRef = (0, import_react3.useRef)(null);
const [viewportSize, setViewportSize] = (0, import_react3.useState)({ width: 0, height: 0 });
const [contentSize, setContentSize] = (0, import_react3.useState)({ width: 0, height: 0 });
const [limitPan, setLimitPan] = (0, import_react3.useState)(true);
const colors = (0, import_react3.useMemo)(() => ({
...DEFAULT_COLORS,
...customColors
}), [customColors]);
const allErrors = (0, import_react3.useMemo)(() => {
const validationErrors = validateBracket(internalMatches);
return externalErrors ? [...validationErrors, ...externalErrors] : validationErrors;
}, [internalMatches, externalErrors]);
import_react3.default.useEffect(() => {
setInternalMatches(matches);
}, [matches]);
const groupedMatches = (0, import_react3.useMemo)(() => {
return internalMatches.reduce((acc, match) => {
if (!acc[match.round]) {
acc[match.round] = [];
}
acc[match.round].push(match);
return acc;
}, {});
}, [internalMatches]);
const rounds = (0, import_react3.useMemo)(
() => Object.keys(groupedMatches).map(Number).sort((a, b) => a - b),
[groupedMatches]
);
const handleDragStart = (0, import_react3.useCallback)((matchId, participantId, slot) => {
setIsDragging(true);
dragDataRef.current = {
fromMatchId: matchId,
participantId,
participantSlot: slot
};
}, []);
const handleDragEnd = (0, import_react3.useCallback)(() => {
setIsDragging(false);
dragDataRef.current = null;
}, []);
const handleDragOver = (0, import_react3.useCallback)((e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
}, []);
const handleDrop = (0, import_react3.useCallback)((toMatchId, toSlot) => {
if (!dragDataRef.current || !enableDragDrop) return;
const { fromMatchId, participantId, participantSlot } = dragDataRef.current;
if (fromMatchId === toMatchId && participantSlot === toSlot) {
dragDataRef.current = null;
setIsDragging(false);
return;
}
const sourceMatch = internalMatches.find((m) => m.id === fromMatchId);
const targetMatch = internalMatches.find((m) => m.id === toMatchId);
if (!sourceMatch || !targetMatch) {
dragDataRef.current = null;
setIsDragging(false);
return;
}
const sourceParticipant = sourceMatch[participantSlot];
const targetSlot = toSlot;
const targetParticipant = targetMatch[targetSlot];
const updatedMatches = internalMatches.map((match) => {
if (match.id === fromMatchId) {
return {
...match,
[participantSlot]: targetParticipant || null
};
} else if (match.id === toMatchId) {
return {
...match,
[targetSlot]: sourceParticipant || null
};
} else {
return match;
}
});
setInternalMatches(updatedMatches);
if (onBracketChange) {
const dragDropResult = {
fromMatchId,
toMatchId,
participantId,
participantSlot: toSlot
};
const event = {
type: "drag-drop",
matches: updatedMatches,
dragDropResult
};
onBracketChange(event);
}
dragDataRef.current = null;
setIsDragging(false);
}, [internalMatches, enableDragDrop, onBracketChange]);
const renderMatch = (0, import_react3.useCallback)((match, round, matchIndex) => {
const hasError = hasMatchError(match.id, allErrors);
const errorMessage = getMatchErrorMessage(match.id, allErrors);
const ref = round === 1 && matchIndex === 0 ? round1MatchRef : void 0;
const schedule = schedules && match.id in schedules ? schedules[match.id] : void 0;
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
MatchComponent_default,
{
match,
hasError,
errorMessage,
enableDragDrop,
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,
onDragOver: handleDragOver,
onDrop: handleDrop,
colors,
matchWidth,
matchHeight: fixedMatchHeight,
forwardedRef: ref,
onScheduleChange,
schedule
},
match.id
);
}, [allErrors, enableDragDrop, handleDragStart, handleDragEnd, handleDragOver, handleDrop, colors, matchWidth, fixedMatchHeight, schedules]);
(0, import_react3.useEffect)(() => {
if (round1MatchRef.current && (!enableZoomPan || currentScale === 1)) {
const height = round1MatchRef.current.getBoundingClientRect().height;
if (height > 0 && Math.abs(height - fixedMatchHeight) > 1) {
setFixedMatchHeight(height);
console.log("[Bracket] Measured fixedMatchHeight from round 1 (scale=1):", height);
}
}
}, [internalMatches, enableZoomPan, currentScale]);
const getRoundName = (0, import_react3.useCallback)((round) => {
if (roundNames && roundNames[round]) {
return roundNames[round];
}
const totalRounds = rounds.length;
const roundsFromEnd = totalRounds - round + 1;
if (roundsFromEnd === 1) return "Final";
if (roundsFromEnd === 2) return "Semi-Final";
if (roundsFromEnd === 3) return "Quarter-Final";
return `Round ${round}`;
}, [rounds, roundNames]);
const handleMatchPositionsChange = (0, import_react3.useCallback)((positions) => {
setMatchPositions((prev) => {
const newMap = new Map(prev);
positions.forEach(({ matchId, top, height }) => {
newMap.set(matchId, { top, height });
});
return newMap;
});
}, []);
const defaultStyles = {
border: "1px solid #ddd",
borderRadius: "8px",
padding: "16px",
fontFamily: "Arial, sans-serif",
backgroundColor: colors.background,
height: "600px",
overflow: "hidden"
};
(0, import_react3.useEffect)(() => {
if (!enableZoomPan) return;
const handleResize = () => {
if (bracketContentRef.current) {
const contentRect = bracketContentRef.current.getBoundingClientRect();
setContentSize({ width: contentRect.width, height: contentRect.height });
}
const parent = bracketContentRef.current?.parentElement;
if (parent) {
const viewportRect = parent.getBoundingClientRect();
setViewportSize({ width: viewportRect.width, height: viewportRect.height });
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [enableZoomPan, fixedMatchHeight, internalMatches]);
(0, import_react3.useEffect)(() => {
if (!enableZoomPan) return;
if (contentSize.width > viewportSize.width || contentSize.height > viewportSize.height) {
setLimitPan(false);
} else {
setLimitPan(true);
}
}, [contentSize, viewportSize, enableZoomPan]);
const bracketContent = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { ref: enableZoomPan ? bracketContentRef : void 0, style: { padding: enableZoomPan ? "20px" : "0", position: "relative" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { style: { margin: "0 0 16px 0", color: colors.secondary }, children: "Tournament Bracket" }),
rounds.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
textAlign: "center",
color: "#999",
fontStyle: "italic",
padding: "40px 0"
}, children: "No matches available" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
display: "flex",
alignItems: "flex-start",
gap: "0px",
minWidth: "min-content"
}, children: rounds.map((round, index) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react3.default.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
BracketRound_default,
{
round,
matches: groupedMatches[round],
totalRounds: rounds.length,
renderMatch: (match, matchIdx) => renderMatch(match, round, matchIdx),
matchHeight: fixedMatchHeight,
matchWidth,
gap,
roundName: getRoundName(round),
showRoundNames,
primaryColor: colors.primary,
onMatchPositionsChange: handleMatchPositionsChange,
zoomScale: enableZoomPan ? currentScale : 1
}
),
index < rounds.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
BracketConnectors_default,
{
leftRoundMatches: groupedMatches[round],
rightRoundMatches: groupedMatches[rounds[index + 1]],
leftRound: round,
rightRound: rounds[index + 1],
matchHeight: fixedMatchHeight,
gap,
color: colors.secondary,
showRoundNames,
matchPositions,
zoomScale: enableZoomPan ? currentScale : 1
}
)
] }, round)) })
] });
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
"div",
{
className,
style: { ...defaultStyles, ...style, position: "relative", overflow: "hidden" },
children: [
enableZoomPan && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
position: "absolute",
bottom: "24px",
right: "24px",
zIndex: 100,
display: "flex",
gap: "8px",
backgroundColor: "white",
padding: "8px",
borderRadius: "6px",
boxShadow: "0 2px 8px rgba(0,0,0,0.15)"
} }),
enableZoomPan ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
import_react_zoom_pan_pinch.TransformWrapper,
{
initialScale: initialZoom,
minScale: 0.25,
maxScale: 2,
wheel: { disabled: true },
doubleClick: { disabled: true },
panning: {
disabled: isDragging,
excluded: enableDragDrop ? ["draggable-participant", "match-container"] : [],
velocityDisabled: false
},
limitToBounds: false,
centerOnInit: true,
onTransformed: (ref, state) => {
setCurrentScale(state.scale);
},
children: ({ zoomIn, zoomOut, resetTransform, setTransform }) => {
const zoomToLevel = (level) => {
setTransform(0, 0, level, 300, "easeOut");
};
const zoomLevels = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
const currentLevelIndex = zoomLevels.findIndex((level) => Math.abs(level - currentScale) < 0.01);
const canZoomIn = currentLevelIndex < zoomLevels.length - 1;
const canZoomOut = currentLevelIndex > 0;
const handleZoomIn = () => {
if (canZoomIn) {
zoomToLevel(zoomLevels[currentLevelIndex + 1]);
}
};
const handleZoomOut = () => {
if (canZoomOut) {
zoomToLevel(zoomLevels[currentLevelIndex - 1]);
}
};
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
position: "absolute",
bottom: "24px",
right: "24px",
zIndex: 100,
display: "flex",
gap: "8px",
backgroundColor: "white",
padding: "8px",
borderRadius: "6px",
boxShadow: "0 2px 8px rgba(0,0,0,0.15)"
}, children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"button",
{
onClick: handleZoomOut,
disabled: !canZoomOut,
style: {
padding: "6px 12px",
border: "1px solid #ddd",
borderRadius: "4px",
backgroundColor: "white",
cursor: canZoomOut ? "pointer" : "not-allowed",
fontSize: "16px",
opacity: canZoomOut ? 1 : 0.5
},
title: "Zoom Out",
children: "\u2212"
}
),
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: {
padding: "6px 12px",
fontSize: "14px",
fontWeight: "bold",
display: "flex",
alignItems: "center",
minWidth: "60px",
justifyContent: "center"
}, children: [
Math.round(currentScale * 100),
"%"
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"button",
{
onClick: handleZoomIn,
disabled: !canZoomIn,
style: {
padding: "6px 12px",
border: "1px solid #ddd",
borderRadius: "4px",
backgroundColor: "white",
cursor: canZoomIn ? "pointer" : "not-allowed",
fontSize: "16px",
opacity: canZoomIn ? 1 : 0.5
},
title: "Zoom In",
children: "+"
}
),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"button",
{
onClick: () => zoomToLevel(1),
style: {
padding: "6px 12px",
border: "1px solid #ddd",
borderRadius: "4px",
backgroundColor: "white",
cursor: "pointer",
fontSize: "12px"
},
title: "Reset to 100%",
children: "\u21BA"
}
),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"button",
{
onClick: () => setTransform(0, 0, currentScale, 300, "easeOut"),
style: {
padding: "6px 12px",
border: "1px solid #1976d2",
borderRadius: "4px",
backgroundColor: "#1976d2",
color: "white",
cursor: "pointer",
fontSize: "12px",
fontWeight: 600
},
title: "Center Bracket",
children: "Center"
}
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
import_react_zoom_pan_pinch.TransformComponent,
{
wrapperStyle: {
width: "100%",
height: "100%"
},
contentStyle: {
width: "100%",
height: "100%"
},
children: bracketContent
}
)
] });
}
}
) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
width: "100%",
height: "100%",
overflow: "auto"
}, children: bracketContent })
]
}
);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Bracket,
getMatchErrorMessage,
hasMatchError,
validateBracket
});
//# sourceMappingURL=index.js.map